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
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Agent-only TypeScript SDK and `dss` CLI for Dataiku DSS automation.
|
|
|
4
4
|
|
|
5
5
|
## Platform support
|
|
6
6
|
|
|
7
|
-
The published `dss` CLI
|
|
7
|
+
The published `dss` CLI requires Bun >= 1.4.0 and supports Linux, macOS, and Windows. The release gate runs Bun 1.4.0 on all three operating systems. The runtime dependency is pure JavaScript, so the same package also runs on every Bun-supported x64 and ARM64 system.
|
|
8
8
|
|
|
9
9
|
Run directly with Bun:
|
|
10
10
|
|
|
@@ -17,9 +17,11 @@ Or install the npm binary:
|
|
|
17
17
|
```text
|
|
18
18
|
npm install --global dataiku-sdk
|
|
19
19
|
```
|
|
20
|
+
The installed `dss` bin bootstraps Bun with automatic `.env` preloading disabled, so Bun >= 1.4.0 must be on `PATH`; the CLI's own `.env` handling stays authoritative. `bunx --bun` forces Bun's Node-compatibility mode, which provides the same no-preload guarantee across operating systems.
|
|
20
21
|
|
|
21
|
-
Bun is the
|
|
22
|
+
Bun is the only supported runtime and the package manager. Examples below assume an installed `dss` binary. From a checkout, use `bun --no-env-file src/cli.ts ...` or the packaged launcher `bun --no-env-file ./bin/dss.js ...`. From another working directory, pass the checkout's absolute `bin/dss.js` path to Bun.
|
|
22
23
|
`--no-env-file` disables Bun's automatic preloading only; the CLI still applies its documented `.env` handling unless `DATAIKU_DISABLE_ENV=1` is set.
|
|
24
|
+
`dss version` reports whether the running code came from `source` or `dist`, the runtime (always `bun`), the packaged build revision when available, and `staleBuild` when that revision differs from the checkout. Release builds generate `dist/build-metadata.json`; source snapshots without Git metadata still build but report no build revision.
|
|
23
25
|
|
|
24
26
|
## CLI contract
|
|
25
27
|
|
|
@@ -119,7 +121,14 @@ The command saves credentials and returns `{ "saved": true, "path": "..." }`.
|
|
|
119
121
|
dss version
|
|
120
122
|
dss doctor --fast
|
|
121
123
|
dss project list
|
|
124
|
+
dss project inspect-archive ./project.zip
|
|
125
|
+
dss project import ./project.zip --target-project-key IMPORTED_PROJECT --record-cleanup cleanup.jsonl
|
|
122
126
|
dss dataset list --project-key MYPROJ
|
|
127
|
+
dss dataset files uploaded_input --project-key MYPROJ
|
|
128
|
+
dss dataset upload-file uploaded_input ./new.csv --file-name new.csv --project-key MYPROJ
|
|
129
|
+
dss dataset assert-count jmp_doe_ready --expected 12 --project-key MYPROJ
|
|
130
|
+
dss dataset assert-schema jmp_doe_ready --data-file expected-schema.json --project-key MYPROJ
|
|
131
|
+
dss data-quality assert-results jmp_doe_ready --project-key MYPROJ
|
|
123
132
|
dss recipe get-payload compute_orders --project-key MYPROJ
|
|
124
133
|
dss recipe get-payload compute_orders --output code.py --project-key MYPROJ
|
|
125
134
|
dss install-skill --dry-run
|
|
@@ -127,7 +136,8 @@ dss app validate-manifest --project-key MYAPP_INSTANCE
|
|
|
127
136
|
dss app compare-manifest my-app --project-key MYAPP_INSTANCE
|
|
128
137
|
dss app create-instance my-app --data '{"targetProjectKey":"MYAPP_INSTANCE"}' --wait --record-cleanup cleanup.jsonl
|
|
129
138
|
dss app manifest-version --project-key MYAPP_TEMPLATE
|
|
130
|
-
dss app
|
|
139
|
+
dss app successor-preflight my-app --from MYAPP_INSTANCE --to MYAPP_INSTANCE_V2 --copy-permissions
|
|
140
|
+
dss app set-manifest-version --manifest-version 1.4.0 --expect-hash PREFLIGHT_TEMPLATE_MANIFEST_HASH --project-key MYAPP_TEMPLATE
|
|
131
141
|
dss app create-successor-instance my-app --from MYAPP_INSTANCE --to MYAPP_INSTANCE_V2 --copy-permissions --record-cleanup cleanup.jsonl
|
|
132
142
|
dss app verify-instance my-app --project-key MYAPP_INSTANCE_V2 --expect-version 1.4.0
|
|
133
143
|
dss app permissions-snapshot --project-key MYAPP_INSTANCE --output permissions.json
|
|
@@ -136,6 +146,28 @@ dss app permissions-restore --project-key MYAPP_INSTANCE --file permissions.json
|
|
|
136
146
|
dss cleanup --file cleanup.jsonl
|
|
137
147
|
dss cleanup --file cleanup.jsonl --apply
|
|
138
148
|
```
|
|
149
|
+
`project import` performs both public API stages: archive upload, then import processing. It
|
|
150
|
+
first validates ZIP integrity, safe unique member paths, the export manifest, and provable Flow
|
|
151
|
+
references locally. Verified success reports DSS's actual project key, explicit remapping, and a
|
|
152
|
+
`creationTag` incarnation hash; `--record-cleanup` records an incarnation-bound guarded delete.
|
|
153
|
+
It exits nonzero when DSS returns `success:false` even if both HTTP requests succeeded. A timeout,
|
|
154
|
+
5xx, malformed response, missing used key, or failed post-import identity read is
|
|
155
|
+
`ambiguous_outcome` and retains the temporary import ID for inspection before retry.
|
|
156
|
+
|
|
157
|
+
`dataset upload-file` only adds a filename that is not already present and verifies its byte
|
|
158
|
+
length afterward. DSS 14.7 exposes no public UploadedFiles delete or replacement endpoint;
|
|
159
|
+
the command refuses an existing filename before POST instead of triggering DSS's opaque 500.
|
|
160
|
+
For replacement, import a successor project archive containing the desired upload.
|
|
161
|
+
|
|
162
|
+
`dataset preview` returns `truncated` and `limit`; it probes one row past the requested cap instead
|
|
163
|
+
of presenting a capped sample as an exact count. `dataset assert-count`, `dataset assert-schema`,
|
|
164
|
+
and `data-quality assert-results` return `satisfied` and exit `4`/`assertion_failed` on a failed
|
|
165
|
+
postcondition, so they can be used as `batch` steps. Multi-job `job monitor` and `job watch`
|
|
166
|
+
aggregate child failures and also exit `4` when any watched job fails.
|
|
167
|
+
|
|
168
|
+
`doctor` reads the documented `DSS-Version` and `DSS-API-Version` response headers plus the HTTP
|
|
169
|
+
`Date` header from the public project-list call. Feature support remains behavioral: the CLI does
|
|
170
|
+
not call an undocumented feature endpoint or infer licensed capabilities from a version string.
|
|
139
171
|
|
|
140
172
|
### Application release safety
|
|
141
173
|
|
|
@@ -152,6 +184,12 @@ dss app compare-manifest APP_ID --project-key RELEASE_INSTANCE
|
|
|
152
184
|
# Read the raw `version` from the public app manifest (string fields only; never appVersion).
|
|
153
185
|
dss app manifest-version --project-key APP_TEMPLATE
|
|
154
186
|
|
|
187
|
+
# Run every read-only successor gate before changing the template version. This validates the
|
|
188
|
+
# template references, verifies the predecessor, proves target absence, and optionally snapshots
|
|
189
|
+
# the predecessor ACL. Keep template.manifestHash from the result for the next command.
|
|
190
|
+
dss app successor-preflight APP_ID \
|
|
191
|
+
--from RELEASE_INSTANCE --to RELEASE_INSTANCE_V2 --copy-permissions
|
|
192
|
+
|
|
155
193
|
# Write version/versionNotes through the public app-manifest endpoint. This is NOT a publish
|
|
156
194
|
# transaction: it only changes the raw manifest metadata. --expect-hash SHA256 is a non-atomic
|
|
157
195
|
# stale-read guard, not a conditional write: it refuses the write when the manifest already
|
|
@@ -159,7 +197,8 @@ dss app manifest-version --project-key APP_TEMPLATE
|
|
|
159
197
|
# overwrite a write landing between its read and PUT, and the final read cannot detect that lost
|
|
160
198
|
# update when this command's payload wins. Every result reports
|
|
161
199
|
# concurrencyControl: "client-side-non-atomic-stale-read-check".
|
|
162
|
-
dss app set-manifest-version --manifest-version 1.4.0
|
|
200
|
+
dss app set-manifest-version --manifest-version 1.4.0 \
|
|
201
|
+
--expect-hash PREFLIGHT_TEMPLATE_MANIFEST_HASH --project-key APP_TEMPLATE
|
|
163
202
|
|
|
164
203
|
# Create an instance only after its target key is confirmed absent, wait on the DSS future, and
|
|
165
204
|
# record deterministic cleanup.
|
|
@@ -170,7 +209,8 @@ dss app create-instance APP_ID \
|
|
|
170
209
|
# Roll out a new template version to an existing instance as an additive successor: the old
|
|
171
210
|
# instance is never modified or deleted, and the command always waits on the DSS future
|
|
172
211
|
# (there is no --wait flag). The recorded cleanup entry targets only the new project key. If DSS
|
|
173
|
-
#
|
|
212
|
+
# may have accepted creation but no future ID is returned, the outcome is indeterminate and no
|
|
213
|
+
# unbound cleanup entry is written.
|
|
174
214
|
dss app create-successor-instance APP_ID \
|
|
175
215
|
--from RELEASE_INSTANCE --to RELEASE_INSTANCE_V2 \
|
|
176
216
|
--name "Release instance v2" --copy-permissions --record-cleanup cleanup.jsonl
|
|
@@ -203,18 +243,32 @@ managed-folder IDs, and runtime-form parameter names against supported public pr
|
|
|
203
243
|
|
|
204
244
|
Some DSS deployments hide unknown project keys behind `403` instead of returning `404`.
|
|
205
245
|
Instance creation requires confirmed target absence before POST: an inaccessible target not present
|
|
206
|
-
in the visible project
|
|
207
|
-
|
|
246
|
+
in the visible project and app-instance lists is still unconfirmed and is rejected with
|
|
247
|
+
`target_absence_unverifiable` / `permission_or_environment` rather than risking cleanup against a
|
|
248
|
+
pre-existing project. Choosing another key does not solve deployments that mask every unknown key.
|
|
249
|
+
DSS exposes no permission-independent public availability endpoint, and its published app-instance
|
|
250
|
+
API does not guarantee duplicate-key rejection before any write or non-overwrite behavior, so
|
|
251
|
+
there is no unsafe force or unconfirmed-target bypass. Use an identity with global project
|
|
252
|
+
visibility. A definitive create rejection never produces a cleanup entry; an ambiguous POST
|
|
253
|
+
without a future ID or verified
|
|
254
|
+
incarnation also produces no cleanup entry.
|
|
255
|
+
|
|
256
|
+
Static create plans describe, but do not execute, these reads:
|
|
257
|
+
`preflightExecuted:false` and `preflightWillRunDuringApply:true`. Run `successor-preflight` when
|
|
258
|
+
live read evidence is required before changing the template version.
|
|
208
259
|
|
|
209
260
|
Every new cleanup entry records the canonical DSS URL. App-instance cleanup records a
|
|
210
|
-
`creationTag` hash observed after the DSS future identifies the target key;
|
|
211
|
-
stops unresolved
|
|
261
|
+
`creationTag` hash observed after the DSS future identifies the target key; a future-addressable
|
|
262
|
+
entry stops unresolved until the future names that key and its incarnation can be bound. An
|
|
263
|
+
ambiguous POST with no future ID produces no entry. The future target and later `creationTag` are independent, non-atomic
|
|
212
264
|
observations: the public API exposes neither an immutable project ID joined to the future nor a
|
|
213
265
|
conditional DELETE. Cleanup rechecks type and `creationTag` immediately before deletion, rejecting
|
|
214
266
|
detected key reuse, but cannot eliminate replacement in the remaining check-to-DELETE gap.
|
|
215
267
|
`cleanup --apply` validates the full ledger before issuing any request and rejects legacy entries
|
|
216
268
|
without server identity, mixed-server ledgers, entries for another DSS URL, and app cleanup entries
|
|
217
|
-
without a valid incarnation binding.
|
|
269
|
+
without a valid incarnation binding. Project-import cleanup uses the same rule: the ledger binds
|
|
270
|
+
the actual imported key and verified `creationTag` hash, and `project delete
|
|
271
|
+
--expect-project-incarnation` refuses a reused or unverifiable key before DELETE.
|
|
218
272
|
|
|
219
273
|
Saving `version`/`versionNotes` in the public app manifest is a metadata write, not a publish
|
|
220
274
|
transaction. If the manifest PUT or its verification read has an ambiguous transport/server
|
package/bin/dss.js
CHANGED
|
@@ -1,98 +1,87 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// npm bin for the dss CLI. Bun is the only runtime that executes CLI code.
|
|
3
|
+
// A Node-launched npm shim only bootstraps Bun with automatic .env loading
|
|
4
|
+
// disabled; `bunx --bun` enters Bun's Node-compatibility mode, which disables
|
|
5
|
+
// that loading before this module runs.
|
|
2
6
|
import { spawnSync, } from "node:child_process";
|
|
3
|
-
import * as fs from "node:fs";
|
|
4
7
|
import * as path from "node:path";
|
|
5
8
|
import { fileURLToPath, pathToFileURL, } from "node:url";
|
|
6
9
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const runningUnderBun = typeof process.versions.bun === "string";
|
|
11
|
+
const bunNodeCompatibilityMode = runningUnderBun
|
|
12
|
+
&& (
|
|
13
|
+
path.basename(process.argv0,).toLowerCase().startsWith("node",)
|
|
14
|
+
|| (
|
|
15
|
+
process.env.npm_lifecycle_event === "bunx"
|
|
16
|
+
&& ["dss", "dataiku-sdk",].includes(process.env.npm_lifecycle_script ?? "",)
|
|
17
|
+
&& /^bun(?:\.exe)?$/i.test(path.basename(process.env.npm_execpath ?? "",),)
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
const envFileAutoloadDisabled = process.execArgv.includes("--no-env-file",)
|
|
21
|
+
|| bunNodeCompatibilityMode;
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function hasFlag(names,) {
|
|
27
|
-
return names.some((name,) => optionArgs.includes(name,));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function loadSavedTlsSettings() {
|
|
31
|
-
try {
|
|
32
|
-
const { loadCredentials, } = await import("../dist/src/config.js");
|
|
33
|
-
const creds = loadCredentials();
|
|
34
|
-
return {
|
|
35
|
-
caCertPath: creds?.caCertPath,
|
|
36
|
-
tlsRejectUnauthorized: creds?.tlsRejectUnauthorized,
|
|
37
|
-
};
|
|
38
|
-
} catch {
|
|
39
|
-
return {};
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const savedTls = await loadSavedTlsSettings();
|
|
44
|
-
const env = { ...process.env, };
|
|
45
|
-
const explicitCaCert = flagValue(["--ca-cert", "--extra-ca-certs",],);
|
|
46
|
-
if (explicitCaCert) {
|
|
47
|
-
env.NODE_EXTRA_CA_CERTS = explicitCaCert;
|
|
48
|
-
} else if (!env.NODE_EXTRA_CA_CERTS && savedTls.caCertPath) {
|
|
49
|
-
env.NODE_EXTRA_CA_CERTS = savedTls.caCertPath;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (hasFlag(["--insecure", "--skip-tls-verify",],)) {
|
|
53
|
-
env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
54
|
-
} else if (
|
|
55
|
-
env.NODE_TLS_REJECT_UNAUTHORIZED === undefined && savedTls.tlsRejectUnauthorized === false
|
|
56
|
-
) {
|
|
57
|
-
env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const runningOnBun = Boolean(process.versions.bun,);
|
|
61
|
-
const usesSourceCli = cliPath === sourceCliPath;
|
|
62
|
-
const runtimeBin = runningOnBun ? process.execPath : usesSourceCli ? "bun" : process.execPath;
|
|
63
|
-
const runtimeArgs = runningOnBun || usesSourceCli ? ["--no-env-file",] : ["--use-system-ca",];
|
|
64
|
-
const result = spawnSync(
|
|
65
|
-
runtimeBin,
|
|
66
|
-
[...runtimeArgs, cliPath, ...args,],
|
|
67
|
-
{
|
|
68
|
-
stdio: "inherit",
|
|
69
|
-
env,
|
|
70
|
-
},
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
if (result.error) {
|
|
74
|
-
const message = usesSourceCli
|
|
75
|
-
? `Unable to start Bun runtime for source dss CLI (${result.error.message}).`
|
|
76
|
-
: `Unable to start ${
|
|
77
|
-
runningOnBun ? "Bun" : "Node"
|
|
78
|
-
} runtime for packaged dss CLI (${result.error.message}); falling back to the current runtime.`;
|
|
79
|
-
if (usesSourceCli) {
|
|
23
|
+
if (!runningUnderBun) {
|
|
24
|
+
const result = spawnSync(
|
|
25
|
+
"bun",
|
|
26
|
+
["--no-env-file", fileURLToPath(import.meta.url,), ...process.argv.slice(2,),],
|
|
27
|
+
{ stdio: "inherit", env: process.env, },
|
|
28
|
+
);
|
|
29
|
+
if (result.error) {
|
|
80
30
|
process.stdout.write(`${
|
|
81
31
|
JSON.stringify({
|
|
82
32
|
type: "error",
|
|
83
33
|
ok: false,
|
|
84
|
-
error:
|
|
34
|
+
error: "Unable to start the required Bun runtime.",
|
|
85
35
|
code: "internal_error",
|
|
86
36
|
category: "internal",
|
|
87
37
|
exitCode: 2,
|
|
38
|
+
hint: "Install Bun >= 1.4.0 and ensure `bun` is on PATH.",
|
|
88
39
|
},)
|
|
89
40
|
}\n`,);
|
|
90
41
|
process.exitCode = 2;
|
|
42
|
+
} else if (result.signal) {
|
|
43
|
+
process.kill(process.pid, result.signal,);
|
|
91
44
|
} else {
|
|
92
|
-
|
|
45
|
+
process.exitCode = result.status ?? 1;
|
|
93
46
|
}
|
|
94
|
-
} else if (
|
|
95
|
-
process.
|
|
47
|
+
} else if (!envFileAutoloadDisabled) {
|
|
48
|
+
process.stdout.write(`${
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
type: "error",
|
|
51
|
+
ok: false,
|
|
52
|
+
error: "Bun automatic .env loading must be disabled for the dss launcher.",
|
|
53
|
+
code: "env_autoload_enabled",
|
|
54
|
+
category: "usage",
|
|
55
|
+
exitCode: 1,
|
|
56
|
+
hint: "Use `bunx --bun dataiku-sdk` or pass `--no-env-file` before the script path.",
|
|
57
|
+
},)
|
|
58
|
+
}\n`,);
|
|
59
|
+
process.exitCode = 1;
|
|
96
60
|
} else {
|
|
97
|
-
|
|
61
|
+
const here = import.meta.dir;
|
|
62
|
+
const distCliPath = path.resolve(here, "../dist/src/cli.js",);
|
|
63
|
+
const sourceCliPath = path.resolve(here, "../src/cli.ts",);
|
|
64
|
+
const usesDistCli = await Bun.file(distCliPath,).exists();
|
|
65
|
+
const cliPath = usesDistCli ? distCliPath : sourceCliPath;
|
|
66
|
+
|
|
67
|
+
// Published dist carries dist/build-metadata.json with the revision it was
|
|
68
|
+
// built from. Hand it to the CLI so provenance can report dist source and its
|
|
69
|
+
// build revision; only a full lowercase hexadecimal revision is accepted, so an
|
|
70
|
+
// inherited or corrupt variable never stands in for packaged metadata.
|
|
71
|
+
if (usesDistCli) {
|
|
72
|
+
process.env.DSS_LOAD_SOURCE = "dist";
|
|
73
|
+
delete process.env.DSS_BUILD_REVISION;
|
|
74
|
+
try {
|
|
75
|
+
const metadata = await Bun.file(path.resolve(here, "../dist/build-metadata.json",),).json();
|
|
76
|
+
const revision = metadata.buildRevision;
|
|
77
|
+
if (typeof revision === "string" && /^[0-9a-f]{40}$/.test(revision,)) {
|
|
78
|
+
process.env.DSS_BUILD_REVISION = revision;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Dist without build metadata: provenance still reports dist source.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Same process, same argv: the CLI sees process.argv.slice(2) exactly as before.
|
|
86
|
+
await import(pathToFileURL(cliPath,).href);
|
|
98
87
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"format":1,"buildRevision":"730725833d165213e28d6affee90bd3c1b012ec9"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { writeFileSync, } from "node:fs";
|
|
2
1
|
import { requiredJsonInput, } from "../coerce.js";
|
|
3
2
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
4
3
|
export const apiServiceCommands = {
|
|
@@ -100,9 +99,11 @@ export const apiServiceCommands = {
|
|
|
100
99
|
if (!out)
|
|
101
100
|
throw new UsageError("--output PATH is required.", "missing_required_flag");
|
|
102
101
|
const res = await c.apiServices.downloadPackageArchive(a[0], a[1], f["project-key"]);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
if (!res.body) {
|
|
103
|
+
throw new Error("apiServices.downloadPackageArchive response did not include a body");
|
|
104
|
+
}
|
|
105
|
+
const bytes = await Bun.write(out, new Response(res.body), { createPath: false, });
|
|
106
|
+
return { path: out, bytes, };
|
|
106
107
|
},
|
|
107
108
|
usage: "dss api-service download-package <serviceId> <packageId> --output PATH [--project-key KEY]",
|
|
108
109
|
description: "Download an API service package archive to a local file.",
|
|
@@ -2,16 +2,17 @@ import { ClientValidationError, DataikuError, } from "../../errors.js";
|
|
|
2
2
|
import { projectIncarnationHash, } from "../../utils/project-incarnation.js";
|
|
3
3
|
import { jsonInput, num, parseBooleanOption, plainRecord, requiredJsonInput, requiredStringFlag, stringField, } from "../coerce.js";
|
|
4
4
|
import { appPermissionsHash, appPermissionsVerificationError, assertAppPermissionsSnapshotBinding, buildAppPermissionsSnapshot, diffAppPermissions, readAppPermissionsSnapshot, writeAppPermissionsSnapshot, } from "../helpers/app-permissions.js";
|
|
5
|
-
import { createSuccessorInstance, futureTargetIdentity, isDefinitiveRejection, requireAbsentProjectTarget, safeErrorSummary, verifyAppInstance, VISUAL_UI_GATE, } from "../helpers/app-successor.js";
|
|
5
|
+
import { createSuccessorInstance, futureTargetIdentity, isDefinitiveRejection, preflightSuccessorInstance, requireAbsentProjectTarget, safeErrorSummary, verifyAppInstance, VISUAL_UI_GATE, } from "../helpers/app-successor.js";
|
|
6
6
|
import { CommandResultFailure, } from "../output.js";
|
|
7
7
|
import { requireArgs, requireNoArgs, UsageError, } from "../usage.js";
|
|
8
8
|
const CREATE_INSTANCE_USAGE = "dss app create-instance <appId> (--data JSON|--data-file PATH|--stdin) [--wait] [--timeout MS] [--poll-interval MS]";
|
|
9
9
|
const MANIFEST_VERSION_USAGE = "dss app manifest-version [--project-key KEY]";
|
|
10
10
|
const SET_MANIFEST_VERSION_USAGE = "dss app set-manifest-version (--manifest-version V|--version-notes NOTES) [--expect-hash SHA256] [--dry-run] [--project-key KEY]";
|
|
11
11
|
const CREATE_SUCCESSOR_USAGE = "dss app create-successor-instance <appId> --from KEY --to KEY [--name NAME] [--copy-permissions] [--timeout MS] [--poll-interval MS] [--dry-run] [--record-cleanup PATH]";
|
|
12
|
+
const SUCCESSOR_PREFLIGHT_USAGE = "dss app successor-preflight <appId> --from KEY --to KEY [--name NAME] [--copy-permissions]";
|
|
12
13
|
const VERIFY_INSTANCE_USAGE = "dss app verify-instance <appId> --project-key KEY [--expect-version V]";
|
|
13
14
|
const DELETE_INSTANCE_USAGE = "dss app delete-instance --project-key KEY [--future-id ID] [--expect-project-incarnation SHA256] [--unconfirmed-creation] [--timeout MS] [--poll-interval MS]";
|
|
14
|
-
const CREATE_INSTANCE_INDETERMINATE_REMEDIATION = "DSS may have accepted the creation, but no future ID
|
|
15
|
+
const CREATE_INSTANCE_INDETERMINATE_REMEDIATION = "DSS may have accepted the creation, but no future ID or project incarnation authorizes cleanup. Inspect `dss app instances <appId>` and DSS task history; delete only after creation is known terminal and the target identity is verified.";
|
|
15
16
|
const CREATE_INSTANCE_REJECTED_REMEDIATION = "DSS refused the creation request, so no instance project was created: fix the reported problem and retry.";
|
|
16
17
|
const DELETE_INSTANCE_FUTURE_REMEDIATION = "The supplied creation future could not authorize deletion. Inspect that future and the target project, then retry only with a future whose terminal result reports this project.";
|
|
17
18
|
const DELETE_INSTANCE_UNCONFIRMED_REMEDIATION = "Creation may still be running and DSS returned no future ID. Inspect the app's instances and wait until creation is known terminal before running delete-instance without --unconfirmed-creation.";
|
|
@@ -116,7 +117,9 @@ export const appCommands = {
|
|
|
116
117
|
throw new UsageError("Instance creation payload must include a non-empty targetProjectKey.", "validation_failed", `Usage: ${CREATE_INSTANCE_USAGE}`);
|
|
117
118
|
}
|
|
118
119
|
body.targetProjectKey = requestedProjectKey;
|
|
119
|
-
await requireAbsentProjectTarget(c, requestedProjectKey, "targetProjectKey"
|
|
120
|
+
await requireAbsentProjectTarget(c, requestedProjectKey, "targetProjectKey", {
|
|
121
|
+
appId: a[0],
|
|
122
|
+
});
|
|
120
123
|
let created;
|
|
121
124
|
try {
|
|
122
125
|
created = await c.applications.createInstance(a[0], body);
|
|
@@ -124,15 +127,19 @@ export const appCommands = {
|
|
|
124
127
|
catch (error) {
|
|
125
128
|
// A definitive rejection proves nothing was created; anything else
|
|
126
129
|
// (5xx, retryable 4xx, transport failure) leaves the outcome
|
|
127
|
-
// unknown
|
|
130
|
+
// unknown. Without a returned future ID or verified incarnation,
|
|
131
|
+
// cleanup cannot be authorized.
|
|
128
132
|
const rejected = isDefinitiveRejection(error);
|
|
129
133
|
return {
|
|
130
134
|
success: false,
|
|
131
|
-
state: "CREATE_FAILED",
|
|
135
|
+
state: rejected ? "CREATE_FAILED" : "INDETERMINATE",
|
|
132
136
|
elapsedMs: 0,
|
|
133
137
|
pollCount: 0,
|
|
134
138
|
projectKey: requestedProjectKey,
|
|
135
|
-
|
|
139
|
+
outcome: rejected ? "rejected" : "indeterminate",
|
|
140
|
+
creationPostAttempted: true,
|
|
141
|
+
cleanupEligible: false,
|
|
142
|
+
recoveryCommands: rejected ? [] : [`dss app instances ${a[0]}`,],
|
|
136
143
|
remediation: rejected
|
|
137
144
|
? CREATE_INSTANCE_REJECTED_REMEDIATION
|
|
138
145
|
: CREATE_INSTANCE_INDETERMINATE_REMEDIATION,
|
|
@@ -143,11 +150,13 @@ export const appCommands = {
|
|
|
143
150
|
if (!instance) {
|
|
144
151
|
return {
|
|
145
152
|
success: false,
|
|
146
|
-
state: "
|
|
153
|
+
state: "INDETERMINATE",
|
|
147
154
|
elapsedMs: 0,
|
|
148
155
|
pollCount: 0,
|
|
149
156
|
projectKey: requestedProjectKey,
|
|
150
|
-
|
|
157
|
+
outcome: "indeterminate",
|
|
158
|
+
creationPostAttempted: true,
|
|
159
|
+
cleanupEligible: false,
|
|
151
160
|
responseKind: created === undefined
|
|
152
161
|
? "empty"
|
|
153
162
|
: created === null
|
|
@@ -157,6 +166,7 @@ export const appCommands = {
|
|
|
157
166
|
: "scalar",
|
|
158
167
|
error: "DSS accepted instance creation without returning a structured creation state.",
|
|
159
168
|
remediation: CREATE_INSTANCE_INDETERMINATE_REMEDIATION,
|
|
169
|
+
recoveryCommands: [`dss app instances ${a[0]}`,],
|
|
160
170
|
};
|
|
161
171
|
}
|
|
162
172
|
// The create endpoint's `jobId` is a DSS future ID (the official client wraps it in
|
|
@@ -174,7 +184,7 @@ export const appCommands = {
|
|
|
174
184
|
projectKey: requestedProjectKey,
|
|
175
185
|
instance,
|
|
176
186
|
...(jobId !== undefined ? { futureId: jobId, jobId, } : {}),
|
|
177
|
-
cleanupEligible:
|
|
187
|
+
cleanupEligible: jobId !== undefined,
|
|
178
188
|
error: "DSS named a different target project in the creation response.",
|
|
179
189
|
expected: { projectKey: requestedProjectKey, },
|
|
180
190
|
actual: { projectKey: echoedTarget, },
|
|
@@ -251,14 +261,17 @@ export const appCommands = {
|
|
|
251
261
|
if (!jobId) {
|
|
252
262
|
return {
|
|
253
263
|
success: false,
|
|
254
|
-
state: "
|
|
264
|
+
state: "INDETERMINATE",
|
|
255
265
|
elapsedMs: 0,
|
|
256
266
|
pollCount: 0,
|
|
257
267
|
projectKey: requestedProjectKey,
|
|
258
268
|
instance,
|
|
259
|
-
|
|
260
|
-
|
|
269
|
+
outcome: "indeterminate",
|
|
270
|
+
creationPostAttempted: true,
|
|
271
|
+
cleanupEligible: false,
|
|
272
|
+
error: "DSS returned no instance-creation future ID. The instance may still have been created; inspect app instances before any recovery action.",
|
|
261
273
|
remediation: CREATE_INSTANCE_INDETERMINATE_REMEDIATION,
|
|
274
|
+
recoveryCommands: [`dss app instances ${a[0]}`,],
|
|
262
275
|
};
|
|
263
276
|
}
|
|
264
277
|
let waited;
|
|
@@ -278,6 +291,7 @@ export const appCommands = {
|
|
|
278
291
|
instance,
|
|
279
292
|
jobId,
|
|
280
293
|
cleanupEligible: true,
|
|
294
|
+
recoveryCommands: [`dss future wait ${jobId}`, `dss app instances ${a[0]}`,],
|
|
281
295
|
remediation: CREATE_INSTANCE_INDETERMINATE_REMEDIATION,
|
|
282
296
|
...safeErrorSummary(error),
|
|
283
297
|
};
|
|
@@ -343,6 +357,25 @@ export const appCommands = {
|
|
|
343
357
|
'dss app create-instance my-app --data \'{"targetProjectKey":"NEWPROJ"}\' --wait --timeout 120000 --poll-interval 2000',
|
|
344
358
|
],
|
|
345
359
|
},
|
|
360
|
+
"successor-preflight": {
|
|
361
|
+
handler: async (c, a, f) => {
|
|
362
|
+
requireArgs(a, 1, SUCCESSOR_PREFLIGHT_USAGE);
|
|
363
|
+
return preflightSuccessorInstance(c, {
|
|
364
|
+
appId: a[0],
|
|
365
|
+
from: requiredStringFlag(f, "from", SUCCESSOR_PREFLIGHT_USAGE),
|
|
366
|
+
to: requiredStringFlag(f, "to", SUCCESSOR_PREFLIGHT_USAGE),
|
|
367
|
+
...(f["name"] !== undefined ? { name: f["name"], } : {}),
|
|
368
|
+
copyPermissions: parseBooleanOption(f["copy-permissions"], "--copy-permissions") ?? false,
|
|
369
|
+
dryRun: true,
|
|
370
|
+
}, SUCCESSOR_PREFLIGHT_USAGE);
|
|
371
|
+
},
|
|
372
|
+
usage: SUCCESSOR_PREFLIGHT_USAGE,
|
|
373
|
+
description: "Run every read-only successor gate before changing the template version: validate the template, verify the predecessor, prove target absence, and optionally snapshot the predecessor ACL.",
|
|
374
|
+
examples: [
|
|
375
|
+
"dss app successor-preflight my-app --from OLD_INSTANCE --to NEW_INSTANCE",
|
|
376
|
+
"dss app successor-preflight my-app --from OLD_INSTANCE --to NEW_INSTANCE --copy-permissions",
|
|
377
|
+
],
|
|
378
|
+
},
|
|
346
379
|
"create-successor-instance": {
|
|
347
380
|
handler: async (c, a, f) => {
|
|
348
381
|
requireArgs(a, 1, CREATE_SUCCESSOR_USAGE);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { writeFileSync, } from "node:fs";
|
|
2
1
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
3
2
|
export const bundleCommands = {
|
|
4
3
|
"list-exported": {
|
|
@@ -34,9 +33,11 @@ export const bundleCommands = {
|
|
|
34
33
|
if (!out)
|
|
35
34
|
throw new UsageError("--output PATH is required.", "missing_required_flag");
|
|
36
35
|
const res = await c.bundles.downloadExportedArchive(a[0], f["project-key"]);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
if (!res.body) {
|
|
37
|
+
throw new Error("bundles.downloadExportedArchive response did not include a body");
|
|
38
|
+
}
|
|
39
|
+
const bytes = await Bun.write(out, new Response(res.body), { createPath: false, });
|
|
40
|
+
return { path: out, bytes, };
|
|
40
41
|
},
|
|
41
42
|
usage: "dss bundle download-exported <bundleId> --output PATH [--project-key KEY]",
|
|
42
43
|
description: "Download an exported Design-node bundle archive to a local file.",
|
|
@@ -2,6 +2,18 @@ import { deepMerge, } from "../../utils/deep-merge.js";
|
|
|
2
2
|
import { num, parseBooleanOption, requiredJsonInput, } from "../coerce.js";
|
|
3
3
|
import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
|
|
4
4
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
5
|
+
/**
|
|
6
|
+
* A data quality result counts as passing only when it actually carries a
|
|
7
|
+
* verdict and every verdict it carries reports OK. A result with no outcome
|
|
8
|
+
* and no status has proven nothing, and a result whose fields disagree
|
|
9
|
+
* (outcome ERROR alongside status OK) is a failure — never a pass.
|
|
10
|
+
*/
|
|
11
|
+
function resultReportsOk(result) {
|
|
12
|
+
const verdicts = [result.outcome, result.status,]
|
|
13
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
14
|
+
.map((value) => value.trim().toUpperCase());
|
|
15
|
+
return verdicts.length > 0 && verdicts.every((value) => value === "OK");
|
|
16
|
+
}
|
|
5
17
|
export const dataQualityCommands = {
|
|
6
18
|
rules: {
|
|
7
19
|
handler: (c, a, f) => {
|
|
@@ -160,6 +172,36 @@ export const dataQualityCommands = {
|
|
|
160
172
|
description: "Get latest data quality rule results for a dataset.",
|
|
161
173
|
examples: ["dss data-quality last-results orders",],
|
|
162
174
|
},
|
|
175
|
+
"assert-results": {
|
|
176
|
+
handler: async (c, a, f) => {
|
|
177
|
+
requireArgs(a, 1, "dss data-quality assert-results <dataset>");
|
|
178
|
+
const ruleId = f["rule-id"];
|
|
179
|
+
const pk = f["project-key"];
|
|
180
|
+
const results = await c.dataQuality.lastResults(a[0], { ruleId, projectKey: pk, });
|
|
181
|
+
const selected = results.filter((result) => ruleId === undefined || result.id === ruleId || result.ruleId === ruleId);
|
|
182
|
+
const failed = selected
|
|
183
|
+
.filter((result) => !resultReportsOk(result))
|
|
184
|
+
.map((result) => ({
|
|
185
|
+
ruleId: result.id ?? result.ruleId ?? result.name ?? null,
|
|
186
|
+
outcome: result.outcome ?? null,
|
|
187
|
+
status: result.status ?? null,
|
|
188
|
+
}));
|
|
189
|
+
return {
|
|
190
|
+
satisfied: selected.length > 0 && failed.length === 0,
|
|
191
|
+
dataset: a[0],
|
|
192
|
+
...(ruleId !== undefined ? { ruleId, } : {}),
|
|
193
|
+
checked: selected.length,
|
|
194
|
+
failed,
|
|
195
|
+
...(selected.length === 0 ? { reason: "no_results", } : {}),
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
usage: "dss data-quality assert-results <dataset> [--rule-id ID] [--project-key KEY]",
|
|
199
|
+
description: "Assert the latest data quality results: at least one selected rule must exist and every selected result must report OK. Returns { satisfied, checked, failed } with failed rule ids/outcomes only; a failed assertion exits 4 with assertion_failed.",
|
|
200
|
+
examples: [
|
|
201
|
+
"dss data-quality assert-results orders",
|
|
202
|
+
"dss data-quality assert-results orders --rule-id RULE_ID",
|
|
203
|
+
],
|
|
204
|
+
},
|
|
163
205
|
history: {
|
|
164
206
|
handler: (c, a, f) => {
|
|
165
207
|
requireArgs(a, 1, "dss data-quality history <dataset>");
|
|
@@ -1,2 +1,25 @@
|
|
|
1
1
|
import type { CommandMeta } from "../types.js";
|
|
2
|
+
export interface DatasetSchemaDifference {
|
|
3
|
+
/** Path from the schema root in dot/bracket notation, e.g. `columns[2].type`. */
|
|
4
|
+
path: string;
|
|
5
|
+
kind: "added" | "removed" | "changed";
|
|
6
|
+
/** SHA-256 of the deterministic JSON rendering of the expected subtree. */
|
|
7
|
+
expectedHash?: string;
|
|
8
|
+
/** SHA-256 of the deterministic JSON rendering of the actual subtree. */
|
|
9
|
+
actualHash?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DatasetSchemaComparison {
|
|
12
|
+
equal: boolean;
|
|
13
|
+
expectedHash: string;
|
|
14
|
+
actualHash: string;
|
|
15
|
+
differences: DatasetSchemaDifference[];
|
|
16
|
+
totalDifferences: number;
|
|
17
|
+
differencesTruncated: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Compare two schema values deterministically — object key order never matters
|
|
21
|
+
* — and report each structural difference as a concise per-path entry carrying
|
|
22
|
+
* subtree hashes instead of subtree values.
|
|
23
|
+
*/
|
|
24
|
+
export declare function compareDatasetSchemas(expected: unknown, actual: unknown): DatasetSchemaComparison;
|
|
2
25
|
export declare const datasetCommands: Record<string, CommandMeta>;
|