dataiku-sdk 3.4.0 → 3.6.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 (131) hide show
  1. package/README.md +4 -4
  2. package/bin/dss.js +28 -2
  3. package/dist/build-metadata.json +1 -1
  4. package/dist/src/cli/coerce.d.ts +0 -1
  5. package/dist/src/cli/coerce.js +0 -5
  6. package/dist/src/cli/command-syntax.json +14524 -0
  7. package/dist/src/cli/commands/analysis.js +6 -9
  8. package/dist/src/cli/commands/api-deployer.js +15 -33
  9. package/dist/src/cli/commands/api-service.js +17 -27
  10. package/dist/src/cli/commands/app.js +25 -41
  11. package/dist/src/cli/commands/bundle.js +15 -24
  12. package/dist/src/cli/commands/business-app.js +10 -17
  13. package/dist/src/cli/commands/code-env.js +23 -36
  14. package/dist/src/cli/commands/code.js +4 -4
  15. package/dist/src/cli/commands/connection.js +12 -22
  16. package/dist/src/cli/commands/continuous-activity.js +6 -9
  17. package/dist/src/cli/commands/dashboard.js +8 -13
  18. package/dist/src/cli/commands/data-collection.js +9 -16
  19. package/dist/src/cli/commands/data-quality.js +14 -26
  20. package/dist/src/cli/commands/dataset.js +29 -54
  21. package/dist/src/cli/commands/discussion.js +7 -10
  22. package/dist/src/cli/commands/doctor.js +3 -3
  23. package/dist/src/cli/commands/flow-zone.js +8 -17
  24. package/dist/src/cli/commands/folder.js +12 -20
  25. package/dist/src/cli/commands/future.js +7 -10
  26. package/dist/src/cli/commands/group.js +7 -11
  27. package/dist/src/cli/commands/insight.js +7 -11
  28. package/dist/src/cli/commands/job.js +17 -26
  29. package/dist/src/cli/commands/knowledge-bank.js +5 -6
  30. package/dist/src/cli/commands/llm.js +5 -8
  31. package/dist/src/cli/commands/macro.js +9 -16
  32. package/dist/src/cli/commands/meaning.js +7 -11
  33. package/dist/src/cli/commands/metrics.js +7 -10
  34. package/dist/src/cli/commands/ml-task.js +12 -20
  35. package/dist/src/cli/commands/model-evaluation-store.js +7 -11
  36. package/dist/src/cli/commands/notebook.js +15 -27
  37. package/dist/src/cli/commands/plugin.js +35 -68
  38. package/dist/src/cli/commands/project-deployer.js +10 -22
  39. package/dist/src/cli/commands/project-folder.js +10 -17
  40. package/dist/src/cli/commands/project-git.js +38 -70
  41. package/dist/src/cli/commands/project-library.js +15 -23
  42. package/dist/src/cli/commands/project.js +9 -26
  43. package/dist/src/cli/commands/recipe.d.ts +16 -0
  44. package/dist/src/cli/commands/recipe.js +83 -94
  45. package/dist/src/cli/commands/saved-model.js +23 -39
  46. package/dist/src/cli/commands/scenario.js +20 -34
  47. package/dist/src/cli/commands/sql.js +4 -4
  48. package/dist/src/cli/commands/statistics.js +11 -18
  49. package/dist/src/cli/commands/streaming-endpoint.js +7 -11
  50. package/dist/src/cli/commands/user.js +11 -22
  51. package/dist/src/cli/commands/variable.js +3 -4
  52. package/dist/src/cli/commands/webapp.js +10 -16
  53. package/dist/src/cli/commands/wiki.js +9 -13
  54. package/dist/src/cli/commands/workspace.js +8 -14
  55. package/dist/src/cli/contract.d.ts +5 -15
  56. package/dist/src/cli/contract.js +157 -254
  57. package/dist/src/cli/doctor.js +1 -0
  58. package/dist/src/cli/env.d.ts +2 -0
  59. package/dist/src/cli/env.js +16 -1
  60. package/dist/src/cli/flags.d.ts +1 -0
  61. package/dist/src/cli/flags.js +4 -9
  62. package/dist/src/cli/helpers/app-permissions.js +7 -5
  63. package/dist/src/cli/helpers/flow-zone.js +8 -6
  64. package/dist/src/cli/helpers/recipe.js +2 -1
  65. package/dist/src/cli/syntax.d.ts +83 -0
  66. package/dist/src/cli/syntax.js +217 -0
  67. package/dist/src/cli/types.d.ts +3 -0
  68. package/dist/src/cli/usage.d.ts +1 -1
  69. package/dist/src/cli/usage.js +4 -2
  70. package/dist/src/cli/version.d.ts +0 -2
  71. package/dist/src/cli/version.js +6 -7
  72. package/dist/src/cli.js +132 -274
  73. package/dist/src/client.d.ts +2 -1
  74. package/dist/src/client.js +32 -17
  75. package/dist/src/errors.d.ts +12 -1
  76. package/dist/src/errors.js +28 -3
  77. package/dist/src/index.d.ts +1 -3
  78. package/dist/src/index.js +2 -2
  79. package/dist/src/resources/api-services.js +2 -3
  80. package/dist/src/resources/applications.js +2 -4
  81. package/dist/src/resources/base.d.ts +2 -0
  82. package/dist/src/resources/base.js +14 -2
  83. package/dist/src/resources/code-envs.js +3 -5
  84. package/dist/src/resources/data-quality.js +14 -13
  85. package/dist/src/resources/datasets.d.ts +13 -0
  86. package/dist/src/resources/datasets.js +5 -4
  87. package/dist/src/resources/flow-zones.js +4 -2
  88. package/dist/src/resources/folders.js +4 -2
  89. package/dist/src/resources/futures.js +1 -3
  90. package/dist/src/resources/insights.js +3 -2
  91. package/dist/src/resources/jobs.js +4 -6
  92. package/dist/src/resources/macros.d.ts +1 -1
  93. package/dist/src/resources/macros.js +3 -7
  94. package/dist/src/resources/notebooks.js +2 -3
  95. package/dist/src/resources/project-git.js +9 -11
  96. package/dist/src/resources/project-library.d.ts +0 -2
  97. package/dist/src/resources/project-library.js +2 -7
  98. package/dist/src/resources/projects.js +4 -3
  99. package/dist/src/resources/recipes.d.ts +28 -0
  100. package/dist/src/resources/recipes.js +117 -89
  101. package/dist/src/resources/saved-models.js +1 -13
  102. package/dist/src/resources/scenarios.js +1 -3
  103. package/dist/src/resources/sql.js +9 -8
  104. package/dist/src/resources/statistics.js +3 -4
  105. package/dist/src/resources/variables.js +2 -1
  106. package/dist/src/resources/webapps.js +2 -3
  107. package/dist/src/schemas.d.ts +1 -2
  108. package/dist/src/schemas.js +1 -1
  109. package/dist/src/skill.js +6 -7
  110. package/dist/src/utils/cleanup-ledger.js +9 -8
  111. package/dist/src/utils/deep-merge.js +2 -4
  112. package/dist/src/utils/flow-map.js +1 -6
  113. package/dist/src/utils/polling.d.ts +3 -0
  114. package/dist/src/utils/polling.js +3 -0
  115. package/dist/src/utils/project-archive.js +10 -14
  116. package/dist/src/utils/project-incarnation.js +3 -7
  117. package/dist/src/utils/records.d.ts +4 -0
  118. package/dist/src/utils/records.js +8 -0
  119. package/dist/src/utils/stable-hash.d.ts +3 -1
  120. package/dist/src/utils/stable-hash.js +2 -0
  121. package/package.json +10 -14
  122. package/plugin.json +1 -1
  123. package/skills/dataiku-dss/SKILL.md +3 -4
  124. package/skills/dataiku-dss/references/authentication.md +3 -3
  125. package/skills/dataiku-dss/references/coding.md +1 -1
  126. package/skills/dataiku-dss/references/discovery.md +1 -1
  127. package/skills/dataiku-dss/references/mutations.md +0 -1
  128. package/dist/src/utils/pagination.d.ts +0 -8
  129. package/dist/src/utils/pagination.js +0 -23
  130. package/packages/types/dist/index.d.ts +0 -1062
  131. package/packages/types/dist/index.js +0 -871
