dataiku-sdk 3.4.0 → 3.5.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.
Files changed (110) hide show
  1. package/README.md +3 -3
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/src/cli/command-syntax.json +14524 -0
  4. package/dist/src/cli/commands/analysis.js +6 -9
  5. package/dist/src/cli/commands/api-deployer.js +15 -33
  6. package/dist/src/cli/commands/api-service.js +17 -27
  7. package/dist/src/cli/commands/app.js +22 -39
  8. package/dist/src/cli/commands/bundle.js +15 -24
  9. package/dist/src/cli/commands/business-app.js +10 -17
  10. package/dist/src/cli/commands/code-env.js +23 -36
  11. package/dist/src/cli/commands/code.js +4 -4
  12. package/dist/src/cli/commands/connection.js +12 -22
  13. package/dist/src/cli/commands/continuous-activity.js +6 -9
  14. package/dist/src/cli/commands/dashboard.js +8 -13
  15. package/dist/src/cli/commands/data-collection.js +9 -16
  16. package/dist/src/cli/commands/data-quality.js +14 -26
  17. package/dist/src/cli/commands/dataset.js +27 -50
  18. package/dist/src/cli/commands/discussion.js +7 -10
  19. package/dist/src/cli/commands/doctor.js +3 -3
  20. package/dist/src/cli/commands/flow-zone.js +8 -17
  21. package/dist/src/cli/commands/folder.js +12 -20
  22. package/dist/src/cli/commands/future.js +7 -10
  23. package/dist/src/cli/commands/group.js +7 -11
  24. package/dist/src/cli/commands/insight.js +7 -11
  25. package/dist/src/cli/commands/job.js +13 -23
  26. package/dist/src/cli/commands/knowledge-bank.js +5 -6
  27. package/dist/src/cli/commands/llm.js +5 -8
  28. package/dist/src/cli/commands/macro.js +9 -16
  29. package/dist/src/cli/commands/meaning.js +7 -11
  30. package/dist/src/cli/commands/metrics.js +7 -10
  31. package/dist/src/cli/commands/ml-task.js +12 -20
  32. package/dist/src/cli/commands/model-evaluation-store.js +7 -11
  33. package/dist/src/cli/commands/notebook.js +15 -27
  34. package/dist/src/cli/commands/plugin.js +35 -68
  35. package/dist/src/cli/commands/project-deployer.js +10 -22
  36. package/dist/src/cli/commands/project-folder.js +10 -17
  37. package/dist/src/cli/commands/project-git.js +38 -70
  38. package/dist/src/cli/commands/project-library.js +15 -23
  39. package/dist/src/cli/commands/project.js +9 -26
  40. package/dist/src/cli/commands/recipe.d.ts +16 -0
  41. package/dist/src/cli/commands/recipe.js +83 -94
  42. package/dist/src/cli/commands/saved-model.js +23 -39
  43. package/dist/src/cli/commands/scenario.js +20 -34
  44. package/dist/src/cli/commands/sql.js +4 -4
  45. package/dist/src/cli/commands/statistics.js +11 -18
  46. package/dist/src/cli/commands/streaming-endpoint.js +7 -11
  47. package/dist/src/cli/commands/user.js +11 -22
  48. package/dist/src/cli/commands/variable.js +3 -4
  49. package/dist/src/cli/commands/webapp.js +10 -16
  50. package/dist/src/cli/commands/wiki.js +9 -13
  51. package/dist/src/cli/commands/workspace.js +8 -14
  52. package/dist/src/cli/contract.d.ts +5 -15
  53. package/dist/src/cli/contract.js +156 -253
  54. package/dist/src/cli/doctor.js +1 -0
  55. package/dist/src/cli/env.d.ts +2 -0
  56. package/dist/src/cli/env.js +15 -1
  57. package/dist/src/cli/helpers/app-permissions.js +2 -1
  58. package/dist/src/cli/helpers/flow-zone.js +2 -1
  59. package/dist/src/cli/helpers/recipe.js +2 -1
  60. package/dist/src/cli/syntax.d.ts +83 -0
  61. package/dist/src/cli/syntax.js +217 -0
  62. package/dist/src/cli/types.d.ts +3 -0
  63. package/dist/src/cli/usage.d.ts +1 -1
  64. package/dist/src/cli/usage.js +4 -2
  65. package/dist/src/cli/version.js +2 -0
  66. package/dist/src/cli.js +37 -64
  67. package/dist/src/client.js +7 -6
  68. package/dist/src/errors.d.ts +12 -1
  69. package/dist/src/errors.js +28 -3
  70. package/dist/src/index.d.ts +1 -3
  71. package/dist/src/index.js +2 -2
  72. package/dist/src/resources/api-services.js +2 -3
  73. package/dist/src/resources/applications.js +2 -4
  74. package/dist/src/resources/code-envs.js +2 -2
  75. package/dist/src/resources/data-quality.js +4 -3
  76. package/dist/src/resources/datasets.d.ts +13 -0
  77. package/dist/src/resources/datasets.js +5 -4
  78. package/dist/src/resources/flow-zones.js +4 -2
  79. package/dist/src/resources/folders.js +4 -2
  80. package/dist/src/resources/futures.js +1 -3
  81. package/dist/src/resources/insights.js +3 -2
  82. package/dist/src/resources/jobs.js +4 -6
  83. package/dist/src/resources/macros.js +2 -4
  84. package/dist/src/resources/notebooks.js +2 -3
  85. package/dist/src/resources/project-git.js +9 -11
  86. package/dist/src/resources/project-library.d.ts +0 -2
  87. package/dist/src/resources/project-library.js +2 -7
  88. package/dist/src/resources/projects.js +4 -3
  89. package/dist/src/resources/recipes.d.ts +28 -0
  90. package/dist/src/resources/recipes.js +116 -84
  91. package/dist/src/resources/sql.js +8 -3
  92. package/dist/src/resources/statistics.js +2 -1
  93. package/dist/src/resources/variables.js +2 -1
  94. package/dist/src/resources/webapps.js +2 -3
  95. package/dist/src/schemas.d.ts +1 -2
  96. package/dist/src/schemas.js +1 -1
  97. package/dist/src/skill.js +6 -7
  98. package/dist/src/utils/cleanup-ledger.js +9 -8
  99. package/dist/src/utils/polling.d.ts +3 -0
  100. package/dist/src/utils/polling.js +3 -0
  101. package/dist/src/utils/project-archive.js +2 -2
  102. package/dist/src/utils/stable-hash.d.ts +3 -1
  103. package/dist/src/utils/stable-hash.js +2 -0
  104. package/package.json +4 -4
  105. package/plugin.json +1 -1
  106. package/skills/dataiku-dss/SKILL.md +2 -3
  107. package/skills/dataiku-dss/references/authentication.md +3 -3
  108. package/skills/dataiku-dss/references/coding.md +1 -1
  109. package/dist/src/utils/pagination.d.ts +0 -8
  110. package/dist/src/utils/pagination.js +0 -23
