dataiku-sdk 3.5.0 → 3.6.1

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 (44) hide show
  1. package/README.md +1 -1
  2. package/bin/dss.js +50 -9
  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/commands/app.js +3 -2
  7. package/dist/src/cli/commands/dataset.js +2 -4
  8. package/dist/src/cli/commands/job.js +4 -3
  9. package/dist/src/cli/contract.js +1 -1
  10. package/dist/src/cli/env.js +2 -1
  11. package/dist/src/cli/flags.d.ts +1 -0
  12. package/dist/src/cli/flags.js +4 -9
  13. package/dist/src/cli/helpers/app-permissions.js +5 -4
  14. package/dist/src/cli/helpers/flow-zone.js +6 -5
  15. package/dist/src/cli/version.d.ts +0 -2
  16. package/dist/src/cli/version.js +6 -9
  17. package/dist/src/cli.js +107 -222
  18. package/dist/src/client.d.ts +2 -1
  19. package/dist/src/client.js +25 -11
  20. package/dist/src/resources/base.d.ts +2 -0
  21. package/dist/src/resources/base.js +14 -2
  22. package/dist/src/resources/code-envs.js +1 -3
  23. package/dist/src/resources/data-quality.js +10 -10
  24. package/dist/src/resources/macros.d.ts +1 -1
  25. package/dist/src/resources/macros.js +1 -3
  26. package/dist/src/resources/recipes.js +1 -5
  27. package/dist/src/resources/saved-models.js +1 -13
  28. package/dist/src/resources/scenarios.js +1 -3
  29. package/dist/src/resources/sql.js +1 -5
  30. package/dist/src/resources/statistics.js +1 -3
  31. package/dist/src/utils/deep-merge.js +2 -4
  32. package/dist/src/utils/flow-map.js +1 -6
  33. package/dist/src/utils/project-archive.js +8 -12
  34. package/dist/src/utils/project-incarnation.js +3 -7
  35. package/dist/src/utils/records.d.ts +4 -0
  36. package/dist/src/utils/records.js +8 -0
  37. package/package.json +7 -11
  38. package/plugin.json +1 -1
  39. package/skills/dataiku-dss/SKILL.md +1 -1
  40. package/skills/dataiku-dss/references/authentication.md +2 -2
  41. package/skills/dataiku-dss/references/discovery.md +1 -1
  42. package/skills/dataiku-dss/references/mutations.md +0 -1
  43. package/packages/types/dist/index.d.ts +0 -1062
  44. package/packages/types/dist/index.js +0 -871