@@ -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,4 +1,5 @@
1
1
  import type { MacroDefinition, MacroRunStart, MacroState, MacroSummary, MacroWaitResult } from "../schemas.js";
2
+ import { isRecord } from "../utils/records.js";
2
3
  import { BaseResource } from "./base.js";
3
4
  export interface MacroRunOptions {
4
5
  /**
@@ -42,7 +43,6 @@ export interface MacroResultOptions {
42
43
  * inclusive range [1, 2 GiB], rejecting non-integers before any DSS request.
43
44
  */
44
45
  export declare function macroResultMaxBytes(maxBytes: number | undefined): number;
45
- declare function isRecord(value: unknown): value is Record<string, unknown>;
46
46
  /**
47
47
  * Run result of {@link MacrosResource.run} when the caller opted into
48
48
  * `wait: false` (the default): a run id for abort/state/result tracking.
@@ -1,9 +1,8 @@
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
+ import { isRecord, } from "../utils/records.js";
4
5
  import { BaseResource, requireNonEmpty, } from "./base.js";
5
- const DEFAULT_POLL_INTERVAL_MS = 2_000;
6
- const DEFAULT_TIMEOUT_MS = 120_000;
7
6
  /** Hard floor for the result download cap; keeps a caller typo from hanging the read. */
8
7
  const MIN_RESULT_MAX_BYTES = 1;
9
8
  const MAX_RESULT_MAX_BYTES = 2 * 1024 * 1024 * 1024;
@@ -18,9 +17,6 @@ export function macroResultMaxBytes(maxBytes) {
18
17
  }
19
18
  return Math.min(raw, MAX_RESULT_MAX_BYTES);
20
19
  }
