dataiku-sdk 0.12.4 → 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.
Files changed (84) hide show
  1. package/README.md +118 -7
  2. package/dist/packages/types/src/index.d.ts +5 -0
  3. package/dist/packages/types/src/index.js +5 -0
  4. package/dist/src/cli/coerce.d.ts +1 -3
  5. package/dist/src/cli/coerce.js +1 -17
  6. package/dist/src/cli/commands/app.js +709 -14
  7. package/dist/src/cli/commands/code-env.js +2 -2
  8. package/dist/src/cli/commands/dashboard.js +1 -1
  9. package/dist/src/cli/commands/dataset.js +2 -2
  10. package/dist/src/cli/commands/folder.js +1 -1
  11. package/dist/src/cli/commands/insight.js +2 -2
  12. package/dist/src/cli/commands/notebook.js +2 -2
  13. package/dist/src/cli/commands/recipe.js +3 -3
  14. package/dist/src/cli/commands/scenario.js +1 -1
  15. package/dist/src/cli/commands/sql.js +1 -1
  16. package/dist/src/cli/commands/variable.js +1 -1
  17. package/dist/src/cli/commands/wiki.js +1 -1
  18. package/dist/src/cli/contract.d.ts +12 -3
  19. package/dist/src/cli/contract.js +805 -49
  20. package/dist/src/cli/flags.js +8 -0
  21. package/dist/src/cli/helpers/app-permissions.d.ts +63 -0
  22. package/dist/src/cli/helpers/app-permissions.js +239 -0
  23. package/dist/src/cli/helpers/app-successor.d.ts +106 -0
  24. package/dist/src/cli/helpers/app-successor.js +625 -0
  25. package/dist/src/cli/helpers/cleanup.js +46 -12
  26. package/dist/src/cli/output.d.ts +12 -1
  27. package/dist/src/cli/output.js +53 -6
  28. package/dist/src/cli/usage.d.ts +1 -1
  29. package/dist/src/cli/usage.js +5 -2
  30. package/dist/src/cli.js +329 -32
  31. package/dist/src/client.d.ts +6 -0
  32. package/dist/src/client.js +28 -1
  33. package/dist/src/errors.d.ts +1 -1
  34. package/dist/src/resources/applications.d.ts +191 -1
  35. package/dist/src/resources/applications.js +549 -10
  36. package/dist/src/resources/datasets.js +8 -0
  37. package/dist/src/resources/futures.js +7 -2
  38. package/dist/src/skill.js +76 -18
  39. package/dist/src/utils/cleanup-ledger.d.ts +34 -1
  40. package/dist/src/utils/cleanup-ledger.js +118 -7
  41. package/dist/src/utils/dss-url.d.ts +6 -0
  42. package/dist/src/utils/dss-url.js +17 -0
  43. package/dist/src/utils/project-incarnation.d.ts +6 -0
  44. package/dist/src/utils/project-incarnation.js +27 -0
  45. package/dist/src/utils/stable-hash.d.ts +11 -0
  46. package/dist/src/utils/stable-hash.js +27 -0
  47. package/node_modules/@sinclair/typebox/build/cjs/compiler/compiler.js +26 -46
  48. package/node_modules/@sinclair/typebox/build/cjs/parser/runtime/guard.d.ts +1 -1
  49. package/node_modules/@sinclair/typebox/build/cjs/parser/runtime/token.d.ts +2 -2
  50. package/node_modules/@sinclair/typebox/build/cjs/syntax/mapping.d.ts +1 -1
  51. package/node_modules/@sinclair/typebox/build/cjs/system/evaluate.d.ts +5 -0
  52. package/node_modules/@sinclair/typebox/build/cjs/system/evaluate.js +11 -0
  53. package/node_modules/@sinclair/typebox/build/cjs/system/index.d.ts +1 -0
  54. package/node_modules/@sinclair/typebox/build/cjs/system/index.js +1 -0
  55. package/node_modules/@sinclair/typebox/build/cjs/type/object/object.d.ts +2 -2
  56. package/node_modules/@sinclair/typebox/build/cjs/type/object/object.js +18 -2
  57. package/node_modules/@sinclair/typebox/build/cjs/value/parse/parse.d.ts +1 -1
  58. package/node_modules/@sinclair/typebox/build/esm/compiler/compiler.mjs +27 -47
  59. package/node_modules/@sinclair/typebox/build/esm/parser/runtime/guard.d.mts +1 -1
  60. package/node_modules/@sinclair/typebox/build/esm/parser/runtime/token.d.mts +2 -2
  61. package/node_modules/@sinclair/typebox/build/esm/syntax/mapping.d.mts +1 -1
  62. package/node_modules/@sinclair/typebox/build/esm/system/evaluate.d.mts +5 -0
  63. package/node_modules/@sinclair/typebox/build/esm/system/evaluate.mjs +7 -0
  64. package/node_modules/@sinclair/typebox/build/esm/system/index.d.mts +1 -0
  65. package/node_modules/@sinclair/typebox/build/esm/system/index.mjs +1 -0
  66. package/node_modules/@sinclair/typebox/build/esm/type/composite/composite.mjs +2 -2
  67. package/node_modules/@sinclair/typebox/build/esm/type/const/const.mjs +3 -3
  68. package/node_modules/@sinclair/typebox/build/esm/type/instantiate/instantiate.mjs +2 -2
  69. package/node_modules/@sinclair/typebox/build/esm/type/mapped/mapped.mjs +3 -3
  70. package/node_modules/@sinclair/typebox/build/esm/type/module/compute.mjs +2 -2
  71. package/node_modules/@sinclair/typebox/build/esm/type/object/object.d.mts +2 -2
  72. package/node_modules/@sinclair/typebox/build/esm/type/object/object.mjs +18 -2
  73. package/node_modules/@sinclair/typebox/build/esm/type/omit/omit.mjs +3 -3
  74. package/node_modules/@sinclair/typebox/build/esm/type/partial/partial.mjs +3 -3
  75. package/node_modules/@sinclair/typebox/build/esm/type/pick/pick.mjs +3 -3
  76. package/node_modules/@sinclair/typebox/build/esm/type/record/record.mjs +3 -3
  77. package/node_modules/@sinclair/typebox/build/esm/type/required/required.mjs +3 -3
  78. package/node_modules/@sinclair/typebox/build/esm/type/type/json.mjs +2 -2
  79. package/node_modules/@sinclair/typebox/build/esm/value/delta/delta.mjs +4 -4
  80. package/node_modules/@sinclair/typebox/build/esm/value/parse/parse.d.mts +1 -1
  81. package/node_modules/@sinclair/typebox/package.json +1 -1
  82. package/package.json +7 -5
  83. package/packages/types/dist/index.d.ts +5 -0
  84. 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 currentManifest = await this.getInstanceManifest(projectKey);
27
- if (currentManifest.projectAppType === "APP_INSTANCE") {
28
- throw new ClientValidationError("Classic Dataiku App instance manifests cannot be saved through the app-manifest endpoint; only Dataiku App template project manifests can be updated.", "validation_failed", "Save against the Dataiku App template project instead; existing classic app-instance manifests are read-only through this endpoint.", { projectAppType: "APP_INSTANCE", projectKey: this.resolveProjectKey(projectKey), });
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
- await this.client.putVoid(`/public/api/projects/${this.enc(projectKey)}/app-manifest`, manifest);
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
- if (manifest?.projectAppType !== "APP_INSTANCE") {
50
- 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 projects whose app manifest has projectAppType=APP_INSTANCE; use `dss project delete` for ordinary projects.", {
51
- projectAppType: manifest?.projectAppType ?? null,
52
- projectKey: this.resolveProjectKey(projectKey),
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
- await this.client.del(`/public/api/projects/${this.enc(projectKey)}`);
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
- const timeoutMs = Math.max(baseIntervalMs, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
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
- await sleep(baseIntervalMs);
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
  }