dataiku-sdk 0.12.3 → 0.12.5
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 +118 -7
- package/dist/packages/types/src/index.d.ts +5 -0
- package/dist/packages/types/src/index.js +5 -0
- package/dist/src/cli/coerce.d.ts +1 -3
- package/dist/src/cli/coerce.js +1 -17
- package/dist/src/cli/commands/app.js +709 -14
- package/dist/src/cli/commands/code-env.js +2 -2
- package/dist/src/cli/commands/dashboard.js +1 -1
- package/dist/src/cli/commands/dataset.js +2 -2
- package/dist/src/cli/commands/folder.js +1 -1
- package/dist/src/cli/commands/insight.js +2 -2
- package/dist/src/cli/commands/notebook.js +2 -2
- package/dist/src/cli/commands/recipe.js +3 -3
- package/dist/src/cli/commands/scenario.js +1 -1
- package/dist/src/cli/commands/sql.js +1 -1
- package/dist/src/cli/commands/variable.js +1 -1
- package/dist/src/cli/commands/wiki.js +1 -1
- package/dist/src/cli/contract.d.ts +12 -3
- package/dist/src/cli/contract.js +805 -49
- package/dist/src/cli/flags.js +8 -0
- package/dist/src/cli/helpers/app-permissions.d.ts +63 -0
- package/dist/src/cli/helpers/app-permissions.js +239 -0
- package/dist/src/cli/helpers/app-successor.d.ts +106 -0
- package/dist/src/cli/helpers/app-successor.js +625 -0
- package/dist/src/cli/helpers/cleanup.js +46 -12
- package/dist/src/cli/output.d.ts +12 -1
- package/dist/src/cli/output.js +53 -6
- package/dist/src/cli/usage.d.ts +1 -1
- package/dist/src/cli/usage.js +5 -2
- package/dist/src/cli.js +329 -32
- package/dist/src/client.d.ts +6 -0
- package/dist/src/client.js +28 -1
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/resources/applications.d.ts +191 -1
- package/dist/src/resources/applications.js +549 -10
- package/dist/src/resources/datasets.js +8 -0
- package/dist/src/resources/futures.js +7 -2
- package/dist/src/skill.js +80 -15
- package/dist/src/utils/cleanup-ledger.d.ts +34 -1
- package/dist/src/utils/cleanup-ledger.js +118 -7
- package/dist/src/utils/dss-url.d.ts +6 -0
- package/dist/src/utils/dss-url.js +17 -0
- package/dist/src/utils/project-incarnation.d.ts +6 -0
- package/dist/src/utils/project-incarnation.js +27 -0
- package/dist/src/utils/stable-hash.d.ts +11 -0
- package/dist/src/utils/stable-hash.js +27 -0
- package/node_modules/@sinclair/typebox/build/cjs/compiler/compiler.js +26 -46
- package/node_modules/@sinclair/typebox/build/cjs/parser/runtime/guard.d.ts +1 -1
- package/node_modules/@sinclair/typebox/build/cjs/parser/runtime/token.d.ts +2 -2
- package/node_modules/@sinclair/typebox/build/cjs/syntax/mapping.d.ts +1 -1
- package/node_modules/@sinclair/typebox/build/cjs/system/evaluate.d.ts +5 -0
- package/node_modules/@sinclair/typebox/build/cjs/system/evaluate.js +11 -0
- package/node_modules/@sinclair/typebox/build/cjs/system/index.d.ts +1 -0
- package/node_modules/@sinclair/typebox/build/cjs/system/index.js +1 -0
- package/node_modules/@sinclair/typebox/build/cjs/type/object/object.d.ts +2 -2
- package/node_modules/@sinclair/typebox/build/cjs/type/object/object.js +18 -2
- package/node_modules/@sinclair/typebox/build/cjs/value/parse/parse.d.ts +1 -1
- package/node_modules/@sinclair/typebox/build/esm/compiler/compiler.mjs +27 -47
- package/node_modules/@sinclair/typebox/build/esm/parser/runtime/guard.d.mts +1 -1
- package/node_modules/@sinclair/typebox/build/esm/parser/runtime/token.d.mts +2 -2
- package/node_modules/@sinclair/typebox/build/esm/syntax/mapping.d.mts +1 -1
- package/node_modules/@sinclair/typebox/build/esm/system/evaluate.d.mts +5 -0
- package/node_modules/@sinclair/typebox/build/esm/system/evaluate.mjs +7 -0
- package/node_modules/@sinclair/typebox/build/esm/system/index.d.mts +1 -0
- package/node_modules/@sinclair/typebox/build/esm/system/index.mjs +1 -0
- package/node_modules/@sinclair/typebox/build/esm/type/composite/composite.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/const/const.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/instantiate/instantiate.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/mapped/mapped.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/module/compute.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/object/object.d.mts +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/object/object.mjs +18 -2
- package/node_modules/@sinclair/typebox/build/esm/type/omit/omit.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/partial/partial.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/pick/pick.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/record/record.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/required/required.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/type/json.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/value/delta/delta.mjs +4 -4
- package/node_modules/@sinclair/typebox/build/esm/value/parse/parse.d.mts +1 -1
- package/node_modules/@sinclair/typebox/package.json +1 -1
- package/package.json +7 -5
- package/packages/types/dist/index.d.ts +5 -0
- package/packages/types/dist/index.js +5 -0
|
@@ -1,5 +1,191 @@
|
|
|
1
1
|
import { ClientValidationError, DataikuError, } from "../errors.js";
|
|
2
|
+
import { projectIncarnationHash, } from "../utils/project-incarnation.js";
|
|
3
|
+
import { compareStrings, stableHash, stableJson, } from "../utils/stable-hash.js";
|
|
2
4
|
import { BaseResource, } from "./base.js";
|
|
5
|
+
/**
|
|
6
|
+
* Source-verifiable app-manifest reference locations. Validation is scoped to
|
|
7
|
+
* homepage tiles so identically named keys in arbitrary custom-form `config`
|
|
8
|
+
* objects are never treated as references.
|
|
9
|
+
*/
|
|
10
|
+
export const APP_MANIFEST_REFERENCE_KEYS = {
|
|
11
|
+
scenario: ["SCENARIO_RUN.scenarioId",],
|
|
12
|
+
folder: ["DOWNLOAD_FILE.folderId", "DOWNLOAD_FILE.managedFolderId",],
|
|
13
|
+
variable: ["params[].name",],
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The only concurrency control the public app-manifest endpoint permits. DSS
|
|
17
|
+
* exposes no conditional write (no ETag, no If-Match, no version token), so
|
|
18
|
+
* `expectHash` can only detect a hash that is already stale when it is checked;
|
|
19
|
+
* it can never turn the subsequent PUT into a conditional write. Every
|
|
20
|
+
* manifest-version write
|
|
21
|
+
* result reports this literal so callers can machine-check the guarantee they
|
|
22
|
+
* actually get instead of inferring one from prose.
|
|
23
|
+
*/
|
|
24
|
+
export const APP_MANIFEST_CONCURRENCY_CONTROL = "client-side-non-atomic-stale-read-check";
|
|
25
|
+
/**
|
|
26
|
+
* Root-level project identity fields excluded from template-vs-instance
|
|
27
|
+
* comparison. `projectAppType` differs by construction (APP_TEMPLATE vs
|
|
28
|
+
* APP_INSTANCE), and `projectKey` names the distinct template/instance
|
|
29
|
+
* projects. Every other field, including version markers and unknown fields,
|
|
30
|
+
* remains governed and is compared.
|
|
31
|
+
*/
|
|
32
|
+
const APP_MANIFEST_COMPARE_OMITTED_FIELDS = [
|
|
33
|
+
"projectKey",
|
|
34
|
+
"projectAppType",
|
|
35
|
+
];
|
|
36
|
+
function isPlainRecord(value) {
|
|
37
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
function propertyPath(path, key) {
|
|
40
|
+
return `${path}[${JSON.stringify(key)}]`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Extract a reference value using the manifest key's singular/plural shape:
|
|
44
|
+
* singular `*Id`/`*Name` keys hold one non-empty string and plural
|
|
45
|
+
* `*Ids`/`*Names` keys hold an array of non-empty strings. Anything else is
|
|
46
|
+
* reported as malformed. Purely functional: never mutates the manifest.
|
|
47
|
+
*/
|
|
48
|
+
function collectReferenceValue(value, path, key, kind, references, issues) {
|
|
49
|
+
const plural = key.endsWith("Ids") || key.endsWith("Names");
|
|
50
|
+
const expected = plural ? "an array of non-empty strings" : "a non-empty string";
|
|
51
|
+
const malformed = (issuePath = path) => {
|
|
52
|
+
issues.push({
|
|
53
|
+
kind,
|
|
54
|
+
issue: {
|
|
55
|
+
code: "INVALID_REFERENCE_VALUE",
|
|
56
|
+
path: issuePath,
|
|
57
|
+
message: `Reference key "${key}" must hold ${expected}.`,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
if (!plural) {
|
|
62
|
+
if (typeof value === "string" && value.length > 0) {
|
|
63
|
+
references.push({ kind, path, value, exists: false, });
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
malformed();
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(value)) {
|
|
71
|
+
malformed();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (let i = 0; i < value.length; i++) {
|
|
75
|
+
const item = value[i];
|
|
76
|
+
const itemPath = `${path}[${i}]`;
|
|
77
|
+
if (typeof item === "string" && item.length > 0) {
|
|
78
|
+
references.push({ kind, path: itemPath, value: item, exists: false, });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
malformed(itemPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function omitRootFields(value, omitted) {
|
|
86
|
+
if (!isPlainRecord(value))
|
|
87
|
+
return value;
|
|
88
|
+
const result = {};
|
|
89
|
+
for (const key of Object.keys(value)) {
|
|
90
|
+
if (omitted.includes(key))
|
|
91
|
+
continue;
|
|
92
|
+
result[key] = value[key];
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/** Lowercase or uppercase SHA-256 hex digest, as emitted by `stableHash`. */
|
|
97
|
+
const EXPECT_HASH_PATTERN = /^[0-9a-fA-F]{64}$/;
|
|
98
|
+
/**
|
|
99
|
+
* Project a manifest onto its version state. Only string values count: a
|
|
100
|
+
* non-string `version` is reported as null rather than coerced, so callers can
|
|
101
|
+
* never mistake `3` or `null` for a released version string, and no `N/A`
|
|
102
|
+
* placeholder is invented. The hash covers the whole manifest so the
|
|
103
|
+
* stale-read check detects edits to any field, not just the version markers.
|
|
104
|
+
*/
|
|
105
|
+
function manifestVersionState(projectKey, projectAppType, manifest) {
|
|
106
|
+
return {
|
|
107
|
+
projectKey,
|
|
108
|
+
projectAppType,
|
|
109
|
+
version: typeof manifest["version"] === "string" ? manifest["version"] : null,
|
|
110
|
+
versionNotes: typeof manifest["versionNotes"] === "string" ? manifest["versionNotes"] : null,
|
|
111
|
+
manifestHash: stableHash(manifest),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function diffValues(template, instance, path) {
|
|
115
|
+
if (isPlainRecord(template) && isPlainRecord(instance)) {
|
|
116
|
+
const keys = Array.from(new Set([...Object.keys(template), ...Object.keys(instance),])).sort(compareStrings);
|
|
117
|
+
const differences = [];
|
|
118
|
+
for (const key of keys) {
|
|
119
|
+
const childPath = propertyPath(path, key);
|
|
120
|
+
const inTemplate = Object.prototype.hasOwnProperty.call(template, key);
|
|
121
|
+
const inInstance = Object.prototype.hasOwnProperty.call(instance, key);
|
|
122
|
+
if (inTemplate && !inInstance) {
|
|
123
|
+
differences.push({ path: childPath, kind: "removed", template: template[key], });
|
|
124
|
+
}
|
|
125
|
+
else if (!inTemplate && inInstance) {
|
|
126
|
+
differences.push({ path: childPath, kind: "added", instance: instance[key], });
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
differences.push(...diffValues(template[key], instance[key], childPath));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return differences;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(template) && Array.isArray(instance)) {
|
|
135
|
+
const differences = [];
|
|
136
|
+
const length = Math.max(template.length, instance.length);
|
|
137
|
+
for (let i = 0; i < length; i++) {
|
|
138
|
+
const childPath = `${path}[${i}]`;
|
|
139
|
+
if (i >= template.length) {
|
|
140
|
+
differences.push({ path: childPath, kind: "added", instance: instance[i], });
|
|
141
|
+
}
|
|
142
|
+
else if (i >= instance.length) {
|
|
143
|
+
differences.push({ path: childPath, kind: "removed", template: template[i], });
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
differences.push(...diffValues(template[i], instance[i], childPath));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return differences;
|
|
150
|
+
}
|
|
151
|
+
if (stableJson(template) === stableJson(instance))
|
|
152
|
+
return [];
|
|
153
|
+
return [{ path, kind: "changed", template, instance, },];
|
|
154
|
+
}
|
|
155
|
+
const APP_MANIFEST_WRITE_INDETERMINATE_REMEDIATION = "Whether DSS persisted the manifest version could not be verified. Re-read the manifest version and compare its hash to the desired hash before retrying; a safe retry writes the identical manifest again.";
|
|
156
|
+
/** Never echoes response bodies or secrets, only the DataikuError taxonomy. */
|
|
157
|
+
function manifestVersionWriteErrorSummary(error) {
|
|
158
|
+
if (error instanceof DataikuError) {
|
|
159
|
+
return {
|
|
160
|
+
error: error.safeMessage,
|
|
161
|
+
errorStatus: error.status,
|
|
162
|
+
errorCategory: error.category,
|
|
163
|
+
remediation: APP_MANIFEST_WRITE_INDETERMINATE_REMEDIATION,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
error: error instanceof Error ? error.message : String(error),
|
|
168
|
+
errorStatus: null,
|
|
169
|
+
errorCategory: null,
|
|
170
|
+
remediation: APP_MANIFEST_WRITE_INDETERMINATE_REMEDIATION,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function indeterminateManifestVersionWriteResult(pk, before, desired, requested, error) {
|
|
174
|
+
return {
|
|
175
|
+
projectKey: pk,
|
|
176
|
+
projectAppType: before.projectAppType,
|
|
177
|
+
requested,
|
|
178
|
+
changed: true,
|
|
179
|
+
dryRun: false,
|
|
180
|
+
persisted: null,
|
|
181
|
+
outcome: "indeterminate",
|
|
182
|
+
before,
|
|
183
|
+
desired,
|
|
184
|
+
after: null,
|
|
185
|
+
concurrencyControl: APP_MANIFEST_CONCURRENCY_CONTROL,
|
|
186
|
+
error: manifestVersionWriteErrorSummary(error),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
3
189
|
export class ApplicationsResource extends BaseResource {
|
|
4
190
|
/** List all Dataiku Apps. */
|
|
5
191
|
async listApps() {
|
|
@@ -21,16 +207,351 @@ export class ApplicationsResource extends BaseResource {
|
|
|
21
207
|
async getInstanceManifest(projectKey) {
|
|
22
208
|
return this.client.get(`/public/api/projects/${this.enc(projectKey)}/app-manifest`);
|
|
23
209
|
}
|
|
210
|
+
async getProjectDetails(projectKey) {
|
|
211
|
+
return this.client.get(`/public/api/projects/${encodeURIComponent(projectKey)}/`);
|
|
212
|
+
}
|
|
213
|
+
async getProjectAppType(projectKey) {
|
|
214
|
+
const details = await this.getProjectDetails(projectKey);
|
|
215
|
+
return typeof details.projectAppType === "string" ? details.projectAppType : undefined;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Shared app-manifest write guard: DSS only accepts manifest saves for app
|
|
219
|
+
* template projects, so every mutation path rejects instances and ordinary
|
|
220
|
+
* projects before issuing a PUT.
|
|
221
|
+
*/
|
|
222
|
+
requireAppTemplateForManifestSave(projectKey, projectAppType) {
|
|
223
|
+
if (projectAppType === "APP_TEMPLATE")
|
|
224
|
+
return;
|
|
225
|
+
throw new ClientValidationError("Only Dataiku App template project manifests can be saved through the app-manifest endpoint.", "validation_failed", projectAppType === "APP_INSTANCE"
|
|
226
|
+
? "Save against the Dataiku App template project instead; existing classic app-instance manifests are read-only through this endpoint."
|
|
227
|
+
: "Convert the project to a Dataiku App template before saving its manifest.", { projectAppType: projectAppType ?? null, projectKey, });
|
|
228
|
+
}
|
|
24
229
|
/** Save the manifest for a Dataiku App template project (rejects classic app-instance projects). */
|
|
25
230
|
async saveInstanceManifest(manifest, projectKey) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
231
|
+
const pk = this.resolveProjectKey(projectKey);
|
|
232
|
+
const currentManifest = await this.getInstanceManifest(pk);
|
|
233
|
+
const projectAppType = typeof currentManifest.projectAppType === "string"
|
|
234
|
+
? currentManifest.projectAppType
|
|
235
|
+
: await this.getProjectAppType(pk);
|
|
236
|
+
this.requireAppTemplateForManifestSave(pk, projectAppType);
|
|
237
|
+
await this.client.putVoid(`/public/api/projects/${encodeURIComponent(pk)}/app-manifest`, manifest);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Read the manifest plus its derived version state. The project type comes
|
|
241
|
+
* from the manifest when DSS includes it and from project details otherwise,
|
|
242
|
+
* so ordinary reads cost a single request.
|
|
243
|
+
*/
|
|
244
|
+
async readManifestVersion(projectKey) {
|
|
245
|
+
const pk = this.resolveProjectKey(projectKey);
|
|
246
|
+
const manifest = await this.getInstanceManifest(pk);
|
|
247
|
+
if (!isPlainRecord(manifest)) {
|
|
248
|
+
throw new ClientValidationError(`The app-manifest endpoint for project ${pk} did not return a JSON object.`, "validation_failed", "Confirm the project is a Dataiku App template or app instance and that no proxy rewrote the response.", { projectKey: pk, });
|
|
29
249
|
}
|
|
30
|
-
|
|
250
|
+
const projectAppType = typeof manifest["projectAppType"] === "string"
|
|
251
|
+
? manifest["projectAppType"]
|
|
252
|
+
: (await this.getProjectAppType(pk)) ?? null;
|
|
253
|
+
return { manifest, state: manifestVersionState(pk, projectAppType, manifest), };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Read the authoritative app-manifest version markers. Allowed for app
|
|
257
|
+
* templates and app instances alike: instance verification compares an
|
|
258
|
+
* instance's inherited `version` against its template.
|
|
259
|
+
*/
|
|
260
|
+
async getManifestVersion(projectKey) {
|
|
261
|
+
const { state, } = await this.readManifestVersion(projectKey);
|
|
262
|
+
return state;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Set the manifest `version`/`versionNotes` strings on an app template.
|
|
266
|
+
*
|
|
267
|
+
* This is a manifest save, not a publish transaction: DSS exposes no release
|
|
268
|
+
* endpoint, and new instances simply inherit whatever version string the
|
|
269
|
+
* template carries at creation time.
|
|
270
|
+
*
|
|
271
|
+
* The write is not atomic, and `expectHash` cannot change that: the
|
|
272
|
+
* endpoint takes every PUT unconditionally, so `expectHash` only rejects a
|
|
273
|
+
* caller whose hash is already stale at the verification read. This client
|
|
274
|
+
* can overwrite a writer that commits after that read but before this PUT,
|
|
275
|
+
* and a final state equal to this client's desired manifest cannot reveal
|
|
276
|
+
* that lost update. The final read still prevents a different state observed
|
|
277
|
+
* after the PUT from being reported as persisted.
|
|
278
|
+
*/
|
|
279
|
+
async setManifestVersion(update, projectKey) {
|
|
280
|
+
const pk = this.resolveProjectKey(projectKey);
|
|
281
|
+
if (!isPlainRecord(update)) {
|
|
282
|
+
throw new ClientValidationError("App manifest version update must be an object.", "validation_failed", "Supply { version } and/or { versionNotes }.", { projectKey: pk, });
|
|
283
|
+
}
|
|
284
|
+
const requestedVersion = update.version;
|
|
285
|
+
const requestedNotes = update.versionNotes;
|
|
286
|
+
if (requestedVersion === undefined && requestedNotes === undefined) {
|
|
287
|
+
throw new ClientValidationError("No app-manifest version field was supplied.", "validation_failed", "Supply a version string, version notes, or both.", { projectKey: pk, });
|
|
288
|
+
}
|
|
289
|
+
if (requestedVersion !== undefined
|
|
290
|
+
&& (typeof requestedVersion !== "string" || requestedVersion.trim() === "")) {
|
|
291
|
+
throw new ClientValidationError("App manifest version must be a non-empty string.", "validation_failed", "Pass the version string DSS should display in the Application header, for example 1.2.0.", { projectKey: pk, });
|
|
292
|
+
}
|
|
293
|
+
if (requestedNotes !== undefined && typeof requestedNotes !== "string") {
|
|
294
|
+
throw new ClientValidationError("App manifest version notes must be a string.", "validation_failed", "Pass release notes as text, or an empty string to clear them.", { projectKey: pk, });
|
|
295
|
+
}
|
|
296
|
+
const expectHash = update.expectHash;
|
|
297
|
+
if (expectHash !== undefined
|
|
298
|
+
&& (typeof expectHash !== "string" || !EXPECT_HASH_PATTERN.test(expectHash))) {
|
|
299
|
+
throw new ClientValidationError("Expected manifest hash must be a 64-character SHA-256 hex digest.", "validation_failed", "Use the manifestHash value returned by the manifest-version read.", { projectKey: pk, });
|
|
300
|
+
}
|
|
301
|
+
if (update.dryRun !== undefined && typeof update.dryRun !== "boolean") {
|
|
302
|
+
throw new ClientValidationError("Dry-run flag must be a boolean.", "validation_failed", undefined, { projectKey: pk, });
|
|
303
|
+
}
|
|
304
|
+
const dryRun = update.dryRun === true;
|
|
305
|
+
const { manifest, state: before, } = await this.readManifestVersion(pk);
|
|
306
|
+
this.requireAppTemplateForManifestSave(pk, before.projectAppType);
|
|
307
|
+
if (expectHash !== undefined && expectHash.toLowerCase() !== before.manifestHash) {
|
|
308
|
+
throw new ClientValidationError(`The app manifest for project ${pk} changed since it was read.`, "validation_failed", "Re-read the manifest version and retry with the current manifestHash value.", {
|
|
309
|
+
projectKey: pk,
|
|
310
|
+
expectedHash: expectHash.toLowerCase(),
|
|
311
|
+
actualHash: before.manifestHash,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const desiredManifest = { ...manifest, };
|
|
315
|
+
if (requestedVersion !== undefined)
|
|
316
|
+
desiredManifest["version"] = requestedVersion;
|
|
317
|
+
if (requestedNotes !== undefined)
|
|
318
|
+
desiredManifest["versionNotes"] = requestedNotes;
|
|
319
|
+
const desired = manifestVersionState(pk, before.projectAppType, desiredManifest);
|
|
320
|
+
const requested = {
|
|
321
|
+
version: requestedVersion ?? null,
|
|
322
|
+
versionNotes: requestedNotes ?? null,
|
|
323
|
+
};
|
|
324
|
+
const changed = desired.manifestHash !== before.manifestHash;
|
|
325
|
+
if (!changed || dryRun) {
|
|
326
|
+
return {
|
|
327
|
+
projectKey: pk,
|
|
328
|
+
projectAppType: before.projectAppType,
|
|
329
|
+
requested,
|
|
330
|
+
changed,
|
|
331
|
+
dryRun,
|
|
332
|
+
persisted: false,
|
|
333
|
+
outcome: dryRun ? "dry-run" : "unchanged",
|
|
334
|
+
before,
|
|
335
|
+
desired,
|
|
336
|
+
after: before,
|
|
337
|
+
concurrencyControl: APP_MANIFEST_CONCURRENCY_CONTROL,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
await this.client.putVoid(`/public/api/projects/${encodeURIComponent(pk)}/app-manifest`, desiredManifest);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
// Definitive server rejections keep their normal DataikuError
|
|
345
|
+
// classification; everything else means the write outcome cannot
|
|
346
|
+
// be observed and is reported as indeterminate, never as success.
|
|
347
|
+
const definitiveRejection = error instanceof DataikuError
|
|
348
|
+
&& !error.retryable
|
|
349
|
+
&& error.category !== "unknown"
|
|
350
|
+
&& error.status >= 400
|
|
351
|
+
&& error.status < 500;
|
|
352
|
+
if (definitiveRejection)
|
|
353
|
+
throw error;
|
|
354
|
+
return indeterminateManifestVersionWriteResult(pk, before, desired, requested, error);
|
|
355
|
+
}
|
|
356
|
+
let afterManifest;
|
|
357
|
+
let after;
|
|
358
|
+
try {
|
|
359
|
+
const verification = await this.readManifestVersion(pk);
|
|
360
|
+
afterManifest = verification.manifest;
|
|
361
|
+
after = verification.state;
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
return indeterminateManifestVersionWriteResult(pk, before, desired, requested, error);
|
|
365
|
+
}
|
|
366
|
+
if (stableJson(afterManifest) !== stableJson(desiredManifest)) {
|
|
367
|
+
throw new ClientValidationError(`Saving the app-manifest version for project ${pk} did not persist the requested manifest.`, "validation_failed", "DSS rejected or rewrote the submitted manifest; re-read the manifest version before claiming any version change.", { projectKey: pk, before, desired, after, });
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
projectKey: pk,
|
|
371
|
+
projectAppType: after.projectAppType,
|
|
372
|
+
requested,
|
|
373
|
+
changed: true,
|
|
374
|
+
dryRun: false,
|
|
375
|
+
persisted: true,
|
|
376
|
+
outcome: "persisted",
|
|
377
|
+
before,
|
|
378
|
+
desired,
|
|
379
|
+
after,
|
|
380
|
+
concurrencyControl: APP_MANIFEST_CONCURRENCY_CONTROL,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Validate source-verifiable references in app homepage tiles:
|
|
385
|
+
* SCENARIO_RUN scenario IDs, DOWNLOAD_FILE managed-folder IDs, and runtime
|
|
386
|
+
* form parameter names (which DSS maps to project variables). Arbitrary
|
|
387
|
+
* custom-form `config` objects remain opaque. The manifest is never mutated.
|
|
388
|
+
* Missing/malformed references are returned deterministically; a reference
|
|
389
|
+
* kind with no applicable tiles does not trigger its project API.
|
|
390
|
+
*/
|
|
391
|
+
async validateAppManifest(manifest, projectKey) {
|
|
392
|
+
const pk = this.resolveProjectKey(projectKey);
|
|
393
|
+
const references = [];
|
|
394
|
+
const referenceIssues = [];
|
|
395
|
+
const rootIssues = [];
|
|
396
|
+
if (!isPlainRecord(manifest)) {
|
|
397
|
+
rootIssues.push({
|
|
398
|
+
code: "INVALID_MANIFEST_ROOT",
|
|
399
|
+
path: "$",
|
|
400
|
+
message: "App manifest root must be a JSON object.",
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
const sections = manifest["homepageSections"];
|
|
405
|
+
if (Array.isArray(sections)) {
|
|
406
|
+
for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex += 1) {
|
|
407
|
+
const section = sections[sectionIndex];
|
|
408
|
+
if (!isPlainRecord(section) || !Array.isArray(section["tiles"]))
|
|
409
|
+
continue;
|
|
410
|
+
const tiles = section["tiles"];
|
|
411
|
+
for (let tileIndex = 0; tileIndex < tiles.length; tileIndex += 1) {
|
|
412
|
+
const tile = tiles[tileIndex];
|
|
413
|
+
if (!isPlainRecord(tile))
|
|
414
|
+
continue;
|
|
415
|
+
const tilePath = `$["homepageSections"][${sectionIndex}]["tiles"][${tileIndex}]`;
|
|
416
|
+
if (tile["type"] === "SCENARIO_RUN") {
|
|
417
|
+
collectReferenceValue(tile["scenarioId"], propertyPath(tilePath, "scenarioId"), "scenarioId", "scenario", references, referenceIssues);
|
|
418
|
+
}
|
|
419
|
+
if (tile["type"] === "DOWNLOAD_FILE") {
|
|
420
|
+
for (const key of ["folderId", "managedFolderId",]) {
|
|
421
|
+
if (!Object.prototype.hasOwnProperty.call(tile, key))
|
|
422
|
+
continue;
|
|
423
|
+
collectReferenceValue(tile[key], propertyPath(tilePath, key), key, "folder", references, referenceIssues);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const params = tile["params"];
|
|
427
|
+
if (!Array.isArray(params))
|
|
428
|
+
continue;
|
|
429
|
+
for (let paramIndex = 0; paramIndex < params.length; paramIndex += 1) {
|
|
430
|
+
const param = params[paramIndex];
|
|
431
|
+
if (!isPlainRecord(param))
|
|
432
|
+
continue;
|
|
433
|
+
collectReferenceValue(param["name"], `${propertyPath(tilePath, "params")}[${paramIndex}]["name"]`, "name", "variable", references, referenceIssues);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const scenarioIds = new Set();
|
|
440
|
+
const folderIds = new Set();
|
|
441
|
+
const variableNames = new Set();
|
|
442
|
+
if (references.some((r) => r.kind === "scenario")) {
|
|
443
|
+
const scenarios = await this.client.scenarios.list(pk);
|
|
444
|
+
for (const scenario of scenarios)
|
|
445
|
+
scenarioIds.add(scenario.id);
|
|
446
|
+
}
|
|
447
|
+
if (references.some((r) => r.kind === "folder")) {
|
|
448
|
+
const folders = await this.client.folders.list(pk);
|
|
449
|
+
for (const folder of folders)
|
|
450
|
+
folderIds.add(folder.id);
|
|
451
|
+
}
|
|
452
|
+
if (references.some((r) => r.kind === "variable")) {
|
|
453
|
+
const variables = await this.client.variables.get(pk);
|
|
454
|
+
for (const name of Object.keys({ ...variables.standard, ...variables.local, })) {
|
|
455
|
+
variableNames.add(name);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const missingCodeByKind = {
|
|
459
|
+
scenario: "MISSING_SCENARIO",
|
|
460
|
+
folder: "MISSING_FOLDER",
|
|
461
|
+
variable: "MISSING_VARIABLE",
|
|
462
|
+
};
|
|
463
|
+
for (const ref of references) {
|
|
464
|
+
const exists = ref.kind === "scenario"
|
|
465
|
+
? scenarioIds.has(ref.value)
|
|
466
|
+
: ref.kind === "folder"
|
|
467
|
+
? folderIds.has(ref.value)
|
|
468
|
+
: variableNames.has(ref.value);
|
|
469
|
+
ref.exists = exists;
|
|
470
|
+
if (!exists) {
|
|
471
|
+
referenceIssues.push({
|
|
472
|
+
kind: ref.kind,
|
|
473
|
+
issue: {
|
|
474
|
+
code: missingCodeByKind[ref.kind],
|
|
475
|
+
path: ref.path,
|
|
476
|
+
message: `${ref.kind} reference "${ref.value}" does not exist in project ${pk}.`,
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const kindOrder = {
|
|
482
|
+
scenario: 0,
|
|
483
|
+
folder: 1,
|
|
484
|
+
variable: 2,
|
|
485
|
+
};
|
|
486
|
+
references.sort((a, b) => (a.kind === b.kind ? compareStrings(a.path, b.path) : kindOrder[a.kind] - kindOrder[b.kind]));
|
|
487
|
+
const errors = [...rootIssues, ...referenceIssues.map((e) => e.issue),].sort((a, b) => compareStrings(a.path, b.path) || compareStrings(a.code, b.code));
|
|
488
|
+
const checks = [];
|
|
489
|
+
if (rootIssues.length > 0) {
|
|
490
|
+
for (const kind of Object.keys(APP_MANIFEST_REFERENCE_KEYS)) {
|
|
491
|
+
checks.push({ kind, status: "skipped", checked: 0, malformed: 0, missing: 0, });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
for (const kind of Object.keys(APP_MANIFEST_REFERENCE_KEYS)) {
|
|
496
|
+
const kindReferences = references.filter((r) => r.kind === kind);
|
|
497
|
+
const malformed = referenceIssues.filter((e) => e.kind === kind && e.issue.code === "INVALID_REFERENCE_VALUE").length;
|
|
498
|
+
const missing = kindReferences.filter((r) => !r.exists).length;
|
|
499
|
+
checks.push({
|
|
500
|
+
kind,
|
|
501
|
+
status: kindReferences.length === 0 && malformed === 0
|
|
502
|
+
? "skipped"
|
|
503
|
+
: malformed + missing > 0
|
|
504
|
+
? "failed"
|
|
505
|
+
: "ok",
|
|
506
|
+
checked: kindReferences.length,
|
|
507
|
+
malformed,
|
|
508
|
+
missing,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
valid: errors.length === 0,
|
|
514
|
+
projectKey: pk,
|
|
515
|
+
manifestHash: stableHash(manifest),
|
|
516
|
+
references,
|
|
517
|
+
checks,
|
|
518
|
+
errors,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Compare a Dataiku App template manifest with the app-instance manifest of
|
|
523
|
+
* the target project. Root-level project identity fields in
|
|
524
|
+
* APP_MANIFEST_COMPARE_OMITTED_FIELDS are omitted from both sides; every
|
|
525
|
+
* other field — including ones the SDK does not model — is compared.
|
|
526
|
+
* Hashes cover the omitted-field-normalized states; differences are emitted
|
|
527
|
+
* as deterministic, path-ordered entries.
|
|
528
|
+
*/
|
|
529
|
+
async compareAppManifest(appId, projectKey) {
|
|
530
|
+
const pk = this.resolveProjectKey(projectKey);
|
|
531
|
+
const [templateManifest, instanceManifest,] = await Promise.all([
|
|
532
|
+
this.getAppManifest(appId),
|
|
533
|
+
this.getInstanceManifest(pk),
|
|
534
|
+
]);
|
|
535
|
+
const omitted = [...APP_MANIFEST_COMPARE_OMITTED_FIELDS,];
|
|
536
|
+
const template = omitRootFields(templateManifest, omitted);
|
|
537
|
+
const instance = omitRootFields(instanceManifest, omitted);
|
|
538
|
+
const differences = diffValues(template, instance, "$");
|
|
539
|
+
return {
|
|
540
|
+
appId,
|
|
541
|
+
projectKey: pk,
|
|
542
|
+
templateHash: stableHash(template),
|
|
543
|
+
instanceHash: stableHash(instance),
|
|
544
|
+
identical: differences.length === 0,
|
|
545
|
+
omittedFields: omitted,
|
|
546
|
+
differences,
|
|
547
|
+
};
|
|
31
548
|
}
|
|
32
549
|
/** Delete an app instance project. */
|
|
33
|
-
async deleteInstance(projectKey) {
|
|
550
|
+
async deleteInstance(projectKey, options = {}) {
|
|
551
|
+
const expectedIncarnation = options.expectedProjectIncarnationHash;
|
|
552
|
+
if (expectedIncarnation !== undefined && !/^[0-9a-f]{64}$/.test(expectedIncarnation)) {
|
|
553
|
+
throw new ClientValidationError("expectedProjectIncarnationHash must be a 64-character lowercase SHA-256 hash.", "validation_failed");
|
|
554
|
+
}
|
|
34
555
|
let manifest;
|
|
35
556
|
try {
|
|
36
557
|
manifest = await this.getInstanceManifest(projectKey);
|
|
@@ -46,13 +567,31 @@ export class ApplicationsResource extends BaseResource {
|
|
|
46
567
|
throw error;
|
|
47
568
|
}
|
|
48
569
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
570
|
+
const pk = this.resolveProjectKey(projectKey);
|
|
571
|
+
let projectDetails;
|
|
572
|
+
const projectAppType = typeof manifest?.projectAppType === "string"
|
|
573
|
+
? manifest.projectAppType
|
|
574
|
+
: manifest
|
|
575
|
+
? (projectDetails = await this.getProjectDetails(pk)).projectAppType
|
|
576
|
+
: undefined;
|
|
577
|
+
if (projectAppType !== "APP_INSTANCE") {
|
|
578
|
+
throw new ClientValidationError("Only classic Dataiku App instance projects can be deleted through app delete-instance.", "validation_failed", "Use `dss app delete-instance` only for APP_INSTANCE projects; use `dss project delete` for templates or ordinary projects.", {
|
|
579
|
+
projectAppType: typeof projectAppType === "string" ? projectAppType : null,
|
|
580
|
+
projectKey: pk,
|
|
53
581
|
});
|
|
54
582
|
}
|
|
55
|
-
|
|
583
|
+
if (expectedIncarnation !== undefined) {
|
|
584
|
+
const details = projectDetails ?? await this.getProjectDetails(pk);
|
|
585
|
+
const currentIncarnation = projectIncarnationHash(pk, details);
|
|
586
|
+
if (currentIncarnation !== expectedIncarnation) {
|
|
587
|
+
throw new ClientValidationError(`Project ${pk} is not the project incarnation authorized for deletion.`, "validation_failed", "Refusing deletion after project-key reuse. Inspect the current project and do not delete it as cleanup for the replaced project.", {
|
|
588
|
+
projectKey: pk,
|
|
589
|
+
expectedProjectIncarnationHash: expectedIncarnation,
|
|
590
|
+
currentProjectIncarnationHash: currentIncarnation ?? null,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
await this.client.del(`/public/api/projects/${encodeURIComponent(pk)}`);
|
|
56
595
|
}
|
|
57
596
|
/** List all Business Apps. */
|
|
58
597
|
async listBusinessApps() {
|
|
@@ -254,6 +254,14 @@ function shouldRetryWithConnectionInferredType(error) {
|
|
|
254
254
|
|| detail.includes("illegal argument"));
|
|
255
255
|
}
|
|
256
256
|
function buildDatasetCreateBody(opts) {
|
|
257
|
+
if (opts.dsType.toLowerCase() === "uploadedfiles") {
|
|
258
|
+
return {
|
|
259
|
+
projectKey: opts.projectKey,
|
|
260
|
+
name: opts.datasetName,
|
|
261
|
+
type: opts.dsType,
|
|
262
|
+
params: { uploadConnection: opts.connection, },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
257
265
|
if (opts.table) {
|
|
258
266
|
const params = {
|
|
259
267
|
connection: opts.connection,
|
|
@@ -40,7 +40,10 @@ export class FuturesResource extends BaseResource {
|
|
|
40
40
|
}
|
|
41
41
|
async wait(futureId, opts = {}) {
|
|
42
42
|
const baseIntervalMs = Math.max(1, opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
43
|
-
|
|
43
|
+
// A caller's budget is never rounded up to a whole poll interval: a 5ms
|
|
44
|
+
// wait must answer in about 5ms. One state observation always happens
|
|
45
|
+
// regardless, because the poll precedes the deadline check.
|
|
46
|
+
const timeoutMs = Math.max(0, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
44
47
|
const startedAt = Date.now();
|
|
45
48
|
let pollCount = 0;
|
|
46
49
|
while (true) {
|
|
@@ -80,7 +83,9 @@ export class FuturesResource extends BaseResource {
|
|
|
80
83
|
};
|
|
81
84
|
return this.client.safeParse(FutureWaitResultSchema, result, "futures.wait");
|
|
82
85
|
}
|
|
83
|
-
|
|
86
|
+
// Only the remaining budget is slept: a longer sleep would report an
|
|
87
|
+
// elapsed time the caller never authorized.
|
|
88
|
+
await sleep(Math.min(baseIntervalMs, timeoutMs - elapsedMs));
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
}
|