@@ -1,3 +1,4 @@
1
+ import { ClientValidationError, unexpectedResponseError, } from "../errors.js";
1
2
  import { InsightDetailsSchema, InsightSummaryArraySchema, } from "../schemas.js";
2
3
  import { deepMerge, } from "../utils/deep-merge.js";
3
4
  import { BaseResource, } from "./base.js";
@@ -33,7 +34,7 @@ export class InsightsResource extends BaseResource {
33
34
  const pk = this.resolveProjectKey(opts.projectKey);
34
35
  const prototype = applyInsightFields(opts.data ?? {}, opts);
35
36
  if (!hasCreateShape(prototype)) {
36
- throw new Error("Insight create requires name and type, either as options or in data.");
37
+ throw new ClientValidationError("Insight create requires name and type, either as options or in data.", "validation_failed");
37
38
  }
38
39
  if (prototype.projectKey === undefined)
39
40
  prototype.projectKey = pk;
@@ -43,7 +44,7 @@ export class InsightsResource extends BaseResource {
43
44
  ...(opts.payload !== undefined ? { payload: opts.payload, } : {}),
44
45
  });
45
46
  if (typeof created.id !== "string" || created.id.length === 0) {
46
- throw new Error("Insight create response did not include an id.");
47
+ throw unexpectedResponseError("Insight create response did not include an id.");
47
48
  }
48
49
  return this.get(created.id, pk);
49
50
  }
@@ -1,9 +1,7 @@
1
- import { DataikuError, } from "../errors.js";
1
+ import { ClientValidationError, DataikuError, } from "../errors.js";
2
2
  import { JobSummaryArraySchema, } from "../schemas.js";
3
- import { computeNextPollDelayMs, isRequestDeadlineError, } from "../utils/polling.js";
3
+ import { computeNextPollDelayMs, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, isRequestDeadlineError, } from "../utils/polling.js";
4
4
  import { BaseResource, } from "./base.js";
5
- const DEFAULT_POLL_INTERVAL_MS = 2_000;
6
- const DEFAULT_TIMEOUT_MS = 120_000;
7
5
  const DEFAULT_MAX_LOG_LINES = 500;
8
6
  /**
9
7
  * Default bytes retained from a job log body (default 10 MiB). Logs larger
@@ -51,7 +49,7 @@ function jobBuildOutput(target, defaultProjectKey, defaultPartition, defaultTarg
51
49
  }
52
50
  function jobBuildDefinition(targets, defaultProjectKey, opts) {
53
51
  if (targets.length === 0) {
54
- throw new Error("At least one build target is required.");
52
+ throw new ClientValidationError("At least one build target is required.", "validation_failed");
55
53
  }
56
54
  const payload = {
57
55
  outputs: targets.map((target) => jobBuildOutput(target, defaultProjectKey, opts?.partition, opts?.targetType)),
@@ -203,7 +201,7 @@ export class JobsResource extends BaseResource {
203
201
  const jobId = parsed.searchParams.get("jobId") ?? undefined;
204
202
  const activity = parsed.searchParams.get("activityId") ?? undefined;
205
203
  if (!projectKey || !jobId || !activity) {
206
- throw new Error("Log URL must include projectKey, jobId, and activityId query parameters.");
204
+ throw new ClientValidationError("Log URL must include projectKey, jobId, and activityId query parameters.", "validation_failed");
207
205
  }
208
206
  return this.log(jobId, {
209
207
  activity,
@@ -1,9 +1,7 @@
1
1
  import { ClientValidationError, DataikuError, } from "../errors.js";
2
2
  import { MacroDefinitionSchema, MacroRunStartSchema, MacroStateSchema, MacroSummaryArraySchema, MacroWaitResultSchema, } from "../schemas.js";
3
- import { computeNextPollDelayMs, isRequestDeadlineError, } from "../utils/polling.js";
3
+ import { computeNextPollDelayMs, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, isRequestDeadlineError, } from "../utils/polling.js";
4
4
  import { BaseResource, requireNonEmpty, } from "./base.js";
5
- const DEFAULT_POLL_INTERVAL_MS = 2_000;
6
- const DEFAULT_TIMEOUT_MS = 120_000;
7
5
  /** Hard floor for the result download cap; keeps a caller typo from hanging the read. */
8
6
  const MIN_RESULT_MAX_BYTES = 1;
9
7
  const MAX_RESULT_MAX_BYTES = 2 * 1024 * 1024 * 1024;