package/dist/src/cli.js CHANGED
@@ -9,6 +9,7 @@ import { executionMode, FLAG_ALIASES, isNegativeNumberToken, parseArgs, SHORT_FL
9
9
  import { cleanupLedgerEntry, } from "./cli/helpers/cleanup.js";
10
10
  import { commandFailureExitCode, CommandResultFailure, flushCliWarnings, isFailedWaitResult, planResult, projectResultFields, setOutputFieldProjection, writeCommandResult, } from "./cli/output.js";
11
11
  import { currentCommandContext, resolveCredentials, resolveLoginCredentials, } from "./cli/runtime.js";
12
+ import { commandUsage, } from "./cli/syntax.js";
12
13
  import { COMMANDS_RUN_HINT, inferRequiresProject, missingActionError, noCommandError, requireArgs, RESOURCE_NAMES, unknownActionError, unknownResourceError, UsageError, } from "./cli/usage.js";
13
14
  import { cliVersionResult, } from "./cli/version.js";
14
15
  import { getCredentialsPath, } from "./config.js";
@@ -171,17 +172,6 @@ async function runCleanup(flags) {
171
172
  || !commands[resource]?.[action]) {
172
173
  throw new UsageError(`Invalid cleanup argv: ${entry.cleanup.argv.join(" ")}`);
173
174
  }
174
- if (resource === "project"
175
- && action === "delete"
176
- && typeof parsed.flags["expect-project-incarnation"] !== "string") {
177
- throw new UsageError("Project cleanup entry is not bound to a project incarnation. Refusing deletion after possible project-key reuse.", "validation_failed", "Capture a new cleanup entry from the current CLI. Legacy entries cannot safely delete projects.");
178
- }
179
- if (resource === "app"
180
- && action === "delete-instance"
181
- && parsed.flags["unconfirmed-creation"] !== true
182
- && typeof parsed.flags["expect-project-incarnation"] !== "string") {
183
- throw new UsageError("App cleanup entry is not bound to a project incarnation. Refusing deletion after possible project-key reuse.", "validation_failed", "Capture a new cleanup entry from the current CLI. Legacy entries cannot safely delete app projects.");
184
- }
185
175
  const result = await commands[resource][action].handler(client, args, parsed.flags);
186
176
  if (isFailedWaitResult(result)) {
187
177
  const failure = {
@@ -418,81 +408,60 @@ async function validateDryRunHandlerPreconditions(handler, args, flags) {
418
408
  throw error;
419
409
  }
420
410
  }
411
+ /**
412
+ * The one validator for meta commands, shared by direct runs and batch
413
+ * --dry-run: action, supported flags (incl. --plan/--dry-run), flag conflicts,
414
+ * and required inputs. Returns the canonical action, or undefined for a
415
+ * non-meta resource.
416
+ */
421
417
  async function validateMetaCommandInputs(resource, action, flags) {
422
- if (resource === "doctor") {
423
- if (action !== undefined && action !== "run") {
424
- throw unknownActionError("doctor", action, ["run",]);
425
- }
426
- return "run";
427
- }
418
+ if (META_COMMAND_RESOURCES[resource] !== true)
419
+ return undefined;
420
+ await loadCommandRuntime();
421
+ let resolved;
428
422
  if (resource === "auth") {
429
423
  const validActions = Object.keys(contract.AUTH_ACTIONS);
430
- if (!action) {
431
- throw missingActionError("auth", validActions, "dss auth login --url URL --api-key KEY");
432
- }
424
+ if (!action)
425
+ throw missingActionError("auth", validActions, commandUsage("auth", "login"));
433
426
  if (!contract.AUTH_ACTIONS[action]) {
434
427
  throw unknownActionError("auth", action, validActions, "auth only supports 'login'. To check credentials/connectivity, run 'dss doctor'.");
435
428
  }
436
- return action;
437
- }
438
- if (resource === "install-skill") {
439
- if (action !== undefined && action !== "run") {
440
- throw unknownActionError("install-skill", action, ["run",]);
441
- }
442
- const { AGENTS, } = await import("./skill.js");
443
- const agentFilter = typeof flags["agent"] === "string" ? flags["agent"] : undefined;
444
- if (agentFilter && !AGENTS[agentFilter]) {
445
- throw new UsageError(`Unknown agent: ${agentFilter}.`, "usage_error", COMMANDS_RUN_HINT, { agent: agentFilter, validAgents: Object.keys(AGENTS), });
446
- }
447
- return "run";
429
+ resolved = action;
448
430
  }
449
- if (resource === "agent") {
431
+ else if (resource === "agent" || resource === "commands") {
432
+ const only = resource === "agent" ? "contract" : "run";
433
+ const usage = resource === "agent" ? contract.AGENT_CONTRACT_USAGE : contract.COMMANDS_USAGE;
450
434
  if (!action)
451
- throw missingActionError("agent", ["contract",], contract.AGENT_CONTRACT_USAGE);
452
- if (action !== "contract")
453
- throw unknownActionError("agent", action, ["contract",]);
454
- return action;
435
+ throw missingActionError(resource, [only,], usage);
436
+ if (action !== only)
437
+ throw unknownActionError(resource, action, [only,]);
438
+ resolved = action;
455
439
  }
456
- if (resource === "commands") {
457
- if (!action)
458
- throw missingActionError("commands", ["run",], contract.COMMANDS_USAGE);
459
- if (action !== "run")
460
- throw unknownActionError("commands", action, ["run",]);
461
- validateSupportedCommandFlags("commands", "run", flags);
462
- commandRegistrySelectors(flags);
463
- return action;
464
- }
465
- if (resource === "version") {
440
+ else {
466
441
  if (action !== undefined && action !== "run") {
467
- throw unknownActionError("version", action, ["run",]);
442
+ throw unknownActionError(resource, action, ["run",]);
468
443
  }
469
- return "run";
444
+ resolved = "run";
470
445
  }
471
- if (resource === "cleanup") {
472
- if (action !== undefined && action !== "run") {
473
- throw unknownActionError("cleanup", action, ["run",]);
474
- }
475
- const entry = cachedCommandRegistry("cleanup")?.run;
446
+ validateSupportedCommandFlags(resource, resolved, flags);
447
+ if (resource === "commands")
448
+ commandRegistrySelectors(flags);
449
+ if (resource === "cleanup" || resource === "batch") {
450
+ const entry = cachedCommandRegistry(resource)?.run;
476
451
  if (entry)
477
- validateRequiredCommandInputs("cleanup", "run", [], flags, entry);
478
- return "run";
452
+ validateRequiredCommandInputs(resource, "run", [], flags, entry);
479
453
  }
480
- if (resource === "fixtures") {
481
- if (action !== undefined && action !== "run") {
482
- throw unknownActionError("fixtures", action, ["run",]);
483
- }
484
- return "run";
454
+ if (resource === "cleanup" && flags["apply"] === true && executionMode(flags).dryRun) {
455
+ throw new UsageError("--dry-run and --apply are mutually exclusive.", "usage_error");
485
456
  }
486
- if (resource === "batch") {
487
- if (action !== undefined && action !== "run") {
488
- throw unknownActionError("batch", action, ["run",]);
457
+ if (resource === "install-skill") {
458
+ const { AGENTS, } = await import("./skill.js");
459
+ const agentFilter = typeof flags["agent"] === "string" ? flags["agent"] : undefined;
460
+ if (agentFilter && !AGENTS[agentFilter]) {
461
+ throw new UsageError(`Unknown agent: ${agentFilter}.`, "usage_error", COMMANDS_RUN_HINT, { agent: agentFilter, validAgents: Object.keys(AGENTS), });
489
462
  }
490
- const entry = cachedCommandRegistry("batch")?.run;
491
- if (entry)
492
- validateRequiredCommandInputs("batch", "run", [], flags, entry);
493
- return "run";
494
463
  }
495
- return undefined;
464
+ return resolved;
496
465
  }
497
466
  async function validateBatchStep(argv) {
498
467
  const { positional, flags, } = parseArgs(argv);
@@ -532,29 +501,8 @@ function batchStepNeedsClient(argv) {
532
501
  }
533
502
  }
534
503
  function batchStepCommandContext(argv) {
535
- const positionals = [];
536
- for (let index = 0; index < argv.length; index++) {
537
- const arg = argv[index];
538
- if (arg === "--") {
539
- positionals.push(...argv.slice(index + 1));
540
- break;
541
- }
542
- if (arg.startsWith("--")) {
543
- const name = arg.slice(2).split("=")[0] ?? "";
544
- const canonical = FLAG_ALIASES[name] ?? name;
545
- if (!arg.includes("=") && VALUE_FLAGS.has(canonical))
546
- index++;
547
- continue;
548
- }
549
- if (arg.length === 2 && arg[0] === "-" && arg[1] !== "-") {
550
- const long = SHORT_FLAGS[arg[1]];
551
- if (long && VALUE_FLAGS.has(long))
552
- index++;
553
- continue;
554
- }
555
- positionals.push(arg);
556
- }
557
- return { resource: positionals[0], action: positionals[1], };
504
+ const [resource, action,] = rawPositionals(argv);
505
+ return { resource, action, };
558
506
  }
559
507
  const SENSITIVE_ARGV_VALUE_FLAGS = {
560
508
  "api-key": true,
@@ -748,145 +696,78 @@ function batchPlan(flags) {
748
696
  plannedAndDryRun: executionMode(flags).dryRun,
749
697
  });
750
698
  }
751
- async function runMetaCommand(resource, action, flags) {
752
- if (resource === "doctor") {
753
- await loadCommandRuntime();
754
- if (action !== undefined && action !== "run") {
755
- throw unknownActionError("doctor", action, ["run",]);
756
- }
757
- currentCommandContext.action = action ?? "run";
758
- validateSupportedCommandFlags("doctor", "run", flags);
759
- const { runDoctor, } = await import("./cli/doctor.js");
760
- const { result, exitCode, } = await runDoctor(flags);
761
- return { action: "run", result, exitCode, };
762
- }
763
- if (resource === "auth") {
764
- await loadCommandRuntime();
765
- const validActions = Object.keys(contract.AUTH_ACTIONS);
766
- if (!action) {
767
- throw missingActionError("auth", validActions, "dss auth login --url URL --api-key KEY");
768
- }
769
- currentCommandContext.action = action;
770
- const authMeta = contract.AUTH_ACTIONS[action];
771
- if (!authMeta) {
772
- throw unknownActionError("auth", action, validActions, "auth only supports 'login'. To check credentials/connectivity, run 'dss doctor'.");
773
- }
774
- validateSupportedCommandFlags("auth", action, flags);
775
- if (executionMode(flags).plan) {
776
- return { action, result: authLoginPlan(flags), exitCode: 0, };
777
- }
778
- return { action, result: await authMeta.handler(flags), exitCode: 0, };
779
- }
780
- if (resource === "install-skill") {
781
- await loadCommandRuntime();
782
- if (action !== undefined && action !== "run") {
783
- throw unknownActionError("install-skill", action, ["run",]);
784
- }
785
- currentCommandContext.action = action ?? "run";
786
- validateSupportedCommandFlags("install-skill", "run", flags);
787
- return { action: "run", result: await runInstallSkill(flags), exitCode: 0, };
788
- }
789
- if (resource === "agent") {
790
- await loadCommandRuntime();
791
- if (!action)
792
- throw missingActionError("agent", ["contract",], contract.AGENT_CONTRACT_USAGE);
793
- currentCommandContext.action = action;
794
- if (action !== "contract")
795
- throw unknownActionError("agent", action, ["contract",]);
796
- validateSupportedCommandFlags("agent", "contract", flags);
797
- return { action, result: contract.buildAgentContract(), exitCode: 0, };
798
- }
799
- if (resource === "commands") {
800
- await loadCommandRuntime();
801
- if (!action)
802
- throw missingActionError("commands", ["run",], contract.COMMANDS_USAGE);
803
- currentCommandContext.action = action;
804
- if (action !== "run")
805
- throw unknownActionError("commands", action, ["run",]);
806
- validateSupportedCommandFlags("commands", "run", flags);
807
- const selectors = commandRegistrySelectors(flags);
808
- const outputPath = flags["output"];
809
- if (selectors.length === 0 && typeof outputPath !== "string") {
810
- return { action, result: contract.commandActionSummary(), exitCode: 0, };
699
+ async function runMetaCommand(resource, rawAction, flags) {
700
+ const action = await validateMetaCommandInputs(resource, rawAction, flags);
701
+ if (action === undefined)
702
+ return undefined;
703
+ currentCommandContext.action = action;
704
+ const plan = executionMode(flags).plan;
705
+ switch (resource) {
706
+ case "doctor": {
707
+ const { runDoctor, } = await import("./cli/doctor.js");
708
+ const { result, exitCode, } = await runDoctor(flags);
709
+ return { action, result, exitCode, };
811
710
  }
812
- const registry = selectors.length === 0 ? contract.buildCommandRegistry() : {};
813
- for (const selector of selectors) {
814
- const selectorParts = selector.split(".");
815
- if (selectorParts.some((part) => part.length === 0)) {
816
- throw new UsageError(`Invalid --fields selector: ${selector}. Expected RESOURCE or RESOURCE.ACTION[.FIELD...].`, "usage_error", contract.COMMANDS_USAGE, { selector, });
817
- }
818
- const [selectedResource, selectedAction,] = selectorParts;
819
- const resourceActions = cachedCommandRegistry(selectedResource);
820
- if (!resourceActions)
821
- throw unknownResourceError(selectedResource);
822
- registry[selectedResource] = resourceActions;
823
- if (selectedAction && !resourceActions[selectedAction]) {
824
- throw unknownActionError(selectedResource, selectedAction, Object.keys(resourceActions));
825
- }
711
+ case "auth":
712
+ if (plan)
713
+ return { action, result: authLoginPlan(flags), exitCode: 0, };
714
+ return { action, result: await contract.AUTH_ACTIONS[action].handler(flags), exitCode: 0, };
715
+ case "install-skill":
716
+ return { action, result: await runInstallSkill(flags), exitCode: 0, };
717
+ case "agent":
718
+ return { action, result: contract.buildAgentContract(), exitCode: 0, };
719
+ case "version":
720
+ return { action, result: cliVersionResult(), exitCode: 0, };
721
+ case "fixtures": {
722
+ const { runFixtures, } = await import("./cli/doctor.js");
723
+ return { action, result: await runFixtures(flags), exitCode: 0, };
826
724
  }
827
- const selectedRegistry = selectors.length === 0
828
- ? registry
829
- : projectResultFields(registry, selectors, () => Object.keys(contract.commandActionSummary()).sort());
830
- if (typeof outputPath === "string") {
831
- const exported = selectedRegistry;
832
- await fs.writeFile(outputPath, `${JSON.stringify(exported)}\n`, "utf-8");
833
- return { action, result: { path: outputPath, }, exitCode: 0, };
725
+ case "cleanup": {
726
+ if (plan)
727
+ return { action, result: cleanupPlan(flags), exitCode: 0, };
728
+ const { result, exitCode, } = await runCleanup(flags);
729
+ return { action, result, exitCode, };
834
730
  }
835
- return {
836
- action,
837
- result: selectedRegistry,
838
- exitCode: 0,
839
- };
840
- }
841
- if (resource === "version") {
842
- await loadCommandRuntime();
843
- if (action !== undefined && action !== "run") {
844
- throw unknownActionError("version", action, ["run",]);
731
+ case "batch": {
732
+ if (plan)
733
+ return { action, result: batchPlan(flags), exitCode: 0, };
734
+ const { result, exitCode, } = await runBatch(flags);
735
+ return { action, result, exitCode, };
845
736
  }
846
- currentCommandContext.action = action ?? "run";
847
- validateSupportedCommandFlags("version", "run", flags);
848
- return { action: "run", result: cliVersionResult(), exitCode: 0, };
737
+ default:
738
+ return { action, result: await commandsRunResult(flags), exitCode: 0, };
849
739
  }
850
- if (resource === "cleanup") {
851
- await loadCommandRuntime();
852
- if (action !== undefined && action !== "run") {
853
- throw unknownActionError("cleanup", action, ["run",]);
854
- }
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
- }
860
- if (executionMode(flags).plan) {
861
- return { action: "run", result: cleanupPlan(flags), exitCode: 0, };
740
+ }
741
+ async function commandsRunResult(flags) {
742
+ const selectors = commandRegistrySelectors(flags);
743
+ const outputPath = flags["output"];
744
+ if (selectors.length === 0 && typeof outputPath !== "string") {
745
+ return contract.commandActionSummary();
746
+ }
747
+ const registry = selectors.length === 0 ? contract.buildCommandRegistry() : {};
748
+ for (const selector of selectors) {
749
+ const selectorParts = selector.split(".");
750
+ if (selectorParts.some((part) => part.length === 0)) {
751
+ throw new UsageError(`Invalid --fields selector: ${selector}. Expected RESOURCE or RESOURCE.ACTION[.FIELD...].`, "usage_error", contract.COMMANDS_USAGE, { selector, });
862
752
  }
863
- const { result, exitCode, } = await runCleanup(flags);
864
- return { action: "run", result, exitCode, };
865
- }
866
- if (resource === "fixtures") {
867
- await loadCommandRuntime();
868
- if (action !== undefined && action !== "run") {
869
- throw unknownActionError("fixtures", action, ["run",]);
753
+ const [selectedResource, selectedAction,] = selectorParts;
754
+ const resourceActions = cachedCommandRegistry(selectedResource);
755
+ if (!resourceActions)
756
+ throw unknownResourceError(selectedResource);
757
+ registry[selectedResource] = resourceActions;
758
+ if (selectedAction && !resourceActions[selectedAction]) {
759
+ throw unknownActionError(selectedResource, selectedAction, Object.keys(resourceActions));
870
760
  }
871
- currentCommandContext.action = action ?? "run";
872
- validateSupportedCommandFlags("fixtures", "run", flags);
873
- const { runFixtures, } = await import("./cli/doctor.js");
874
- return { action: "run", result: await runFixtures(flags), exitCode: 0, };
875
761
  }
876
- if (resource === "batch") {
877
- await loadCommandRuntime();
878
- if (action !== undefined && action !== "run") {
879
- throw unknownActionError("batch", action, ["run",]);
880
- }
881
- currentCommandContext.action = action ?? "run";
882
- validateSupportedCommandFlags("batch", "run", flags);
883
- if (executionMode(flags).plan) {
884
- return { action: "run", result: batchPlan(flags), exitCode: 0, };
885
- }
886
- const { result, exitCode, } = await runBatch(flags);
887
- return { action: "run", result, exitCode, };
762
+ const selectedRegistry = selectors.length === 0
763
+ ? registry
764
+ : projectResultFields(registry, selectors, () => Object.keys(contract.commandActionSummary()).sort());
765
+ if (typeof outputPath === "string") {
766
+ const exported = selectedRegistry;
767
+ await fs.writeFile(outputPath, `${JSON.stringify(exported)}\n`, "utf-8");
768
+ return { path: outputPath, };
888
769
  }
889
- return undefined;
770
+ return selectedRegistry;
890
771
  }
891
772
  /**
892
773
  * Cleanup-ledger support is checked before a handler runs so an unsupported
@@ -1389,6 +1270,10 @@ function errorExitCode(err) {
1389
1270
  if (err instanceof ClientValidationError
1390
1271
  && (err.code === "ambiguous_outcome" || err.code === "internal_error"))
1391
1272
  return 2;
1273
+ // A permission/environment refusal is not a usage error (exit 1 is usage only).
1274
+ if ((err instanceof UsageError || err instanceof ClientValidationError)
1275
+ && err.code === "target_absence_unverifiable")
1276
+ return 2;
1392
1277
  if (err instanceof UsageError || err instanceof ClientValidationError)
1393
1278
  return 1;
1394
1279
  if (isAmbiguousMutationFailure(err, rawCommandContext()))
@@ -118,7 +118,7 @@ export interface DataikuClientConfig {
118
118
  projectKey?: string;
119
119
  /** Request/header timeout and buffered-body budget; idle timeout per raw stream read (default 30 000 ms). */
120
120
  requestTimeoutMs?: number;
121
- /** Max retry attempts for idempotent requests (default 4, capped at 10) */
121
+ /** Maximum total attempts, initial request included, for idempotent requests (default 4, capped at 10; 1 = no retry) */
122
122
  retryMaxAttempts?: number;
123
123
  /**
124
124
  * Maximum bytes retained from a single response body by text/JSON consumers
@@ -336,6 +336,7 @@ export declare class DataikuClient {
336
336
  stream(path: string): Promise<Response>;
337
337
  /** Bound each pending read, not total transfer time; preserve large downloads and backpressure. */
338
338
  private withBodyDeadline;
339
+ /** JSON request headers; Content-Type only when a body is sent. */
339
340
  private getHeaders;
340
341
  private getAnyHeaders;
341
342
  private responseMeta;
@@ -470,7 +470,7 @@ export class DataikuClient {
470
470
  /* ---- public: HTTP verbs ---- */
471
471
  async get(path, options) {
472
472
  const deadlineAt = resolveGetDeadlineAt(options);
473
- const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, { method: "GET", headers: this.getHeaders(), }, options?.noRetry === true ? 1 : undefined, deadlineAt);
473
+ const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, { method: "GET", headers: this.getHeaders(false), }, options?.noRetry === true ? 1 : undefined, deadlineAt);
474
474
  return this.parseJsonResponse(res, deadlineAt);
475
475
  }
476
476
  /**
@@ -481,7 +481,7 @@ export class DataikuClient {
481
481
  async getWithMetadata(path) {
482
482
  const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
483
483
  method: "GET",
484
- headers: this.getHeaders(),
484
+ headers: this.getHeaders(false),
485
485
  });
486
486
  const data = await this.parseJsonResponse(res);
487
487
  return { data, meta: this.responseMeta(res.headers), };
@@ -543,7 +543,7 @@ export class DataikuClient {
543
543
  }
544
544
  const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
545
545
  method: "POST",
546
- headers: this.getHeaders(),
546
+ headers: this.getHeaders(body !== undefined),
547
547
  body: body !== undefined ? JSON.stringify(body) : undefined,
548
548
  }, retryMaxAttempts);
549
549
  return this.parseJsonResponse(res);
@@ -555,7 +555,7 @@ export class DataikuClient {
555
555
  async postText(path, body) {
556
556
  const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
557
557
  method: "POST",
558
- headers: this.getHeaders(),
558
+ headers: this.getHeaders(body !== undefined),
559
559
  body: body !== undefined ? JSON.stringify(body) : undefined,
560
560
  });
561
561
  const { text, truncated, } = await this.readBoundedBodyText(res, this.maxResponseBodyBytes);
@@ -600,7 +600,7 @@ export class DataikuClient {
600
600
  async put(path, body) {
601
601
  const res = await this.fetchWithRetry(`${this.baseUrl}${path}`, {
602
602
  method: "PUT",
603
- headers: this.getHeaders(),
603
+ headers: this.getHeaders(true),
604
604
  body: JSON.stringify(body),
605
605
  });
606
606
  return this.parseJsonResponse(res);
@@ -608,13 +608,13 @@ export class DataikuClient {
608
608
  async del(path) {
609
609
  await this.fetchWithRetry(`${this.baseUrl}${path}`, {
610
610
  method: "DELETE",
611
- headers: this.getHeaders(),
611
+ headers: this.getHeaders(false),
612
612
  });
613
613
  }
614
614
  async putVoid(path, body) {
615
615
  await this.fetchWithRetry(`${this.baseUrl}${path}`, {
616
616
  method: "PUT",
617
- headers: this.getHeaders(),
617
+ headers: this.getHeaders(true),
618
618
  body: JSON.stringify(body),
619
619
  });
620
620
  }
@@ -626,7 +626,7 @@ export class DataikuClient {
626
626
  async putVoidNoBody(path) {
627
627
  await this.fetchWithRetry(`${this.baseUrl}${path}`, {
628
628
  method: "PUT",
629
- headers: this.getHeaders(),
629
+ headers: this.getHeaders(false),
630
630
  });
631
631
  }
632
632
  /**
@@ -731,11 +731,12 @@ export class DataikuClient {
731
731
  return response;
732
732
  }
733
733
  /* ---- private: headers ---- */
734
- getHeaders() {
734
+ /** JSON request headers; Content-Type only when a body is sent. */
735
+ getHeaders(withBody) {
735
736
  return {
736
737
  Authorization: `Bearer ${this.apiKey}`,
737
738
  Accept: "application/json",
738
- "Content-Type": "application/json",
739
+ ...(withBody ? { "Content-Type": "application/json", } : {}),
739
740
  };
740
741
  }
741
742
  getAnyHeaders() {
@@ -1030,7 +1031,20 @@ export class DataikuClient {
1030
1031
  elapsedMs: Date.now() - startedAt,
1031
1032
  });
1032
1033
  if (!res.ok) {
1033
- const { text, truncated, } = await this.readBoundedBodyText(res, this.maxResponseBodyBytes, startedAt + attemptTimeoutMs);
1034
+ // The status is already known; a body that misses the deadline must not
1035
+ // turn a real 5xx/4xx into a bare transport timeout.
1036
+ let text = "";
1037
+ let truncated = false;
1038
+ try {
1039
+ ({ text, truncated, } = await this.readBoundedBodyText(res, this.maxResponseBodyBytes, startedAt + attemptTimeoutMs));
1040
+ }
1041
+ catch (bodyError) {
1042
+ // Oversized bodies keep their own classified error; any other read
1043
+ // failure (deadline abort, dropped connection) keeps the known status.
1044
+ if (bodyError instanceof DataikuError && bodyError.status !== 0)
1045
+ throw bodyError;
1046
+ text = `(response body not received within ${attemptTimeoutMs}ms)`;
1047
+ }
1034
1048
  const canRetry = retryEnabled && attempt < maxAttempts && isTransientError(res.status, text);
1035
1049
  const serverDelayMs = canRetry ? retryAfterDelayMs(res.headers.get("retry-after")) : 0;
1036
1050
  // A longer server wait declines automatic retry; never clamp it into an early request.
@@ -3,6 +3,8 @@ import type { DataikuClient } from "../client.js";
3
3
  export declare function requireArrayResponse<T>(value: unknown, operation: string): T[];
4
4
  /** Validate identifiers without changing significant whitespace or encoding. */
5
5
  export declare function requireNonEmpty(value: unknown, name: string): string;
6
+ export declare function requireObject(value: unknown, name: string): Record<string, unknown>;
7
+ export declare function requireNonEmptyArray(value: string[], name: string): string[];
6
8
  export declare abstract class BaseResource {
7
9
  protected readonly client: DataikuClient;
8
10
  constructor(client: DataikuClient);
@@ -1,8 +1,8 @@
1
- import { ClientValidationError, DataikuError, } from "../errors.js";
1
+ import { ClientValidationError, unexpectedResponseError, } from "../errors.js";
2
2
  /** Never disguise an unexpected successful HTTP payload as an empty listing. */
3
3
  export function requireArrayResponse(value, operation) {
4
4
  if (!Array.isArray(value)) {
5
- throw new DataikuError(200, "Unexpected Response", `${operation}: DSS did not return an array.`);
5
+ throw unexpectedResponseError(`${operation}: DSS did not return an array.`);
6
6
  }
7
7
  return value;
8
8
  }
@@ -13,6 +13,18 @@ export function requireNonEmpty(value, name) {
13
13
  }
14
14
  return value;
15
15
  }
16
+ export function requireObject(value, name) {
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
18
+ throw new ClientValidationError(`${name} must be a JSON object.`, "validation_failed");
19
+ }
20
+ return value;
21
+ }
22
+ export function requireNonEmptyArray(value, name) {
23
+ if (!Array.isArray(value) || value.length === 0) {
24
+ throw new ClientValidationError(`${name} must be a non-empty array.`, "validation_failed");
25
+ }
26
+ return value;
27
+ }
16
28
  export class BaseResource {
17
29
  client;
18
30
  constructor(client) {
@@ -1,5 +1,6 @@
1
1
  import { ClientValidationError, } from "../errors.js";
2
2
  import { CodeEnvLogSummaryArraySchema, CodeEnvSummaryArraySchema, CodeEnvUsageArraySchema, CodeEnvVersionForProjectSchema, } from "../schemas.js";
3
+ import { isRecord, } from "../utils/records.js";
3
4
  import { stableHash, } from "../utils/stable-hash.js";
4
5
  import { BaseResource, } from "./base.js";
5
6
  /** Byte cap applied to fetched code-env logs so stdout/JSON stays bounded. */
@@ -212,6 +213,3 @@ function normalizePackageList(packages) {
212
213
  const lines = Array.isArray(packages) ? packages : packages.split(/\r?\n/);
213
214
  return lines.map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
214
215
  }
215
- function isRecord(value) {
216
- return value !== null && typeof value === "object" && !Array.isArray(value);
217
- }