21
- function isRecord(value) {
22
- return typeof value === "object" && value !== null && !Array.isArray(value);
23
- }
24
20
  /**
25
21
  * Build the run POST body from params/adminParams. DSS macro runs accept
26
22
  * `params` keyed by the macro's declared param names and `adminParams` keyed
@@ -121,7 +117,7 @@ export class MacrosResource extends BaseResource {
121
117
  const maxBytes = macroResultMaxBytes(opts?.maxBytes);
122
118
  const raw = await this.client.getTextLimited(`${this.base(runnableType, opts?.projectKey)}/result/${encodeURIComponent(runId)}`, maxBytes);
123
119
  if (raw.truncated) {
124
- throw new Error(`Macro result exceeded ${String(maxBytes)} bytes; retry with a larger maxBytes.`);
120
+ throw new ClientValidationError(`Macro result exceeded ${String(maxBytes)} bytes; retry with a larger maxBytes.`, "validation_failed");
125
121
  }
126
122
  const text = raw.text;
127
123
  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,8 +1,9 @@
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
+ import { asRecord, } from "../utils/records.js";
6
7
  import { sanitizeFileName, } from "../utils/sanitize.js";
7
8
  import { BaseResource, } from "./base.js";
8
9
  // ---------------------------------------------------------------------------
@@ -17,15 +18,16 @@ function asStringArray(value) {
17
18
  const out = value.filter((v) => typeof v === "string" && v.length > 0);
18
19
  return out.length > 0 ? out : undefined;
19
20
  }
20
- function asRecord(value) {
21
- if (!value || typeof value !== "object" || Array.isArray(value))
22
- return undefined;
23
- return value;
24
- }
25
21
  function parseRecipePayload(payload) {
26
22
  if (!payload)
27
23
  return {};
28
- const parsed = JSON.parse(payload);
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(payload);
27
+ }
28
+ catch {
29
+ throw new DataikuError(200, "Invalid JSON response", nonJsonResponseBody(payload));
30
+ }
29
31
  return asRecord(parsed) ?? {};
30
32
  }
31
33
  function parseJoinKeyPairs(values, optionName = "joinOn") {
@@ -41,8 +43,9 @@ function parseJoinKeyPairs(values, optionName = "joinOn") {
41
43
  }
42
44
  const left = token.slice(0, eq).trim();
43
45
  const right = token.slice(eq + 1).trim();
44
- if (!left || !right)
45
- throw new Error(`${optionName} values must use COL or LEFT=RIGHT.`);
46
+ if (!left || !right) {
47
+ throw new ClientValidationError(`${optionName} values must use COL or LEFT=RIGHT.`, "validation_failed");
48
+ }
46
49
  pairs.push({ left, right, });
47
50
  }
48
51
  return pairs;
@@ -57,7 +60,7 @@ function normalizeFuzzyDistance(value) {
57
60
  || normalized === "EUCLIDEAN") {
58
61
  return normalized;
59
62
  }
60
- throw new Error("fuzzyDistance must be one of DAMERAU_LEVENSHTEIN, HAMMING, JACCARD, COSINE, or EUCLIDEAN.");
63
+ throw new ClientValidationError("fuzzyDistance must be one of DAMERAU_LEVENSHTEIN, HAMMING, JACCARD, COSINE, or EUCLIDEAN.", "validation_failed");
61
64
  }
62
65
  function normalizeJoinType(value) {
63
66
  const normalized = typeof value === "string" && value.trim().length > 0
@@ -67,7 +70,7 @@ function normalizeJoinType(value) {
67
70
  || normalized === "FULL") {
68
71
  return normalized;
69
72
  }
70
- throw new Error("joinType must be one of LEFT, INNER, RIGHT, or FULL.");
73
+ throw new ClientValidationError("joinType must be one of LEFT, INNER, RIGHT, or FULL.", "validation_failed");
71
74
  }
72
75
  function recipeVirtualInputs(payload, inputCount) {
73
76
  const current = Array.isArray(payload.virtualInputs) ? payload.virtualInputs : [];
@@ -208,7 +211,7 @@ function rewriteSqlTableReferences(payload, rewrites) {
208
211
  return `${keyword}${space}[${escapedTo}]`;
209
212
  }
210
213
  if (!bareIdentifierPattern.test(to)) {
211
- throw new Error(`Unsafe SQL rewrite target for ${from}: ${to}`);
214
+ throw new ClientValidationError(`Unsafe SQL rewrite target for ${from}: ${to}`, "validation_failed");
212
215
  }
213
216
  return `${keyword}${space}${to}`;
214
217
  });
@@ -292,6 +295,96 @@ function shouldRetryRecipeCreateWithOutputProvisioning(error) {
292
295
  // ---------------------------------------------------------------------------
293
296
  // Resource
294
297
  // ---------------------------------------------------------------------------
298
+ /**
299
+ * Pure request construction for POST /recipes/ (no DSS calls). Shared with
300
+ * `recipe create --plan` so the plan equals the request the command sends.
301
+ */
302
+ export function buildRecipeCreateRequest(opts, pk) {
303
+ const { type, payload, outputConnection: rawConnection, joinType: rawJoinType, } = opts;
304
+ const outputFolder = asString(opts.outputFolder);
305
+ // Build inputs/outputs from simple form (inputDatasets + outputDataset) or
306
+ // advanced form (inputs + outputs); both may coexist — simple form wins when
307
+ // the advanced form is absent.
308
+ const inputDatasets = asStringArray(opts.inputDatasets);
309
+ const requestedOutputDataset = asString(opts.outputDataset);
310
+ let inputs = asRecord(opts.inputs);
311
+ let outputs = asRecord(opts.outputs);
312
+ if (!inputs) {
313
+ inputs = {
314
+ main: {
315
+ items: inputDatasets?.map((ref) => ({ ref, deps: [], })) ?? [],
316
+ },
317
+ };
318
+ }
319
+ // Auto-generate name if not provided
320
+ const outputNameForDefaultRecipe = requestedOutputDataset ?? outputFolder;
321
+ const name = opts.name ?? (type && outputNameForDefaultRecipe
322
+ ? `${type}_${outputNameForDefaultRecipe}`
323
+ : undefined);
324
+ const temporaryOutputDataset = outputFolder && !requestedOutputDataset && name
325
+ ? `${name}_folder_output_marker`
326
+ : undefined;
327
+ const outputDataset = requestedOutputDataset ?? temporaryOutputDataset;
328
+ if (!outputs && outputDataset) {
329
+ outputs = {
330
+ main: {
331
+ items: [{ ref: outputDataset, appendMode: false, },],
332
+ },
333
+ };
334
+ }
335
+ if (!type || !name || !outputs) {
336
+ throw new ClientValidationError("type and outputDataset/outputFolder or (name + outputs) are required for create.", "validation_failed");
337
+ }
338
+ const joinCols = typeof opts.joinOn === "string" ? [opts.joinOn,] : asStringArray(opts.joinOn);
339
+ const joinKeys = type === "join" && joinCols?.length
340
+ ? parseJoinKeyPairs(joinCols)
341
+ : undefined;
342
+ const fuzzyOn = opts.fuzzyOn ?? (type === "fuzzyjoin" ? opts.joinOn : undefined);
343
+ const fuzzyCols = typeof fuzzyOn === "string" ? [fuzzyOn,] : asStringArray(fuzzyOn);
344
+ const fuzzyKeys = type === "fuzzyjoin" && fuzzyCols?.length
345
+ ? parseJoinKeyPairs(fuzzyCols, "fuzzyOn")
346
+ : undefined;
347
+ const normalizedJoinType = joinKeys?.length || fuzzyKeys?.length
348
+ ? normalizeJoinType(rawJoinType)
349
+ : undefined;
350
+ const fuzzyDistance = fuzzyKeys?.length ? normalizeFuzzyDistance(opts.fuzzyDistance) : undefined;
351
+ const fuzzyThreshold = opts.fuzzyThreshold ?? 1;
352
+ if (!Number.isFinite(fuzzyThreshold)) {
353
+ throw new ClientValidationError("fuzzyThreshold must be a finite number.", "validation_failed");
354
+ }
355
+ const recipePrototype = {
356
+ type,
357
+ name,
358
+ projectKey: pk,
359
+ ...(type === "fuzzyjoin" ? {} : { inputs, }),
360
+ outputs,
361
+ };
362
+ const creationSettings = {};
363
+ if (payload !== undefined) {
364
+ creationSettings.script = payload;
365
+ }
366
+ if (type === "fuzzyjoin") {
367
+ creationSettings.virtualInputs = recipeInputItems({ inputs, }).map((item) => item.ref);
368
+ }
369
+ return {
370
+ creationSettings,
371
+ fuzzyDistance,
372
+ fuzzyKeys,
373
+ fuzzyThreshold,
374
+ inputDatasets,
375
+ inputs,
376
+ joinKeys,
377
+ name,
378
+ normalizedJoinType,
379
+ outputFolder,
380
+ outputs,
381
+ payload,
382
+ rawConnection,
383
+ recipePrototype,
384
+ temporaryOutputDataset,
385
+ type,
386
+ };
387
+ }
295
388
  export class RecipesResource extends BaseResource {
296
389
  /** List all recipes in a project. */
297
390
  async list(projectKey) {
@@ -433,7 +526,7 @@ export class RecipesResource extends BaseResource {
433
526
  const { recipe, } = await this.get(recipeName, { projectKey: pk, });
434
527
  const outputItems = recipeOutputItems(recipe);
435
528
  if (outputItems.length === 0) {
436
- throw new Error(`Recipe "${recipeName}" has no output items to build.`);
529
+ throw new ClientValidationError(`Recipe "${recipeName}" has no output items to build.`, "validation_failed");
437
530
  }
438
531
  const [datasets, folders,] = await Promise.all([
439
532
  this.client.datasets.list(pk),
@@ -483,7 +576,7 @@ export class RecipesResource extends BaseResource {
483
576
  }
484
577
  const isDataset = datasetNames.has(item.ref);
485
578
  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.`);
579
+ 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
580
  }
488
581
  if (folderId) {
489
582
  return {
@@ -505,7 +598,7 @@ export class RecipesResource extends BaseResource {
505
598
  partition: opts?.partition,
506
599
  };
507
600
  }
508
- throw new Error(`Recipe "${recipeName}" output "${item.ref}" was not found as a dataset or managed folder in project "${pk}".`);
601
+ throw new ClientValidationError(`Recipe "${recipeName}" output "${item.ref}" was not found as a dataset or managed folder in project "${pk}".`, "validation_failed");
509
602
  });
510
603
  }
511
604
  /** Run a recipe by building its resolved outputs. */
@@ -543,72 +636,7 @@ export class RecipesResource extends BaseResource {
543
636
  async create(opts) {
544
637
  const pk = this.resolveProjectKey(opts.projectKey);
545
638
  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
- }
639
+ const { creationSettings, fuzzyDistance, fuzzyKeys, fuzzyThreshold, inputDatasets, inputs, joinKeys, name, normalizedJoinType, outputFolder, outputs, rawConnection, recipePrototype, temporaryOutputDataset, type, } = buildRecipeCreateRequest(opts, pk);
612
640
  const createRecipe = () => this.client.post(`/public/api/projects/${enc}/recipes/`, {
613
641
  recipePrototype,
614
642
  creationSettings,
@@ -695,7 +723,7 @@ export class RecipesResource extends BaseResource {
695
723
  const created = await createRecipe();
696
724
  const receivedName = asString(created?.["name"]);
697
725
  if (!receivedName) {
698
- throw new Error('DSS create-recipe response did not include the final recipe name (documented body: {"name": ...}).');
726
+ throw unexpectedResponseError('DSS create-recipe response did not include the final recipe name (documented body: {"name": ...}).');
699
727
  }
700
728
  finalRecipeName = receivedName;
701
729
  }
@@ -708,7 +736,7 @@ export class RecipesResource extends BaseResource {
708
736
  const retryCreated = await createRecipe();
709
737
  const retryName = asString(retryCreated?.["name"]);
710
738
  if (!retryName) {
711
- throw new Error('DSS create-recipe response did not include the final recipe name (documented body: {"name": ...}).', { cause: error, });
739
+ throw unexpectedResponseError('DSS create-recipe response did not include the final recipe name (documented body: {"name": ...}).');
712
740
  }
713
741
  finalRecipeName = retryName;
714
742
  }
@@ -869,11 +897,11 @@ export class RecipesResource extends BaseResource {
869
897
  const current = await this.client.get(`/public/api/projects/${enc}/recipes/${rnEnc}`);
870
898
  const currentRecipe = asRecord(current.recipe);
871
899
  if (!currentRecipe) {
872
- throw new Error(`Recipe "${recipeName}" was not found or returned an empty definition.`);
900
+ throw new ClientValidationError(`Recipe "${recipeName}" was not found or returned an empty definition.`, "validation_failed");
873
901
  }
874
902
  const misplacedRecipeFields = rootRecipeDefinitionFields(data);
875
903
  if (misplacedRecipeFields.length > 0) {
876
- throw new Error(`Recipe fields ${misplacedRecipeFields.join(", ")} must be nested under "recipe". Example: {"recipe":{"outputs":{...},"params":{...}}}`);
904
+ throw new ClientValidationError(`Recipe fields ${misplacedRecipeFields.join(", ")} must be nested under "recipe". Example: {"recipe":{"outputs":{...},"params":{...}}}`, "validation_failed");
877
905
  }
878
906
  const mergedRecipe = deepMerge(currentRecipe, asRecord(data.recipe) ?? {});
879
907
  const merged = { ...current, ...data, recipe: mergedRecipe, };
@@ -895,7 +923,7 @@ export class RecipesResource extends BaseResource {
895
923
  if (opts.outputDataset !== undefined) {
896
924
  const outputs = recipeOutputItems(source.recipe).filter((item) => item.type !== "MANAGED_FOLDER");
897
925
  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.`);
926
+ throw new ClientValidationError(`Recipe "${sourceName}" has ${outputs.length} dataset outputs; pass explicit outputRewrites instead of outputDataset.`, "validation_failed");
899
927
  }
900
928
  if (outputs[0])
901
929
  outputRewrites[outputs[0].ref] = opts.outputDataset;
@@ -910,7 +938,7 @@ export class RecipesResource extends BaseResource {
910
938
  if (opts.copyOutputSettings === true
911
939
  && Object.keys(outputRewrites).length > 1
912
940
  && (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.");
941
+ 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
942
  }
915
943
  const payloadTextRewrites = {};
916
944
  if (opts.payloadTextRewrites)
@@ -956,7 +984,7 @@ export class RecipesResource extends BaseResource {
956
984
  projectKey: opts?.projectKey,
957
985
  });
958
986
  if (!result.payload) {
959
- throw new Error(`Recipe "${recipeName}" has no code payload.`);
987
+ throw new ClientValidationError(`Recipe "${recipeName}" has no code payload.`, "validation_failed");
960
988
  }
961
989
  const safeRecipeName = sanitizeFileName(recipeName, "recipe");
962
990
  const filePath = opts?.outputPath ?? resolve(process.cwd(), `${safeRecipeName}${inferRecipeCodeExtension(result.recipe.type)}`);
@@ -970,7 +998,7 @@ export class RecipesResource extends BaseResource {
970
998
  projectKey: opts?.projectKey,
971
999
  });
972
1000
  if (!result.payload) {
973
- throw new Error(`Recipe "${recipeName}" has no code payload.`);
1001
+ throw new ClientValidationError(`Recipe "${recipeName}" has no code payload.`, "validation_failed");
974
1002
  }
975
1003
  return result.payload;
976
1004
  }
@@ -1,18 +1,6 @@
1
1
  import { basename, } from "node:path";
2
2
  import { ClientValidationError, } from "../errors.js";
3
- import { BaseResource, requireNonEmpty, } from "./base.js";
4
- function requireObject(value, name) {
5
- if (!value || typeof value !== "object" || Array.isArray(value)) {
6
- throw new ClientValidationError(`${name} must be a JSON object.`, "validation_failed");
7
- }
8
- return value;
9
- }
10
- function requireNonEmptyArray(value, name) {
11
- if (!Array.isArray(value) || value.length === 0) {
12
- throw new ClientValidationError(`${name} must be a non-empty array.`, "validation_failed");
13
- }
14
- return value;
15
- }
3
+ import { BaseResource, requireNonEmpty, requireNonEmptyArray, requireObject, } from "./base.js";
16
4
  function boolQuery(value) {
17
5
  return value === undefined ? "true" : String(value);
18
6
  }
@@ -3,6 +3,7 @@ import { ClientValidationError, DataikuError, } from "../errors.js";
3
3
  import { ScenarioDetailsSchema, ScenarioStatusSchema, ScenarioSummaryArraySchema, } from "../schemas.js";
4
4
  import { deepMerge, } from "../utils/deep-merge.js";
5
5
  import { computeNextPollDelayMs, isRequestDeadlineError, } from "../utils/polling.js";
6
+ import { isRecord, } from "../utils/records.js";
6
7
  import { BaseResource, requireArrayResponse, requireNonEmpty, } from "./base.js";
7
8
  export const SCENARIO_CANONICAL_EDITABLE_FIELDS = [
8
9
  "params.steps",
@@ -147,9 +148,6 @@ function extractCodeRunOutput(log) {
147
148
  body.pop();
148
149
  return body.join("\n");
149
150
  }
150
- function isRecord(value) {
151
- return typeof value === "object" && value !== null && !Array.isArray(value);
152
- }
153
151
  function scenarioStepWarningSummary(stepResult) {
154
152
  const container = stepResult?.warnings;
155
153
  if (!isRecord(container))