@@ -121,7 +119,7 @@ export class MacrosResource extends BaseResource {
121
119
  const maxBytes = macroResultMaxBytes(opts?.maxBytes);
122
120
  const raw = await this.client.getTextLimited(`${this.base(runnableType, opts?.projectKey)}/result/${encodeURIComponent(runId)}`, maxBytes);
123
121
  if (raw.truncated) {
124
- throw new Error(`Macro result exceeded ${String(maxBytes)} bytes; retry with a larger maxBytes.`);
122
+ throw new ClientValidationError(`Macro result exceeded ${String(maxBytes)} bytes; retry with a larger maxBytes.`, "validation_failed");
125
123
  }
126
124
  const text = raw.text;
127
125
  const trimmed = text.trim();
@@ -1,8 +1,7 @@
1
1
  import { ClientValidationError, DataikuError, } from "../errors.js";
2
2
  import { JupyterNotebookContentSchema, JupyterNotebookSummaryArraySchema, NotebookSessionArraySchema, SqlNotebookContentSchema, SqlNotebookHistorySchema, SqlNotebookSummaryArraySchema, } from "../schemas.js";
3
- import { stableHash, } from "../utils/stable-hash.js";
3
+ import { SHA256_HEX_PATTERN, stableHash, } from "../utils/stable-hash.js";
4
4
  import { BaseResource, } from "./base.js";
5
- const EXPECT_HASH_PATTERN = /^[0-9a-fA-F]{64}$/;
6
5
  export class NotebooksResource extends BaseResource {
7
6
  // ── Jupyter Notebooks ──────────────────────────────────────────────
8
7
  /**
@@ -260,7 +259,7 @@ export class NotebooksResource extends BaseResource {
260
259
  }
261
260
  /** Validate the armed `--expect-hash` value into a comparable hex digest. */
262
261
  validatedExpectHash(expectHash, pk, resourceLabel, id) {
263
- if (!EXPECT_HASH_PATTERN.test(expectHash)) {
262
+ if (!SHA256_HEX_PATTERN.test(expectHash)) {
264
263
  throw new ClientValidationError(`Expected ${resourceLabel} hash must be a 64-character SHA-256 hex digest.`, "validation_failed", "Use the hash value returned by a previous save or read.", { projectKey: pk, id, });
265
264
  }
266
265
  return expectHash.toLowerCase();
@@ -1,7 +1,7 @@
1
- import { ClientValidationError, } from "../errors.js";
1
+ import { ClientValidationError, DataikuError, nonJsonResponseBody, } from "../errors.js";
2
2
  import { ProjectGitActionResultSchema, ProjectGitDiffResultSchema, ProjectGitFutureResponseSchema, ProjectGitFutureStateSchema, ProjectGitLibrariesSchema, ProjectGitLogResultSchema, ProjectGitRemoteSchema, ProjectGitStatusSchema, ProjectGitTagsSchema, } from "../schemas.js";
3
3
  import { encodeGitReferencePath, validateGitReferencePath, } from "../utils/git-reference.js";
4
- import { computeNextPollDelayMs, isRequestDeadlineError, } from "../utils/polling.js";
4
+ import { computeNextPollDelayMs, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, isRequestDeadlineError, } from "../utils/polling.js";
5
5
  import { sanitizeErrorSecrets, } from "../utils/secret-sanitize.js";
6
6
  import { BaseResource, } from "./base.js";
7
7
  /**
@@ -13,8 +13,6 @@ const GIT_API_BASE = "/dip/publicapi";
13
13
  const DEFAULT_REMOTE = "origin";
14
14
  const DEFAULT_LOG_COUNT = 1_000;
15
15
  const DEFAULT_TAG_REFERENCE = "HEAD";
16
- const DEFAULT_POLL_INTERVAL_MS = 2_000;
17
- const DEFAULT_TIMEOUT_MS = 120_000;
18
16
  function sleep(ms) {
19
17
  const { promise, resolve, } = Promise.withResolvers();
20
18
  setTimeout(resolve, ms);
@@ -362,7 +360,7 @@ export class ProjectGitResource extends BaseResource {
362
360
  // the loop reports the documented timeout from the last observed
363
361
  // state instead of issuing a request the budget cannot cover.
364
362
  if (lastState !== undefined && elapsedBeforeMs >= timeoutMs) {
365
- throw new Error(`Timed out after ${String(elapsedBeforeMs)}ms waiting for Dataiku future ${jobId}`);
363
+ throw new ClientValidationError(`Timed out after ${String(elapsedBeforeMs)}ms waiting for Dataiku future ${jobId}`, "long_running_failure");
366
364
  }
367
365
  // Unlike peek state, the ordinary state includes the completed result.
368
366
  // Requests issued while budget remains are bounded by the remaining
@@ -386,23 +384,23 @@ export class ProjectGitResource extends BaseResource {
386
384
  // error escape.
387
385
  if (!isRequestDeadlineError(error, startedAt + timeoutMs))
388
386
  throw error;
389
- throw new Error(`Timed out after ${String(Date.now() - startedAt)}ms waiting for Dataiku future ${jobId}`, { cause: error, });
387
+ throw new ClientValidationError(`Timed out after ${String(Date.now() - startedAt)}ms waiting for Dataiku future ${jobId}`, "long_running_failure", undefined, undefined, { cause: error, });
390
388
  }
391
389
  lastState = state;
392
390
  const failure = describeFutureFailure(state.error);
393
391
  if (failure !== undefined) {
394
- throw new Error(`Dataiku future ${jobId} failed: ${failure}`);
392
+ throw new ClientValidationError(`Dataiku future ${jobId} failed: ${failure}`, "long_running_failure");
395
393
  }
396
394
  if (state.aborted === true) {
397
- throw new Error(`Dataiku future ${jobId} was aborted`);
395
+ throw new ClientValidationError(`Dataiku future ${jobId} was aborted`, "long_running_failure");
398
396
  }
399
397
  if (state.hasResult === true)
400
398
  return state.result;
401
399
  if (state.unknown === true) {
402
- throw new Error(`Dataiku future ${jobId} is unknown to the server`);
400
+ throw new ClientValidationError(`Dataiku future ${jobId} is unknown to the server`, "long_running_failure");
403
401
  }
404
402
  if (state.alive === false) {
405
- throw new Error(`Dataiku future ${jobId} ended without producing a result`);
403
+ throw new ClientValidationError(`Dataiku future ${jobId} ended without producing a result`, "long_running_failure");
406
404
  }
407
405
  const elapsedMs = Date.now() - startedAt;
408
406
  // Deadline reached: the guard at the top of the loop reports the
@@ -449,7 +447,7 @@ export class ProjectGitResource extends BaseResource {
449
447
  return JSON.parse(text);
450
448
  }
451
449
  catch {
452
- throw new Error(`Dataiku Git API returned a non-JSON response for ${method} ${path}`);
450
+ throw new DataikuError(res.status, "Invalid JSON response", nonJsonResponseBody(text));
453
451
  }
454
452
  }
455
453
  catch (error) {
@@ -75,8 +75,6 @@ export interface ProjectLibraryDiffResult {
75
75
  * checked; it can never turn the subsequent POST into a conditional write.
76
76
  */
77
77
  export declare const PROJECT_LIBRARY_CONCURRENCY_CONTROL = "client-side-non-atomic-stale-read-check";
78
- /** Lowercase or uppercase SHA-256 hex digest. */
79
- export declare const EXPECT_SHA256_PATTERN: RegExp;
80
78
  /**
81
79
  * Validate and canonicalize a project library path.
82
80
  *
@@ -1,5 +1,5 @@
1
- import { createHash, } from "node:crypto";
2
1
  import { ClientValidationError, DataikuError, } from "../errors.js";
2
+ import { SHA256_HEX_PATTERN, sha256Hex, } from "../utils/stable-hash.js";
3
3
  import { BaseResource, } from "./base.js";
4
4
  /**
5
5
  * The only concurrency control the public project-library API permits. DSS
@@ -8,11 +8,6 @@ import { BaseResource, } from "./base.js";
8
8
  * checked; it can never turn the subsequent POST into a conditional write.
9
9
  */
10
10
  export const PROJECT_LIBRARY_CONCURRENCY_CONTROL = "client-side-non-atomic-stale-read-check";
11
- /** Lowercase or uppercase SHA-256 hex digest. */
12
- export const EXPECT_SHA256_PATTERN = /^[0-9a-fA-F]{64}$/;
13
- function sha256Hex(value) {
14
- return createHash("sha256").update(value).digest("hex");
15
- }
16
11
  const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]/;
17
12
  const BACKSLASH_RE = /\\/;
18
13
  const TRAILING_AMBIGUOUS_RE = /[\s.]$/;
@@ -376,7 +371,7 @@ export class ProjectLibraryResource extends BaseResource {
376
371
  }
377
372
  const expectSha256 = options?.expectSha256;
378
373
  if (expectSha256 !== undefined
379
- && (typeof expectSha256 !== "string" || !EXPECT_SHA256_PATTERN.test(expectSha256))) {
374
+ && (typeof expectSha256 !== "string" || !SHA256_HEX_PATTERN.test(expectSha256))) {
380
375
  throw new ClientValidationError("expectSha256 must be a 64-character SHA-256 hex digest.", "validation_failed", "Use the sha256 value returned by a project library read or diff.", { path: valid, });
381
376
  }
382
377
  let beforeSha256;
@@ -1,4 +1,4 @@
1
- import { ClientValidationError, DataikuError, } from "../errors.js";
1
+ import { ClientValidationError, DataikuError, unexpectedResponseError, } from "../errors.js";
2
2
  import { isRequestDeadlineError, } from "../utils/polling.js";
3
3
  import { writeResponseToFile, } from "../utils/response-file.js";
4
4
  import { ProjectDetailsSchema, ProjectMetadataSchema, ProjectSummaryArraySchema, ProjectTagsSchema, } from "../schemas.js";
@@ -140,8 +140,9 @@ export class ProjectsResource extends BaseResource {
140
140
  /** Export a project archive to a local file. */
141
141
  async exportArchive(projectKey, filePath, options) {
142
142
  const res = await this.client.postStream(`/public/api/projects/${encodeURIComponent(projectKey)}/export`, options ?? {});
143
- if (!res.body)
144
- throw new Error("projects.exportArchive response did not include a body");
143
+ if (!res.body) {
144
+ throw unexpectedResponseError("projects.exportArchive response did not include a body");
145
+ }
145
146
  await writeResponseToFile(filePath, res);
146
147
  }
147
148
  /** Upload a project archive and return its temporary import handle. */
@@ -1,6 +1,11 @@
1
1
  import type { BuildMode, JobWaitResult, ProjectMetadata, RecipeCreateOptions, RecipeCreateResult, RecipeSummary } from "../schemas.js";
2
2
  import { BaseResource } from "./base.js";
3
3
  import type { JobBuildTarget, JobBuildTargetType, JobLogFilter, JobLogSummary } from "./jobs.js";
4
+ type JoinType = "LEFT" | "INNER" | "RIGHT" | "FULL";
5
+ type JoinKeyPair = {
6
+ left: string;
7
+ right: string;
8
+ };
4
9
  export interface RecipeRunOutput extends JobBuildTarget {
5
10
  ref: string;
6
11
  role: string;
@@ -65,6 +70,28 @@ export interface RecipeCloneResult {
65
70
  payloadTextRewrites: Record<string, string>;
66
71
  copiedOutputDatasets: string[];
67
72
  }
73
+ /**
74
+ * Pure request construction for POST /recipes/ (no DSS calls). Shared with
75
+ * `recipe create --plan` so the plan equals the request the command sends.
76
+ */
77
+ export declare function buildRecipeCreateRequest(opts: RecipeCreateOptions, pk: string): {
78
+ creationSettings: Record<string, unknown>;
79
+ fuzzyDistance: string | undefined;
80
+ fuzzyKeys: JoinKeyPair[] | undefined;
81
+ fuzzyThreshold: number;
82
+ inputDatasets: string[] | undefined;
83
+ inputs: Record<string, unknown>;
84
+ joinKeys: JoinKeyPair[] | undefined;
85
+ name: string;
86
+ normalizedJoinType: JoinType | undefined;
87
+ outputFolder: string | undefined;
88
+ outputs: Record<string, unknown>;
89
+ payload: string | undefined;
90
+ rawConnection: string | undefined;
91
+ recipePrototype: Record<string, unknown>;
92
+ temporaryOutputDataset: string | undefined;
93
+ type: string;
94
+ };
68
95
  export declare class RecipesResource extends BaseResource {
69
96
  /** List all recipes in a project. */
70
97
  list(projectKey?: string): Promise<RecipeSummary[]>;
@@ -144,3 +171,4 @@ export declare class RecipesResource extends BaseResource {
144
171
  projectKey?: string;
145
172
  }): Promise<string>;
146
173
  }
174
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { writeFile, } from "node:fs/promises";
2
2
  import { resolve, } from "node:path";
3
- import { DataikuError, } from "../errors.js";
3
+ import { ClientValidationError, DataikuError, nonJsonResponseBody, unexpectedResponseError, } from "../errors.js";
4
4
  import { ProjectMetadataSchema, RecipeSummaryArraySchema, } from "../schemas.js";
5
5
  import { deepMerge, } from "../utils/deep-merge.js";
6
6
  import { sanitizeFileName, } from "../utils/sanitize.js";
@@ -25,7 +25,13 @@ function asRecord(value) {
25
25
  function parseRecipePayload(payload) {
26
26
  if (!payload)
27
27
  return {};
28
- const parsed = JSON.parse(payload);
28
+ let parsed;
29
+ try {
30
+ parsed = JSON.parse(payload);
31
+ }
32
+ catch {
33
+ throw new DataikuError(200, "Invalid JSON response", nonJsonResponseBody(payload));
34
+ }
29
35
  return asRecord(parsed) ?? {};
30
36
  }
31
37
  function parseJoinKeyPairs(values, optionName = "joinOn") {
@@ -41,8 +47,9 @@ function parseJoinKeyPairs(values, optionName = "joinOn") {
41
47
  }
42
48
  const left = token.slice(0, eq).trim();
43
49
  const right = token.slice(eq + 1).trim();
44
- if (!left || !right)
45
- throw new Error(`${optionName} values must use COL or LEFT=RIGHT.`);
50
+ if (!left || !right) {
51
+ throw new ClientValidationError(`${optionName} values must use COL or LEFT=RIGHT.`, "validation_failed");
52
+ }
46
53
  pairs.push({ left, right, });
47
54
  }
48
55
  return pairs;
@@ -57,7 +64,7 @@ function normalizeFuzzyDistance(value) {
57
64
  || normalized === "EUCLIDEAN") {
58
65
  return normalized;
59
66
  }
60
- throw new Error("fuzzyDistance must be one of DAMERAU_LEVENSHTEIN, HAMMING, JACCARD, COSINE, or EUCLIDEAN.");
67
+ throw new ClientValidationError("fuzzyDistance must be one of DAMERAU_LEVENSHTEIN, HAMMING, JACCARD, COSINE, or EUCLIDEAN.", "validation_failed");
61
68
  }
62
69
  function normalizeJoinType(value) {
63
70
  const normalized = typeof value === "string" && value.trim().length > 0
@@ -67,7 +74,7 @@ function normalizeJoinType(value) {
67
74
  || normalized === "FULL") {
68
75
  return normalized;
69
76
  }
70
- throw new Error("joinType must be one of LEFT, INNER, RIGHT, or FULL.");
77
+ throw new ClientValidationError("joinType must be one of LEFT, INNER, RIGHT, or FULL.", "validation_failed");
71
78
  }
72
79
  function recipeVirtualInputs(payload, inputCount) {
73
80
  const current = Array.isArray(payload.virtualInputs) ? payload.virtualInputs : [];
@@ -208,7 +215,7 @@ function rewriteSqlTableReferences(payload, rewrites) {
208
215
  return `${keyword}${space}[${escapedTo}]`;
209
216
  }
210
217
  if (!bareIdentifierPattern.test(to)) {
211
- throw new Error(`Unsafe SQL rewrite target for ${from}: ${to}`);
218
+ throw new ClientValidationError(`Unsafe SQL rewrite target for ${from}: ${to}`, "validation_failed");
212
219
  }
213
220
  return `${keyword}${space}${to}`;
214
221
  });
@@ -292,6 +299,96 @@ function shouldRetryRecipeCreateWithOutputProvisioning(error) {
292
299
  // ---------------------------------------------------------------------------
293
300
  // Resource
294
301
  // ---------------------------------------------------------------------------
302
+ /**
303
+ * Pure request construction for POST /recipes/ (no DSS calls). Shared with
304
+ * `recipe create --plan` so the plan equals the request the command sends.
305
+ */
306
+ export function buildRecipeCreateRequest(opts, pk) {
307
+ const { type, payload, outputConnection: rawConnection, joinType: rawJoinType, } = opts;
308
+ const outputFolder = asString(opts.outputFolder);
309
+ // Build inputs/outputs from simple form (inputDatasets + outputDataset) or
310
+ // advanced form (inputs + outputs); both may coexist — simple form wins when
311
+ // the advanced form is absent.
312
+ const inputDatasets = asStringArray(opts.inputDatasets);
313
+ const requestedOutputDataset = asString(opts.outputDataset);
314
+ let inputs = asRecord(opts.inputs);
315
+ let outputs = asRecord(opts.outputs);
316
+ if (!inputs) {
317
+ inputs = {
318
+ main: {
319
+ items: inputDatasets?.map((ref) => ({ ref, deps: [], })) ?? [],
320
+ },
321
+ };
322
+ }
323
+ // Auto-generate name if not provided
324
+ const outputNameForDefaultRecipe = requestedOutputDataset ?? outputFolder;
325
+ const name = opts.name ?? (type && outputNameForDefaultRecipe
326
+ ? `${type}_${outputNameForDefaultRecipe}`
327
+ : undefined);
328
+ const temporaryOutputDataset = outputFolder && !requestedOutputDataset && name
329
+ ? `${name}_folder_output_marker`
330
+ : undefined;
331
+ const outputDataset = requestedOutputDataset ?? temporaryOutputDataset;
332
+ if (!outputs && outputDataset) {
333
+ outputs = {
334
+ main: {
335
+ items: [{ ref: outputDataset, appendMode: false, },],
336
+ },
337
+ };
338
+ }
339
+ if (!type || !name || !outputs) {
340
+ throw new ClientValidationError("type and outputDataset/outputFolder or (name + outputs) are required for create.", "validation_failed");
341
+ }
342
+ const joinCols = typeof opts.joinOn === "string" ? [opts.joinOn,] : asStringArray(opts.joinOn);
343
+ const joinKeys = type === "join" && joinCols?.length
344
+ ? parseJoinKeyPairs(joinCols)
345
+ : undefined;
346
+ const fuzzyOn = opts.fuzzyOn ?? (type === "fuzzyjoin" ? opts.joinOn : undefined);
347
+ const fuzzyCols = typeof fuzzyOn === "string" ? [fuzzyOn,] : asStringArray(fuzzyOn);
348
+ const fuzzyKeys = type === "fuzzyjoin" && fuzzyCols?.length
349
+ ? parseJoinKeyPairs(fuzzyCols, "fuzzyOn")
350
+ : undefined;
351
+ const normalizedJoinType = joinKeys?.length || fuzzyKeys?.length
352
+ ? normalizeJoinType(rawJoinType)
353
+ : undefined;
354
+ const fuzzyDistance = fuzzyKeys?.length ? normalizeFuzzyDistance(opts.fuzzyDistance) : undefined;
355
+ const fuzzyThreshold = opts.fuzzyThreshold ?? 1;
356
+ if (!Number.isFinite(fuzzyThreshold)) {
357
+ throw new ClientValidationError("fuzzyThreshold must be a finite number.", "validation_failed");
358
+ }
359
+ const recipePrototype = {
360
+ type,
361
+ name,
362
+ projectKey: pk,
363
+ ...(type === "fuzzyjoin" ? {} : { inputs, }),
364
+ outputs,
365
+ };
366
+ const creationSettings = {};
367
+ if (payload !== undefined) {
368
+ creationSettings.script = payload;
369
+ }
370
+ if (type === "fuzzyjoin") {
371
+ creationSettings.virtualInputs = recipeInputItems({ inputs, }).map((item) => item.ref);
372
+ }
373
+ return {
374
+ creationSettings,
375
+ fuzzyDistance,
376
+ fuzzyKeys,
377
+ fuzzyThreshold,
378
+ inputDatasets,
379
+ inputs,
380
+ joinKeys,
381
+ name,
382
+ normalizedJoinType,
383
+ outputFolder,
384
+ outputs,
385
+ payload,
386
+ rawConnection,
387
+ recipePrototype,
388
+ temporaryOutputDataset,
389
+ type,
390
+ };
391
+ }
295
392
  export class RecipesResource extends BaseResource {
296
393
  /** List all recipes in a project. */
297
394
  async list(projectKey) {
@@ -433,7 +530,7 @@ export class RecipesResource extends BaseResource {
433
530
  const { recipe, } = await this.get(recipeName, { projectKey: pk, });
434
531
  const outputItems = recipeOutputItems(recipe);
435
532
  if (outputItems.length === 0) {
436
- throw new Error(`Recipe "${recipeName}" has no output items to build.`);
533
+ throw new ClientValidationError(`Recipe "${recipeName}" has no output items to build.`, "validation_failed");
437
534
  }
438
535
  const [datasets, folders,] = await Promise.all([
439
536
  this.client.datasets.list(pk),
@@ -483,7 +580,7 @@ export class RecipesResource extends BaseResource {
483
580
  }
484
581
  const isDataset = datasetNames.has(item.ref);
485
582
  if (isDataset && folderId) {
486
- throw new Error(`Recipe "${recipeName}" output "${item.ref}" matches both a dataset and a managed folder. Add an explicit output type to the recipe definition or build the target directly with --target-type.`);
583
+ throw new ClientValidationError(`Recipe "${recipeName}" output "${item.ref}" matches both a dataset and a managed folder. Add an explicit output type to the recipe definition or build the target directly with --target-type.`, "validation_failed");
487
584
  }
488
585
  if (folderId) {
489
586
  return {
@@ -505,7 +602,7 @@ export class RecipesResource extends BaseResource {
505
602
  partition: opts?.partition,
506
603
  };
507
604
  }
508
- throw new Error(`Recipe "${recipeName}" output "${item.ref}" was not found as a dataset or managed folder in project "${pk}".`);
605
+ throw new ClientValidationError(`Recipe "${recipeName}" output "${item.ref}" was not found as a dataset or managed folder in project "${pk}".`, "validation_failed");
509
606
  });
510
607
  }
511
608
  /** Run a recipe by building its resolved outputs. */
@@ -543,72 +640,7 @@ export class RecipesResource extends BaseResource {
543
640
  async create(opts) {
544
641
  const pk = this.resolveProjectKey(opts.projectKey);
545
642
  const enc = encodeURIComponent(pk);
546
- const { type, payload, outputConnection: rawConnection, joinType: rawJoinType, } = opts;
547
- const outputFolder = asString(opts.outputFolder);
548
- // Build inputs/outputs from simple form (inputDatasets + outputDataset) or
549
- // advanced form (inputs + outputs); both may coexist — simple form wins when
550
- // the advanced form is absent.
551
- const inputDatasets = asStringArray(opts.inputDatasets);
552
- const requestedOutputDataset = asString(opts.outputDataset);
553
- let inputs = asRecord(opts.inputs);
554
- let outputs = asRecord(opts.outputs);
555
- if (!inputs) {
556
- inputs = {
557
- main: {
558
- items: inputDatasets?.map((ref) => ({ ref, deps: [], })) ?? [],
559
- },
560
- };
561
- }
562
- // Auto-generate name if not provided
563
- const outputNameForDefaultRecipe = requestedOutputDataset ?? outputFolder;
564
- const name = opts.name ?? (type && outputNameForDefaultRecipe
565
- ? `${type}_${outputNameForDefaultRecipe}`
566
- : undefined);
567
- const temporaryOutputDataset = outputFolder && !requestedOutputDataset && name
568
- ? `${name}_folder_output_marker`
569
- : undefined;
570
- const outputDataset = requestedOutputDataset ?? temporaryOutputDataset;
571
- if (!outputs && outputDataset) {
572
- outputs = {
573
- main: {
574
- items: [{ ref: outputDataset, appendMode: false, },],
575
- },
576
- };
577
- }
578
- if (!type || !name || !outputs) {
579
- throw new Error("type and outputDataset/outputFolder or (name + outputs) are required for create.");
580
- }
581
- const joinCols = typeof opts.joinOn === "string" ? [opts.joinOn,] : asStringArray(opts.joinOn);
582
- const joinKeys = type === "join" && joinCols?.length
583
- ? parseJoinKeyPairs(joinCols)
584
- : undefined;
585
- const fuzzyOn = opts.fuzzyOn ?? (type === "fuzzyjoin" ? opts.joinOn : undefined);
586
- const fuzzyCols = typeof fuzzyOn === "string" ? [fuzzyOn,] : asStringArray(fuzzyOn);
587
- const fuzzyKeys = type === "fuzzyjoin" && fuzzyCols?.length
588
- ? parseJoinKeyPairs(fuzzyCols, "fuzzyOn")
589
- : undefined;
590
- const normalizedJoinType = joinKeys?.length || fuzzyKeys?.length
591
- ? normalizeJoinType(rawJoinType)
592
- : undefined;
593
- const fuzzyDistance = fuzzyKeys?.length ? normalizeFuzzyDistance(opts.fuzzyDistance) : undefined;
594
- const fuzzyThreshold = opts.fuzzyThreshold ?? 1;
595
- if (!Number.isFinite(fuzzyThreshold)) {
596
- throw new Error("fuzzyThreshold must be a finite number.");
597
- }
598
- const recipePrototype = {
599
- type,
600
- name,
601
- projectKey: pk,
602
- ...(type === "fuzzyjoin" ? {} : { inputs, }),
603
- outputs,
604
- };
605
- const creationSettings = {};
606
- if (payload !== undefined) {
607
- creationSettings.script = payload;
608
- }
609
- if (type === "fuzzyjoin") {
610
- creationSettings.virtualInputs = recipeInputItems({ inputs, }).map((item) => item.ref);
611
- }
643
+ const { creationSettings, fuzzyDistance, fuzzyKeys, fuzzyThreshold, inputDatasets, inputs, joinKeys, name, normalizedJoinType, outputFolder, outputs, rawConnection, recipePrototype, temporaryOutputDataset, type, } = buildRecipeCreateRequest(opts, pk);
612
644
  const createRecipe = () => this.client.post(`/public/api/projects/${enc}/recipes/`, {
613
645
  recipePrototype,
614
646
  creationSettings,
@@ -695,7 +727,7 @@ export class RecipesResource extends BaseResource {
695
727
  const created = await createRecipe();
696
728
  const receivedName = asString(created?.["name"]);
697
729
  if (!receivedName) {
698
- throw new Error('DSS create-recipe response did not include the final recipe name (documented body: {"name": ...}).');
730
+ throw unexpectedResponseError('DSS create-recipe response did not include the final recipe name (documented body: {"name": ...}).');
699
731
  }
700
732
  finalRecipeName = receivedName;
701
733
  }
@@ -708,7 +740,7 @@ export class RecipesResource extends BaseResource {
708
740
  const retryCreated = await createRecipe();
709
741
  const retryName = asString(retryCreated?.["name"]);
710
742
  if (!retryName) {
711
- throw new Error('DSS create-recipe response did not include the final recipe name (documented body: {"name": ...}).', { cause: error, });
743
+ throw unexpectedResponseError('DSS create-recipe response did not include the final recipe name (documented body: {"name": ...}).');
712
744
  }
713
745
  finalRecipeName = retryName;
714
746
  }
@@ -869,11 +901,11 @@ export class RecipesResource extends BaseResource {
869
901
  const current = await this.client.get(`/public/api/projects/${enc}/recipes/${rnEnc}`);
870
902
  const currentRecipe = asRecord(current.recipe);
871
903
  if (!currentRecipe) {
872
- throw new Error(`Recipe "${recipeName}" was not found or returned an empty definition.`);
904
+ throw new ClientValidationError(`Recipe "${recipeName}" was not found or returned an empty definition.`, "validation_failed");
873
905
  }
874
906
  const misplacedRecipeFields = rootRecipeDefinitionFields(data);
875
907
  if (misplacedRecipeFields.length > 0) {
876
- throw new Error(`Recipe fields ${misplacedRecipeFields.join(", ")} must be nested under "recipe". Example: {"recipe":{"outputs":{...},"params":{...}}}`);
908
+ throw new ClientValidationError(`Recipe fields ${misplacedRecipeFields.join(", ")} must be nested under "recipe". Example: {"recipe":{"outputs":{...},"params":{...}}}`, "validation_failed");
877
909
  }
878
910
  const mergedRecipe = deepMerge(currentRecipe, asRecord(data.recipe) ?? {});
879
911
  const merged = { ...current, ...data, recipe: mergedRecipe, };
@@ -895,7 +927,7 @@ export class RecipesResource extends BaseResource {
895
927
  if (opts.outputDataset !== undefined) {
896
928
  const outputs = recipeOutputItems(source.recipe).filter((item) => item.type !== "MANAGED_FOLDER");
897
929
  if (outputs.length !== 1 && Object.keys(outputRewrites).length === 0) {
898
- throw new Error(`Recipe "${sourceName}" has ${outputs.length} dataset outputs; pass explicit outputRewrites instead of outputDataset.`);
930
+ throw new ClientValidationError(`Recipe "${sourceName}" has ${outputs.length} dataset outputs; pass explicit outputRewrites instead of outputDataset.`, "validation_failed");
899
931
  }
900
932
  if (outputs[0])
901
933
  outputRewrites[outputs[0].ref] = opts.outputDataset;
@@ -910,7 +942,7 @@ export class RecipesResource extends BaseResource {
910
942
  if (opts.copyOutputSettings === true
911
943
  && Object.keys(outputRewrites).length > 1
912
944
  && (opts.outputPath !== undefined || opts.metastoreTableName !== undefined)) {
913
- throw new Error("Cannot reuse --path or --metastore-table for multiple cloned output datasets; pass per-output settings in a separate step.");
945
+ throw new ClientValidationError("Cannot reuse --path or --metastore-table for multiple cloned output datasets; pass per-output settings in a separate step.", "validation_failed");
914
946
  }
915
947
  const payloadTextRewrites = {};
916
948
  if (opts.payloadTextRewrites)
@@ -956,7 +988,7 @@ export class RecipesResource extends BaseResource {
956
988
  projectKey: opts?.projectKey,
957
989
  });
958
990
  if (!result.payload) {
959
- throw new Error(`Recipe "${recipeName}" has no code payload.`);
991
+ throw new ClientValidationError(`Recipe "${recipeName}" has no code payload.`, "validation_failed");
960
992
  }
961
993
  const safeRecipeName = sanitizeFileName(recipeName, "recipe");
962
994
  const filePath = opts?.outputPath ?? resolve(process.cwd(), `${safeRecipeName}${inferRecipeCodeExtension(result.recipe.type)}`);
@@ -970,7 +1002,7 @@ export class RecipesResource extends BaseResource {
970
1002
  projectKey: opts?.projectKey,
971
1003
  });
972
1004
  if (!result.payload) {
973
- throw new Error(`Recipe "${recipeName}" has no code payload.`);
1005
+ throw new ClientValidationError(`Recipe "${recipeName}" has no code payload.`, "validation_failed");
974
1006
  }
975
1007
  return result.payload;
976
1008
  }
@@ -1,4 +1,4 @@
1
- import { ClientValidationError, DataikuError, } from "../errors.js";
1
+ import { ClientValidationError, DataikuError, nonJsonResponseBody, SQL_QUERY_FAILED_MARKER, } from "../errors.js";
2
2
  import { BaseResource, } from "./base.js";
3
3
  const UNSUPPORTED_SQL_DATASET_CONNECTION_DETAIL = "neither of sql nor hdfs type";
4
4
  /**
@@ -159,7 +159,12 @@ export class SqlResource extends BaseResource {
159
159
  if (truncated) {
160
160
  throw new DataikuError(0, "Response Too Large", `SQL stream results exceeded the ${maxBytes}-byte response limit and were not parsed.`);
161
161
  }
162
- return JSON.parse(text);
162
+ try {
163
+ return JSON.parse(text);
164
+ }
165
+ catch {
166
+ throw new DataikuError(200, "Invalid JSON response", nonJsonResponseBody(text));
167
+ }
163
168
  }
164
169
  /**
165
170
  * Verify that a query finished successfully server-side.
@@ -173,7 +178,7 @@ export class SqlResource extends BaseResource {
173
178
  throw new DataikuError(0, "Response Too Large", `SQL finish-streaming response exceeded the ${maxBytes}-byte response limit.`);
174
179
  }
175
180
  if (text.length > 0) {
176
- throw new Error(`SQL query ${queryId} failed: ${text}`);
181
+ throw new DataikuError(200, "SQL Query Failed", `${SQL_QUERY_FAILED_MARKER} (${queryId}): ${text}`);
177
182
  }
178
183
  }
179
184
  async executeQuery(opts) {
@@ -1,4 +1,5 @@
1
1
  import { UsageError, } from "../cli/usage.js";
2
+ import { ClientValidationError, } from "../errors.js";
2
3
  import { BaseResource, } from "./base.js";
3
4
  function isRecord(value) {
4
5
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -36,7 +37,7 @@ export class StatisticsResource extends BaseResource {
36
37
  async runWorksheet(datasetName, worksheetId, projectKey) {
37
38
  const worksheet = await this.getWorksheet(datasetName, worksheetId, projectKey);
38
39
  if (!isRecord(worksheet.rootCard)) {
39
- throw new Error("statistics.runWorksheet requires worksheet.rootCard");
40
+ throw new ClientValidationError("statistics.runWorksheet requires worksheet.rootCard", "validation_failed");
40
41
  }
41
42
  return this.runCard(datasetName, worksheetId, worksheet.rootCard, projectKey);
42
43
  }
@@ -1,3 +1,4 @@
1
+ import { ClientValidationError, } from "../errors.js";
1
2
  import { ProjectVariablesSchema, } from "../schemas.js";
2
3
  import { BaseResource, } from "./base.js";
3
4
  export class VariablesResource extends BaseResource {
@@ -17,7 +18,7 @@ export class VariablesResource extends BaseResource {
17
18
  return replaced;
18
19
  }
19
20
  if (opts.standard === undefined && opts.local === undefined) {
20
- throw new Error("At least one of standard or local must be provided");
21
+ throw new ClientValidationError("At least one of standard or local must be provided", "validation_failed");
21
22
  }
22
23
  const existing = await this.get(opts.projectKey);
23
24
  const merged = {