dataiku-sdk 3.3.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 (126) hide show
  1. package/README.md +6 -4
  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 +5 -5
  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 +28 -51
  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 +17 -25
  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 +11 -18
  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 +39 -71
  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 +21 -35
  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 +179 -461
  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/output-schemas.d.ts +24 -0
  61. package/dist/src/cli/output-schemas.js +233 -0
  62. package/dist/src/cli/syntax.d.ts +83 -0
  63. package/dist/src/cli/syntax.js +217 -0
  64. package/dist/src/cli/types.d.ts +3 -0
  65. package/dist/src/cli/usage.d.ts +1 -1
  66. package/dist/src/cli/usage.js +4 -2
  67. package/dist/src/cli/version.js +2 -0
  68. package/dist/src/cli.js +37 -64
  69. package/dist/src/client.d.ts +10 -1
  70. package/dist/src/client.js +27 -11
  71. package/dist/src/errors.d.ts +12 -1
  72. package/dist/src/errors.js +34 -7
  73. package/dist/src/generated/action-output-schemas.json +1735 -0
  74. package/dist/src/index.d.ts +1 -3
  75. package/dist/src/index.js +2 -2
  76. package/dist/src/resources/api-services.js +2 -3
  77. package/dist/src/resources/applications.js +2 -4
  78. package/dist/src/resources/code-envs.js +2 -2
  79. package/dist/src/resources/data-quality.js +4 -3
  80. package/dist/src/resources/datasets.d.ts +13 -0
  81. package/dist/src/resources/datasets.js +42 -8
  82. package/dist/src/resources/flow-zones.d.ts +6 -2
  83. package/dist/src/resources/flow-zones.js +10 -5
  84. package/dist/src/resources/folders.js +4 -2
  85. package/dist/src/resources/futures.d.ts +2 -0
  86. package/dist/src/resources/futures.js +39 -27
  87. package/dist/src/resources/insights.js +3 -2
  88. package/dist/src/resources/jobs.js +41 -31
  89. package/dist/src/resources/macros.js +2 -4
  90. package/dist/src/resources/ml-tasks.js +28 -19
  91. package/dist/src/resources/notebooks.js +2 -3
  92. package/dist/src/resources/plugins.d.ts +0 -2
  93. package/dist/src/resources/plugins.js +0 -3
  94. package/dist/src/resources/project-git.d.ts +6 -12
  95. package/dist/src/resources/project-git.js +31 -58
  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.d.ts +4 -2
  99. package/dist/src/resources/projects.js +37 -36
  100. package/dist/src/resources/recipes.d.ts +28 -0
  101. package/dist/src/resources/recipes.js +116 -84
  102. package/dist/src/resources/sql.js +8 -3
  103. package/dist/src/resources/statistics.js +2 -1
  104. package/dist/src/resources/variables.js +2 -1
  105. package/dist/src/resources/webapps.js +2 -3
  106. package/dist/src/schemas.d.ts +1 -2
  107. package/dist/src/schemas.js +1 -1
  108. package/dist/src/skill.js +6 -7
  109. package/dist/src/utils/cleanup-ledger.js +9 -8
  110. package/dist/src/utils/git-reference.d.ts +12 -0
  111. package/dist/src/utils/git-reference.js +38 -0
  112. package/dist/src/utils/polling.d.ts +3 -0
  113. package/dist/src/utils/polling.js +3 -0
  114. package/dist/src/utils/project-archive.js +2 -2
  115. package/dist/src/utils/stable-hash.d.ts +3 -1
  116. package/dist/src/utils/stable-hash.js +2 -0
  117. package/package.json +8 -8
  118. package/plugin.json +1 -1
  119. package/skills/dataiku-dss/SKILL.md +3 -2
  120. package/skills/dataiku-dss/references/app-releases.md +2 -3
  121. package/skills/dataiku-dss/references/authentication.md +7 -7
  122. package/skills/dataiku-dss/references/coding.md +1 -1
  123. package/skills/dataiku-dss/references/mutations.md +1 -1
  124. package/skills/dataiku-dss/references/troubleshooting.md +2 -2
  125. package/dist/src/utils/pagination.d.ts +0 -8
  126. package/dist/src/utils/pagination.js +0 -23
@@ -1,6 +1,6 @@
1
1
  import type { StableErrorCode } from "../errors.js";
2
2
  export declare const RESOURCE_NAMES: string[];
3
- export declare function inferRequiresProject(resource: string, action: string, usage: string): boolean;
3
+ export declare function inferRequiresProject(resource: string, action: string): boolean;
4
4
  export declare class UsageError extends Error {
5
5
  readonly code: StableErrorCode;
6
6
  readonly hint?: string;
@@ -1,3 +1,4 @@
1
+ import { commandSyntaxTree, syntaxHasFlag, } from "./syntax.js";
1
2
  export const RESOURCE_NAMES = [
2
3
  "agent",
3
4
  "app",
@@ -79,7 +80,7 @@ const PROJECT_SCOPED_RESOURCES = new Set([
79
80
  "saved-model",
80
81
  "wiki",
81
82
  ]);
82
- export function inferRequiresProject(resource, action, usage) {
83
+ export function inferRequiresProject(resource, action) {
83
84
  if (resource === "agent" || resource === "auth" || resource === "doctor" || resource === "commands"
84
85
  || resource === "install-skill" || resource === "version")
85
86
  return false;
@@ -92,7 +93,8 @@ export function inferRequiresProject(resource, action, usage) {
92
93
  return false;
93
94
  if (resource === "project-git")
94
95
  return !action.startsWith("future-");
95
- return usage.includes("--project-key");
96
+ const syntax = commandSyntaxTree(resource, action);
97
+ return syntax !== undefined && syntaxHasFlag(syntax, "project-key");
96
98
  }
97
99
  export class UsageError extends Error {
98
100
  code;
@@ -212,8 +212,10 @@ export const AGENT_CONTRACT_VERSION = 2;
212
212
  export const JSON_SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema";
213
213
  export const AGENT_CONTRACT_SCHEMA_ID = "https://clssck.github.io/dataiku-sdk/schemas/agent-contract-v2.json";
214
214
  export function cliVersionResult() {
215
+ /* oxlint-disable dss/no-direct-process-env -- launcher-injected load source and build revision */
215
216
  const loadSource = process.env["DSS_LOAD_SOURCE"] ?? detectLoadSource();
216
217
  const envBuildRevision = process.env["DSS_BUILD_REVISION"];
218
+ /* oxlint-enable dss/no-direct-process-env */
217
219
  return buildVersionPayload({
218
220
  packageVersion: CLI_VERSION,
219
221
  checkoutRevision: CLI_GIT_REVISION,
package/dist/src/cli.js CHANGED
@@ -4,7 +4,7 @@ import { homedir, } from "node:os";
4
4
  import { resolve, } from "node:path";
5
5
  import { num, unknownJsonInput, } from "./cli/coerce.js";
6
6
  import { commands, } from "./cli/commands/index.js";
7
- import { dataikuEnvironmentEnabled, loadEnvFile, } from "./cli/env.js";
7
+ import { ambientProjectKey, loadEnvFile, } from "./cli/env.js";
8
8
  import { executionMode, FLAG_ALIASES, isNegativeNumberToken, parseArgs, SHORT_FLAGS, VALUE_FLAGS, } from "./cli/flags.js";
9
9
  import { cleanupLedgerEntry, } from "./cli/helpers/cleanup.js";
10
10
  import { commandFailureExitCode, CommandResultFailure, flushCliWarnings, isFailedWaitResult, planResult, projectResultFields, setOutputFieldProjection, writeCommandResult, } from "./cli/output.js";
@@ -334,32 +334,11 @@ function validateSupportedCommandFlags(resource, action, flags) {
334
334
  ? "Remove --dry-run; use --plan to preview the mutation instead."
335
335
  : `Remove --dry-run; ${resource} ${action} has no dry-run mode.`);
336
336
  }
337
- }
338
- function stripOptionalUsageGroupsForValidation(usage) {
339
- let stripped = "";
340
- let optionalDepth = 0;
341
- for (const char of usage) {
342
- if (char === "[") {
343
- optionalDepth++;
344
- if (optionalDepth === 1)
345
- stripped += " ";
346
- continue;
347
- }
348
- if (char === "]" && optionalDepth > 0) {
349
- optionalDepth--;
350
- if (optionalDepth === 0)
351
- stripped += " ";
352
- continue;
353
- }
354
- if (optionalDepth === 0)
355
- stripped += char;
337
+ // Registry commands reject --plan in buildMutationPlan; meta commands never reach it.
338
+ if (META_COMMAND_RESOURCES[resource] === true && executionMode(flags).plan
339
+ && !entry.flags.some((flag) => flag.name === "plan")) {
340
+ throw new UsageError(`--plan is only supported for mutating commands; ${resource} ${action} has no plan mode.`, "usage_error", `Remove --plan; ${resource} ${action} has no plan mode.`);
356
341
  }
357
- return stripped;
358
- }
359
- function requiredPositionalCount(usage) {
360
- const requiredUsage = stripOptionalUsageGroupsForValidation(usage);
361
- const positionalTokens = [...requiredUsage.matchAll(/<[^>]+>/g),];
362
- return positionalTokens.length;
363
342
  }
364
343
  function flagIsProvided(flags, name, allowEmpty = false) {
365
344
  const value = flags[name];
@@ -370,7 +349,7 @@ function flagIsProvided(flags, name, allowEmpty = false) {
370
349
  return allowEmpty || value.length > 0;
371
350
  }
372
351
  function validateRequiredCommandInputs(resource, action, args, flags, entry) {
373
- const argCount = requiredPositionalCount(entry.usage);
352
+ const argCount = entry.positionalArguments.filter((positional) => positional.required).length;
374
353
  const allowEmptyFlags = {};
375
354
  for (const flag of entry.flags) {
376
355
  if (flag.allowEmptyValue === true)
@@ -776,6 +755,7 @@ async function runMetaCommand(resource, action, flags) {
776
755
  throw unknownActionError("doctor", action, ["run",]);
777
756
  }
778
757
  currentCommandContext.action = action ?? "run";
758
+ validateSupportedCommandFlags("doctor", "run", flags);
779
759
  const { runDoctor, } = await import("./cli/doctor.js");
780
760
  const { result, exitCode, } = await runDoctor(flags);
781
761
  return { action: "run", result, exitCode, };
@@ -791,6 +771,7 @@ async function runMetaCommand(resource, action, flags) {
791
771
  if (!authMeta) {
792
772
  throw unknownActionError("auth", action, validActions, "auth only supports 'login'. To check credentials/connectivity, run 'dss doctor'.");
793
773
  }
774
+ validateSupportedCommandFlags("auth", action, flags);
794
775
  if (executionMode(flags).plan) {
795
776
  return { action, result: authLoginPlan(flags), exitCode: 0, };
796
777
  }
@@ -802,6 +783,7 @@ async function runMetaCommand(resource, action, flags) {
802
783
  throw unknownActionError("install-skill", action, ["run",]);
803
784
  }
804
785
  currentCommandContext.action = action ?? "run";
786
+ validateSupportedCommandFlags("install-skill", "run", flags);
805
787
  return { action: "run", result: await runInstallSkill(flags), exitCode: 0, };
806
788
  }
807
789
  if (resource === "agent") {
@@ -811,6 +793,7 @@ async function runMetaCommand(resource, action, flags) {
811
793
  currentCommandContext.action = action;
812
794
  if (action !== "contract")
813
795
  throw unknownActionError("agent", action, ["contract",]);
796
+ validateSupportedCommandFlags("agent", "contract", flags);
814
797
  return { action, result: contract.buildAgentContract(), exitCode: 0, };
815
798
  }
816
799
  if (resource === "commands") {
@@ -856,10 +839,12 @@ async function runMetaCommand(resource, action, flags) {
856
839
  };
857
840
  }
858
841
  if (resource === "version") {
842
+ await loadCommandRuntime();
859
843
  if (action !== undefined && action !== "run") {
860
844
  throw unknownActionError("version", action, ["run",]);
861
845
  }
862
846
  currentCommandContext.action = action ?? "run";
847
+ validateSupportedCommandFlags("version", "run", flags);
863
848
  return { action: "run", result: cliVersionResult(), exitCode: 0, };
864
849
  }
865
850
  if (resource === "cleanup") {
@@ -868,6 +853,10 @@ async function runMetaCommand(resource, action, flags) {
868
853
  throw unknownActionError("cleanup", action, ["run",]);
869
854
  }
870
855
  currentCommandContext.action = action ?? "run";
856
+ validateSupportedCommandFlags("cleanup", "run", flags);
857
+ if (flags["apply"] === true && executionMode(flags).dryRun) {
858
+ throw new UsageError("--dry-run and --apply are mutually exclusive.", "usage_error");
859
+ }
871
860
  if (executionMode(flags).plan) {
872
861
  return { action: "run", result: cleanupPlan(flags), exitCode: 0, };
873
862
  }
@@ -880,6 +869,7 @@ async function runMetaCommand(resource, action, flags) {
880
869
  throw unknownActionError("fixtures", action, ["run",]);
881
870
  }
882
871
  currentCommandContext.action = action ?? "run";
872
+ validateSupportedCommandFlags("fixtures", "run", flags);
883
873
  const { runFixtures, } = await import("./cli/doctor.js");
884
874
  return { action: "run", result: await runFixtures(flags), exitCode: 0, };
885
875
  }
@@ -889,6 +879,7 @@ async function runMetaCommand(resource, action, flags) {
889
879
  throw unknownActionError("batch", action, ["run",]);
890
880
  }
891
881
  currentCommandContext.action = action ?? "run";
882
+ validateSupportedCommandFlags("batch", "run", flags);
892
883
  if (executionMode(flags).plan) {
893
884
  return { action: "run", result: batchPlan(flags), exitCode: 0, };
894
885
  }
@@ -993,9 +984,7 @@ async function runBatch(flags) {
993
984
  if (executionMode(flags).dryRun) {
994
985
  const batchProjectKey = typeof flags["project-key"] === "string"
995
986
  ? flags["project-key"]
996
- : dataikuEnvironmentEnabled()
997
- ? process.env.DATAIKU_PROJECT_KEY
998
- : undefined;
987
+ : ambientProjectKey();
999
988
  const planned = await Promise.all(steps.map(async (argv, index) => {
1000
989
  const context = batchStepCommandContext(argv);
1001
990
  Object.assign(currentCommandContext, { ...context, projectKey: batchProjectKey, });
@@ -1018,7 +1007,8 @@ async function runBatch(flags) {
1018
1007
  };
1019
1008
  }
1020
1009
  catch (error) {
1021
- const envelope = buildErrorReport(error);
1010
+ // Steps validate concurrently; the shared context may belong to a sibling.
1011
+ const envelope = buildErrorReport(error, { ...context, projectKey: batchProjectKey, });
1022
1012
  return {
1023
1013
  index,
1024
1014
  args: redactArgv(argv),
@@ -1037,9 +1027,7 @@ async function runBatch(flags) {
1037
1027
  validateBatchStepModes(steps);
1038
1028
  let projectKey = typeof flags["project-key"] === "string"
1039
1029
  ? flags["project-key"]
1040
- : dataikuEnvironmentEnabled()
1041
- ? process.env.DATAIKU_PROJECT_KEY
1042
- : undefined;
1030
+ : ambientProjectKey();
1043
1031
  let client;
1044
1032
  const needsClient = steps.some((argv) => batchStepNeedsClient(argv));
1045
1033
  if (needsClient) {
@@ -1247,8 +1235,7 @@ function rawFlagValue(argv, flagName) {
1247
1235
  function commandIsProjectScoped(resource, action) {
1248
1236
  if (!resource)
1249
1237
  return false;
1250
- const usage = commands[resource]?.[action ?? ""]?.usage ?? "";
1251
- return inferRequiresProject(resource, action ?? "", usage);
1238
+ return inferRequiresProject(resource, action ?? "");
1252
1239
  }
1253
1240
  function rawCommandContext() {
1254
1241
  const argv = process.argv.slice(2);
@@ -1256,15 +1243,13 @@ function rawCommandContext() {
1256
1243
  const resource = currentCommandContext.resource ?? positionals[0];
1257
1244
  const action = currentCommandContext.action ?? positionals[1];
1258
1245
  const explicitProjectKey = rawFlagValue(argv, "project-key") ?? rawFlagValue(argv, "project");
1259
- const ambientProjectKey = dataikuEnvironmentEnabled()
1260
- ? process.env.DATAIKU_PROJECT_KEY
1261
- : undefined;
1246
+ const environmentProjectKey = ambientProjectKey();
1262
1247
  return {
1263
1248
  resource,
1264
1249
  action,
1265
1250
  projectKey: explicitProjectKey
1266
1251
  ?? (commandIsProjectScoped(resource, action)
1267
- ? currentCommandContext.projectKey ?? ambientProjectKey
1252
+ ? currentCommandContext.projectKey ?? environmentProjectKey
1268
1253
  : undefined),
1269
1254
  };
1270
1255
  }
@@ -1364,7 +1349,6 @@ function dssDiagnosticDetails(err) {
1364
1349
  return {};
1365
1350
  }
1366
1351
  }
1367
- const MISSING_PROJECT_KEY_ERROR_PREFIX = "projectKey is required";
1368
1352
  function isAmbiguousMutationFailure(err, context) {
1369
1353
  if (!(err instanceof DataikuError))
1370
1354
  return false;
@@ -1399,9 +1383,11 @@ function boundedFailureResult(result) {
1399
1383
  function errorExitCode(err) {
1400
1384
  if (err instanceof CommandResultFailure)
1401
1385
  return err.exitCode;
1402
- if (err instanceof ClientValidationError && err.code === "assertion_failed")
1386
+ if (err instanceof ClientValidationError
1387
+ && (err.code === "assertion_failed" || err.code === "long_running_failure"))
1403
1388
  return 4;
1404
- if (err instanceof ClientValidationError && err.code === "ambiguous_outcome")
1389
+ if (err instanceof ClientValidationError
1390
+ && (err.code === "ambiguous_outcome" || err.code === "internal_error"))
1405
1391
  return 2;
1406
1392
  if (err instanceof UsageError || err instanceof ClientValidationError)
1407
1393
  return 1;
@@ -1409,15 +1395,14 @@ function errorExitCode(err) {
1409
1395
  return 2;
1410
1396
  if (err instanceof DataikuError)
1411
1397
  return err.category === "transient" ? 3 : 2;
1412
- if (err instanceof Error && err.message.startsWith(MISSING_PROJECT_KEY_ERROR_PREFIX))
1413
- return 1;
1414
1398
  return 2;
1415
1399
  }
1416
- function buildErrorReport(err) {
1417
- const context = rawCommandContext();
1400
+ function buildErrorReport(err, contextOverride) {
1401
+ const context = contextOverride ?? rawCommandContext();
1418
1402
  const exitCode = errorExitCode(err);
1419
1403
  if (err instanceof ClientValidationError
1420
- && (err.code === "ambiguous_outcome" || err.code === "assertion_failed")) {
1404
+ && (err.code === "ambiguous_outcome" || err.code === "assertion_failed"
1405
+ || err.code === "long_running_failure")) {
1421
1406
  return {
1422
1407
  type: "error",
1423
1408
  ok: false,
@@ -1439,7 +1424,9 @@ function buildErrorReport(err) {
1439
1424
  code: err.code,
1440
1425
  category: err.code === "target_absence_unverifiable"
1441
1426
  ? "permission_or_environment"
1442
- : "usage",
1427
+ : err.code === "internal_error"
1428
+ ? "internal"
1429
+ : "usage",
1443
1430
  exitCode,
1444
1431
  ...(err.code === "target_absence_unverifiable" ? { retryable: false, } : {}),
1445
1432
  ...(err.hint ? { hint: err.hint, } : {}),
@@ -1517,18 +1504,6 @@ function buildErrorReport(err) {
1517
1504
  ...context,
1518
1505
  };
1519
1506
  }
1520
- if (err instanceof Error && err.message.startsWith(MISSING_PROJECT_KEY_ERROR_PREFIX)) {
1521
- return {
1522
- type: "error",
1523
- ok: false,
1524
- error: err.message,
1525
- code: "missing_required_flag",
1526
- category: "usage",
1527
- exitCode,
1528
- hint: "Pass --project-key or set DATAIKU_PROJECT_KEY.",
1529
- ...context,
1530
- };
1531
- }
1532
1507
  const message = err instanceof Error ? err.message : String(err);
1533
1508
  return {
1534
1509
  type: "error",
@@ -1579,9 +1554,7 @@ async function main() {
1579
1554
  action: positional[1],
1580
1555
  projectKey: typeof flags["project-key"] === "string"
1581
1556
  ? flags["project-key"]
1582
- : dataikuEnvironmentEnabled()
1583
- ? process.env.DATAIKU_PROJECT_KEY
1584
- : undefined,
1557
+ : ambientProjectKey(),
1585
1558
  });
1586
1559
  const metaCommand = await runMetaCommand(resource, positional[1], flags);
1587
1560
  if (metaCommand) {
@@ -73,6 +73,15 @@ export interface DataikuGetOptions {
73
73
  * budget may shorten those limits but never extends them.
74
74
  */
75
75
  timeoutMs?: number;
76
+ /**
77
+ * Issue exactly one transport attempt (no retries). Independent of
78
+ * `timeoutMs`: when both are supplied the total budget above still applies
79
+ * and aborts the attempt; when no budget is supplied the attempt is capped
80
+ * by the client-level `requestTimeoutMs`. Wait loops use this for the one
81
+ * observation whose wait budget is already spent, so it still reaches the
82
+ * server once instead of failing before any transport attempt.
83
+ */
84
+ noRetry?: boolean;
76
85
  }
77
86
  export interface DataikuClientTraceEvent {
78
87
  type: "trace";
@@ -298,7 +307,7 @@ export declare class DataikuClient {
298
307
  /** Send raw file contents through the shared authenticated transport. */
299
308
  postRawBody(path: string, body: string | Uint8Array): Promise<void>;
300
309
  /** Git endpoints require Basic API-key authentication, unlike the bearer API. */
301
- requestGit(method: string, path: string, body?: unknown, deadlineAt?: number): Promise<Response>;
310
+ requestGit(method: string, path: string, body?: unknown, deadlineAt?: number, noRetry?: boolean): Promise<Response>;
302
311
  postStream(path: string, body?: unknown): Promise<Response>;
303
312
  put<T = unknown>(path: string, body: unknown): Promise<T>;
304
313
  del(path: string): Promise<void>;
@@ -3,7 +3,7 @@ import { basename, } from "node:path";
3
3
  import { getCACertificates, } from "node:tls";
4
4
  import { Value, } from "@sinclair/typebox/value";
5
5
  import { safeParseSchema, } from "./schemas.js";
6
- import { classifyDataikuError, ClientValidationError, DataikuError, NON_JSON_RESPONSE_MARKER, } from "./errors.js";
6
+ import { classifyDataikuError, ClientValidationError, DataikuError, nonJsonResponseBody, } from "./errors.js";
7
7
  import { AnalysesResource, } from "./resources/analyses.js";
8
8
  import { ApiDeployerResource, } from "./resources/api-deployer.js";
9
9
  import { ApiServicesResource, } from "./resources/api-services.js";
@@ -78,6 +78,19 @@ function computeBackoffDelayMs(retryNumber) {
78
78
  const cap = Math.min(MAX_BACKOFF_DELAY_MS, BASE_DELAY_MS * 2 ** Math.max(0, retryNumber - 1));
79
79
  return Math.floor(Math.random() * (cap + 1));
80
80
  }
81
+ /** Server-directed waits never bypass the existing automatic backoff cap. */
82
+ function retryAfterDelayMs(value) {
83
+ if (value === null)
84
+ return 0;
85
+ const text = value.trim();
86
+ if (/^\d+$/.test(text))
87
+ return Number(text) * 1_000;
88
+ // Accept HTTP dates, not Date.parse's permissive numeric date shorthand.
89
+ if (!/^[A-Za-z]{3,9},? /.test(text))
90
+ return 0;
91
+ const date = Date.parse(text);
92
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 0;
93
+ }
81
94
  function isTransientError(status, body) {
82
95
  return classifyDataikuError(status, body).category === "transient";
83
96
  }
@@ -174,7 +187,7 @@ function buildFetchTlsOptions(config) {
174
187
  }
175
188
  catch (error) {
176
189
  const message = error instanceof Error ? error.message : String(error);
177
- throw new Error(`Unable to read CA certificate bundle at ${caCertPath}: ${message}`, {
190
+ throw new ClientValidationError(`Unable to read CA certificate bundle at ${caCertPath}: ${message}`, "invalid_flag_value", undefined, undefined, {
178
191
  cause: error,
179
192
  });
180
193
  }
@@ -403,12 +416,14 @@ export class DataikuClient {
403
416
  return (this.groupsResource ??= new GroupsResource(this));
404
417
  }
405
418
  constructor(config) {
419
+ /* oxlint-disable dss/no-direct-process-env -- SDK constructor defaults (DATAIKU_URL/DATAIKU_API_KEY) for library callers */
406
420
  const envUrl = process.env["DATAIKU_URL"]?.trim();
407
421
  const envApiKey = process.env["DATAIKU_API_KEY"]?.trim();
422
+ /* oxlint-enable dss/no-direct-process-env */
408
423
  const url = config?.url?.trim() || envUrl;
409
424
  const apiKey = config?.apiKey?.trim() || envApiKey;
410
425
  if (!url || !apiKey) {
411
- throw new Error("Dataiku URL and API key are required: pass {url, apiKey} or set DATAIKU_URL/DATAIKU_API_KEY");
426
+ throw new ClientValidationError("Dataiku URL and API key are required: pass {url, apiKey} or set DATAIKU_URL/DATAIKU_API_KEY", "missing_required_flag");
412
427
  }
413
428
  if (hasEmbeddedUserinfo(url)) {
414
429
  throw new ClientValidationError("Dataiku URL must not contain embedded credentials (userinfo). Authenticate with an API key instead.", "validation_failed", "Pass the DSS base URL without a username or password.", { urlHasEmbeddedUserinfo: true, });
@@ -450,12 +465,12 @@ export class DataikuClient {
450
465
  return pk;
451
466
  if (this.defaultProjectKey)
452
467
  return this.defaultProjectKey;
453
- throw new Error("projectKey is required — pass it as a parameter or set projectKey in DataikuClientConfig");
468
+ throw new ClientValidationError("projectKey is required — pass it as a parameter or set projectKey in DataikuClientConfig", "missing_required_flag", "Pass --project-key or set DATAIKU_PROJECT_KEY.");
454
469
  }
455
470
  /* ---- public: HTTP verbs ---- */
456
471
  async get(path, options) {
457
472
  const deadlineAt = resolveGetDeadlineAt(options);
458
- const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, { method: "GET", headers: this.getHeaders(), }, undefined, deadlineAt);
473
+ const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, { method: "GET", headers: this.getHeaders(), }, options?.noRetry === true ? 1 : undefined, deadlineAt);
459
474
  return this.parseJsonResponse(res, deadlineAt);
460
475
  }
461
476
  /**
@@ -563,7 +578,7 @@ export class DataikuClient {
563
578
  await response.text();
564
579
  }
565
580
  /** Git endpoints require Basic API-key authentication, unlike the bearer API. */
566
- async requestGit(method, path, body, deadlineAt) {
581
+ async requestGit(method, path, body, deadlineAt, noRetry = false) {
567
582
  return this.fetchWithRetry(`${this.baseUrl}${path}`, {
568
583
  method,
569
584
  headers: {
@@ -572,7 +587,7 @@ export class DataikuClient {
572
587
  "Content-Type": "application/json",
573
588
  },
574
589
  body: body === undefined ? undefined : JSON.stringify(body),
575
- }, undefined, deadlineAt);
590
+ }, noRetry ? 1 : undefined, deadlineAt);
576
591
  }
577
592
  async postStream(path, body) {
578
593
  const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
@@ -797,8 +812,7 @@ export class DataikuClient {
797
812
  return JSON.parse(text);
798
813
  }
799
814
  catch {
800
- const summary = text.length > 300 ? `${text.slice(0, 300)}…` : text;
801
- throw new DataikuError(res.status, res.statusText || "Invalid JSON response", `${NON_JSON_RESPONSE_MARKER}: ${summary}`, undefined, this.requestIdFromHeaders(res.headers));
815
+ throw new DataikuError(res.status, res.statusText || "Invalid JSON response", nonJsonResponseBody(text), undefined, this.requestIdFromHeaders(res.headers));
802
816
  }
803
817
  }
804
818
  /* ---- private: bounded response body reading ---- */
@@ -1018,8 +1032,10 @@ export class DataikuClient {
1018
1032
  if (!res.ok) {
1019
1033
  const { text, truncated, } = await this.readBoundedBodyText(res, this.maxResponseBodyBytes, startedAt + attemptTimeoutMs);
1020
1034
  const canRetry = retryEnabled && attempt < maxAttempts && isTransientError(res.status, text);
1021
- if (canRetry) {
1022
- const delayMs = computeBackoffDelayMs(attempt);
1035
+ const serverDelayMs = canRetry ? retryAfterDelayMs(res.headers.get("retry-after")) : 0;
1036
+ // A longer server wait declines automatic retry; never clamp it into an early request.
1037
+ if (canRetry && serverDelayMs <= MAX_BACKOFF_DELAY_MS) {
1038
+ const delayMs = Math.max(computeBackoffDelayMs(attempt), serverDelayMs);
1023
1039
  // Do not start a retry whose backoff cannot fit in the budget.
1024
1040
  if (deadlineAt === undefined || Date.now() + delayMs <= deadlineAt) {
1025
1041
  delaysMs.push(delayMs);
@@ -26,7 +26,7 @@ export declare class ClientValidationError extends Error {
26
26
  readonly code: StableErrorCode;
27
27
  readonly hint?: string;
28
28
  readonly details?: Record<string, unknown>;
29
- constructor(message: string, code?: StableErrorCode, hint?: string, details?: Record<string, unknown>);
29
+ constructor(message: string, code?: StableErrorCode, hint?: string, details?: Record<string, unknown>, options?: ErrorOptions);
30
30
  }
31
31
  /**
32
32
  * Message prefix the client stamps on a 2xx body that is not JSON. The
@@ -34,6 +34,17 @@ export declare class ClientValidationError extends Error {
34
34
  * taxonomy stay in sync as one contract.
35
35
  */
36
36
  export declare const NON_JSON_RESPONSE_MARKER = "Expected JSON response body but got non-JSON content";
37
+ /** Body text for a 2xx response that should have been JSON; truncated to keep envelopes bounded. */
38
+ export declare function nonJsonResponseBody(text: string): string;
39
+ /** Prefix for a 2xx DSS response that lacks something the API documents (a body, an id, a job id). */
40
+ export declare const UNEXPECTED_RESPONSE_MARKER = "Unexpected DSS response";
41
+ /**
42
+ * The one representation of a malformed 2xx DSS response: a DataikuError whose
43
+ * body carries {@link UNEXPECTED_RESPONSE_MARKER}, classified `unexpected_response`.
44
+ */
45
+ export declare function unexpectedResponseError(detail: string, status?: number): DataikuError;
46
+ /** Prefix for a SQL query failure reported by DSS on a 2xx finish-streaming response. */
47
+ export declare const SQL_QUERY_FAILED_MARKER = "SQL query failed in DSS";
37
48
  export declare function classifyDataikuError(status: number, body: string): DataikuErrorTaxonomy;
38
49
  export declare class DataikuError extends Error {
39
50
  status: number;
@@ -47,8 +47,8 @@ export class ClientValidationError extends Error {
47
47
  code;
48
48
  hint;
49
49
  details;
50
- constructor(message, code = "validation_failed", hint, details) {
51
- super(message);
50
+ constructor(message, code = "validation_failed", hint, details, options) {
51
+ super(message, options);
52
52
  this.name = "ClientValidationError";
53
53
  this.code = code;
54
54
  this.hint = hint;
@@ -61,6 +61,21 @@ export class ClientValidationError extends Error {
61
61
  * taxonomy stay in sync as one contract.
62
62
  */
63
63
  export const NON_JSON_RESPONSE_MARKER = "Expected JSON response body but got non-JSON content";
64
+ /** Body text for a 2xx response that should have been JSON; truncated to keep envelopes bounded. */
65
+ export function nonJsonResponseBody(text) {
66
+ return `${NON_JSON_RESPONSE_MARKER}: ${text.length > 300 ? `${text.slice(0, 300)}…` : text}`;
67
+ }
68
+ /** Prefix for a 2xx DSS response that lacks something the API documents (a body, an id, a job id). */
69
+ export const UNEXPECTED_RESPONSE_MARKER = "Unexpected DSS response";
70
+ /**
71
+ * The one representation of a malformed 2xx DSS response: a DataikuError whose
72
+ * body carries {@link UNEXPECTED_RESPONSE_MARKER}, classified `unexpected_response`.
73
+ */
74
+ export function unexpectedResponseError(detail, status = 200) {
75
+ return new DataikuError(status, "Unexpected Response", `${UNEXPECTED_RESPONSE_MARKER}: ${detail}`);
76
+ }
77
+ /** Prefix for a SQL query failure reported by DSS on a 2xx finish-streaming response. */
78
+ export const SQL_QUERY_FAILED_MARKER = "SQL query failed in DSS";
64
79
  const TLS_CERTIFICATE_HINT = "TLS certificate verification failed. Trust the DSS/corporate CA with --ca-cert PATH or NODE_EXTRA_CA_CERTS; use --insecure only for temporary troubleshooting.";
65
80
  const BUSINESS_APPS_API_UNAVAILABLE_HINT = "Business Apps API is not available on this DSS instance. Use classic app commands or check DSS version/feature availability.";
66
81
  const MAX_ERROR_SUMMARY_LENGTH = 200;
@@ -92,10 +107,12 @@ function firstHtmlTextLine(body) {
92
107
  .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
93
108
  .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ")
94
109
  .replace(/<[^>]+>/g, "\n"));
95
- return text
96
- .split(/\r?\n/g)
97
- .map((line) => line.trim())
98
- .find((line) => line.length > 0);
110
+ for (const match of text.matchAll(/[^\n]+/g)) {
111
+ const line = match[0].trim();
112
+ if (line.length > 0)
113
+ return line;
114
+ }
115
+ return undefined;
99
116
  }
100
117
  function conciseErrorSummary(text) {
101
118
  const sanitized = stripServerFilesystemPaths(text);
@@ -164,13 +181,23 @@ export function classifyDataikuError(status, body) {
164
181
  // login page, HTML gateway) answered in place of the DSS API. Only the client's
165
182
  // own parse-failure marker produces this prefix.
166
183
  if (status >= 200 && status < 300
167
- && lowerBody.startsWith(NON_JSON_RESPONSE_MARKER.toLowerCase())) {
184
+ && (lowerBody.startsWith(NON_JSON_RESPONSE_MARKER.toLowerCase())
185
+ || lowerBody.startsWith(UNEXPECTED_RESPONSE_MARKER.toLowerCase()))) {
168
186
  return {
169
187
  category: "unexpected_response",
170
188
  retryable: false,
171
189
  retryHint: "DSS returned a non-JSON response to a JSON API request (typically a proxy, gateway, or login page). Verify the DSS URL and that no intermediary serves HTML; do not retry unchanged.",
172
190
  };
173
191
  }
192
+ // finish-streaming answers 2xx with the server-side failure text when the
193
+ // query itself failed (bad SQL, missing column, permissions on the source).
194
+ if (status >= 200 && status < 300 && lowerBody.startsWith(SQL_QUERY_FAILED_MARKER.toLowerCase())) {
195
+ return {
196
+ category: "validation",
197
+ retryable: false,
198
+ retryHint: "The SQL query failed in DSS. Fix the query or its inputs; do not retry unchanged.",
199
+ };
200
+ }
174
201
  const isSqlInputConnectionMismatch = (status === 400 || status >= 500)
175
202
  && lowerBody.includes("s3 dataset")
176
203
  && lowerBody.includes("athena connection");