dataiku-sdk 3.8.0 → 4.0.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 (68) hide show
  1. package/README.md +7 -5
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +4 -0
  4. package/dist/packages/types/src/index.js +4 -0
  5. package/dist/src/cli/coerce.d.ts +6 -0
  6. package/dist/src/cli/coerce.js +9 -0
  7. package/dist/src/cli/command-syntax.json +36 -0
  8. package/dist/src/cli/commands/app.js +2 -2
  9. package/dist/src/cli/commands/code-env.js +5 -6
  10. package/dist/src/cli/commands/code.js +16 -2
  11. package/dist/src/cli/commands/dashboard.js +1 -2
  12. package/dist/src/cli/commands/dataset.js +2 -2
  13. package/dist/src/cli/commands/flow-zone.js +1 -1
  14. package/dist/src/cli/commands/job.js +101 -22
  15. package/dist/src/cli/commands/plugin.js +7 -7
  16. package/dist/src/cli/commands/project-library.js +2 -2
  17. package/dist/src/cli/commands/project.js +25 -7
  18. package/dist/src/cli/commands/recipe.js +11 -19
  19. package/dist/src/cli/commands/scenario.js +2 -4
  20. package/dist/src/cli/contract.js +29 -10
  21. package/dist/src/cli/env.js +2 -3
  22. package/dist/src/cli/helpers/app-permissions.d.ts +1 -1
  23. package/dist/src/cli/helpers/app-permissions.js +2 -2
  24. package/dist/src/cli/helpers/cleanup.d.ts +2 -0
  25. package/dist/src/cli/helpers/cleanup.js +122 -8
  26. package/dist/src/cli/helpers/flow-zone.d.ts +1 -1
  27. package/dist/src/cli/helpers/flow-zone.js +3 -4
  28. package/dist/src/cli/list-output.d.ts +5 -0
  29. package/dist/src/cli/list-output.js +11 -2
  30. package/dist/src/cli/list-shapes.d.ts +6 -0
  31. package/dist/src/cli/list-shapes.js +7 -1
  32. package/dist/src/cli/output-schemas.d.ts +1 -1
  33. package/dist/src/cli/output-schemas.js +28 -4
  34. package/dist/src/cli/output.js +7 -0
  35. package/dist/src/cli/project-map-output.d.ts +24 -0
  36. package/dist/src/cli/project-map-output.js +21 -0
  37. package/dist/src/cli/read-shapes.d.ts +14 -0
  38. package/dist/src/cli/read-shapes.js +114 -0
  39. package/dist/src/cli/result-shapes.d.ts +6 -0
  40. package/dist/src/cli/result-shapes.js +235 -0
  41. package/dist/src/cli/version.d.ts +2 -2
  42. package/dist/src/cli/version.js +4 -5
  43. package/dist/src/cli.js +77 -33
  44. package/dist/src/client.d.ts +14 -2
  45. package/dist/src/client.js +18 -10
  46. package/dist/src/generated/action-output-schemas.json +242 -35
  47. package/dist/src/index.d.ts +1 -1
  48. package/dist/src/resources/datasets.d.ts +11 -1
  49. package/dist/src/resources/datasets.js +91 -30
  50. package/dist/src/resources/jobs.d.ts +37 -6
  51. package/dist/src/resources/jobs.js +110 -44
  52. package/dist/src/resources/projects.js +2 -2
  53. package/dist/src/resources/recipes.d.ts +12 -1
  54. package/dist/src/resources/recipes.js +39 -12
  55. package/dist/src/resources/scenarios.d.ts +8 -1
  56. package/dist/src/resources/scenarios.js +51 -12
  57. package/dist/src/resources/wiki.js +3 -7
  58. package/dist/src/skill.js +1 -2
  59. package/dist/src/utils/project-archive.d.ts +25 -0
  60. package/dist/src/utils/project-archive.js +216 -5
  61. package/dist/src/utils/response-file.js +15 -8
  62. package/dist/src/utils/secret-sanitize.d.ts +11 -2
  63. package/dist/src/utils/secret-sanitize.js +23 -6
  64. package/package.json +13 -14
  65. package/plugin.json +1 -1
  66. package/skills/dataiku-dss/SKILL.md +1 -1
  67. package/skills/dataiku-dss/references/discovery.md +2 -2
  68. package/skills/dataiku-dss/references/troubleshooting.md +2 -2
package/dist/src/cli.js CHANGED
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import * as fs from "node:fs/promises";
3
2
  import { homedir, } from "node:os";
4
3
  import { resolve, } from "node:path";
5
- import { num, seenJsonInputCredentials, unknownJsonInput, } from "./cli/coerce.js";
4
+ import { num, resultRecord, seenJsonInputCredentials, stringField, unknownJsonInput, } from "./cli/coerce.js";
6
5
  import { commands, } from "./cli/commands/index.js";
7
6
  import { ambientProjectKey, loadEnvFile, } from "./cli/env.js";
8
7
  import { executionMode, FLAG_ALIASES, isFlagValueToken, parseArgs, rawPositionals, } from "./cli/flags.js";
9
- import { cleanupLedgerEntry, } from "./cli/helpers/cleanup.js";
10
- import { shapeListResult, } from "./cli/list-output.js";
8
+ import { cleanupInputIdentifierField, cleanupLedgerEntry, } from "./cli/helpers/cleanup.js";
11
9
  import { commandFailureExitCode, CommandResultFailure, enqueueCliWarning, flushCliWarnings, isFailedWaitResult, planResult, projectResultFields, setOutputFieldProjection, writeCommandResult, } from "./cli/output.js";
10
+ import { shapeCommandResult, } from "./cli/result-shapes.js";
12
11
  import { currentCommandContext, resolveCredentials, resolveLoginCredentials, } from "./cli/runtime.js";
13
12
  import { commandSyntaxTree, commandUsage, } from "./cli/syntax.js";
14
13
  import { COMMANDS_RUN_HINT, inferRequiresProject, missingActionError, noCommandError, requireArgs, RESOURCE_NAMES, unknownActionError, unknownFlagError, unknownResourceError, UsageError, } from "./cli/usage.js";
@@ -786,7 +785,7 @@ async function commandsRunResult(flags) {
786
785
  : projectResultFields(registry, selectors, () => Object.keys(contract.commandActionSummary()).sort());
787
786
  if (typeof outputPath === "string") {
788
787
  const exported = selectedRegistry;
789
- await fs.writeFile(outputPath, `${JSON.stringify(exported)}\n`, "utf-8");
788
+ await Bun.write(outputPath, `${JSON.stringify(exported)}\n`);
790
789
  return { path: outputPath, };
791
790
  }
792
791
  return selectedRegistry;
@@ -802,8 +801,9 @@ function assertCleanupLedgerSupported(resource, action, flags) {
802
801
  if (!contract.supportsCleanupLedger(resource, action)) {
803
802
  throw new UsageError(`--record-cleanup is not supported for ${resource} ${action}.`);
804
803
  }
805
- if (resource === "workspace" && action === "create" && flags["stdin"] === true) {
806
- throw new UsageError("workspace create cannot combine --record-cleanup with --stdin because the cleanup identifier cannot be replayed from a consumed stream.", "conflicting_input_sources", "Use --data or --data-file so the workspaceKey remains available for the cleanup ledger.");
804
+ const pinnedField = cleanupInputIdentifierField(resource, action);
805
+ if (pinnedField !== undefined && flags["stdin"] === true) {
806
+ throw new UsageError(`${resource} ${action} cannot combine --record-cleanup with --stdin because the cleanup identifier cannot be replayed from a consumed stream.`, "conflicting_input_sources", `Use --data or --data-file so the ${pinnedField} remains available for the cleanup ledger.`);
807
807
  }
808
808
  }
809
809
  /**
@@ -856,11 +856,36 @@ async function preflightCleanupLedgerForFlags(client, flags) {
856
856
  }
857
857
  }
858
858
  /**
859
- * Append the cleanup entry for a completed handler result. Callers must pass the
860
- * raw, unprojected result and must call this before any failure exit code is
861
- * raised: an indeterminate post-POST outcome stays addressable only when the
862
- * ledger records it first. Eligibility (dry runs, skipped work, an explicit
863
- * `cleanupEligible:false`) is decided by `cleanupLedgerEntry`, never here.
859
+ * What the command result already says about the created object, so a failed
860
+ * ledger write never hides it: the `created`/`saved` receipt, the object `id`
861
+ * and `name`, and the project key DSS landed it in. Fields stay distinct (a
862
+ * name is not an id) and absent ones are omitted rather than guessed.
863
+ */
864
+ function createdObjectFromResult(result, fallbackProjectKey) {
865
+ const record = resultRecord(result);
866
+ const created = stringField(record, ["created", "saved", "usedProjectKey",]);
867
+ const id = stringField(record, ["id",]);
868
+ const name = stringField(record, ["name",]);
869
+ const projectKey = stringField(record, ["projectKey", "usedProjectKey", "targetProjectKey",])
870
+ ?? fallbackProjectKey;
871
+ return {
872
+ ...(created ? { created, } : {}),
873
+ ...(id ? { id, } : {}),
874
+ ...(name ? { name, } : {}),
875
+ ...(projectKey ? { projectKey, } : {}),
876
+ };
877
+ }
878
+ /**
879
+ * Append the cleanup entry for a completed handler result. Callers must call
880
+ * this before any failure exit code is raised: an indeterminate post-POST
881
+ * outcome stays addressable only when the ledger records it first. Eligibility
882
+ * (dry runs, skipped work, an explicit `cleanupEligible:false`) is decided by
883
+ * `cleanupLedgerEntry`, never here.
884
+ *
885
+ * The mutation has already happened when this runs, so every failure here is
886
+ * an `ambiguous_outcome` that says so (`mutationSucceeded: true`,
887
+ * `cleanupRecorded: false`) and carries the created identifier/project key:
888
+ * callers must never retry the create, only clean up or record it themselves.
864
889
  */
865
890
  async function recordCleanupLedgerEntry(client, resource, action, args, flags, result, projectKey) {
866
891
  const ledgerPath = flags["record-cleanup"];
@@ -868,15 +893,33 @@ async function recordCleanupLedgerEntry(client, resource, action, args, flags, r
868
893
  return;
869
894
  const entry = cleanupLedgerEntry(resource, action, args, flags, result, projectKey);
870
895
  if (entry) {
871
- await appendCleanupLedgerEntry(ledgerPath, entry, client.getBaseUrl());
896
+ try {
897
+ await appendCleanupLedgerEntry(ledgerPath, entry, client.getBaseUrl());
898
+ }
899
+ catch (error) {
900
+ throw new ClientValidationError(`Mutation completed but the cleanup entry for ${resource} ${action} could not be recorded: ${error instanceof Error ? error.message : String(error)}`, "ambiguous_outcome", "The mutation already succeeded: do not retry it. Remove the created object manually or append this cleanup entry to the ledger yourself.", {
901
+ resource,
902
+ action,
903
+ ledgerPath,
904
+ mutationSucceeded: true,
905
+ cleanupRecorded: false,
906
+ ...createdObjectFromResult(result, projectKey),
907
+ cleanup: entry.cleanup,
908
+ });
909
+ }
872
910
  return;
873
911
  }
874
- const record = result !== null && typeof result === "object" && !Array.isArray(result)
875
- ? result
876
- : {};
912
+ const record = resultRecord(result);
877
913
  if (record.skipped !== undefined || record.cleanupEligible === false)
878
914
  return;
879
- throw new ClientValidationError(`Mutation completed but no cleanup entry could be derived for ${resource} ${action}.`, "ambiguous_outcome", "Inspect the created object and remove it manually; do not assume the requested cleanup ledger contains it.", { resource, action, ledgerPath, });
915
+ throw new ClientValidationError(`Mutation completed but no cleanup entry could be derived for ${resource} ${action}.`, "ambiguous_outcome", "The mutation already succeeded: do not retry it. Inspect the created object and remove it manually; the requested cleanup ledger does not contain it.", {
916
+ resource,
917
+ action,
918
+ ledgerPath,
919
+ mutationSucceeded: true,
920
+ cleanupRecorded: false,
921
+ ...createdObjectFromResult(result, projectKey),
922
+ });
880
923
  }
881
924
  async function runBatch(flags) {
882
925
  const payload = unknownJsonInput(flags);
@@ -1022,8 +1065,7 @@ async function runBatch(flags) {
1022
1065
  await preflightCleanupLedgerForFlags(client, stepFlags);
1023
1066
  const stepArgs = positional.slice(2);
1024
1067
  result = await meta.handler(client, stepArgs, stepFlags);
1025
- if (action === "list")
1026
- result = shapeListResult(resource, result, stepFlags);
1068
+ result = shapeCommandResult(resource, action, result, stepFlags);
1027
1069
  await recordCleanupLedgerEntry(client, resource, action, stepArgs, stepFlags, result, projectKey);
1028
1070
  }
1029
1071
  const failureExitCode = commandFailureExitCode(result);
@@ -1301,27 +1343,33 @@ function errorExitCode(err) {
1301
1343
  }
1302
1344
  function buildErrorReport(err, contextOverride) {
1303
1345
  const context = contextOverride ?? rawCommandContext();
1346
+ // The envelope echoes only the resolved project; resource/action are known to the caller.
1347
+ const scope = context.projectKey ? { projectKey: context.projectKey, } : {};
1304
1348
  const exitCode = errorExitCode(err);
1305
1349
  if (err instanceof ClientValidationError
1306
1350
  && (err.code === "ambiguous_outcome" || err.code === "assertion_failed"
1307
1351
  || err.code === "long_running_failure")) {
1352
+ // The DSS response behind an indeterminate outcome stays visible (status,
1353
+ // sanitized message), e.g. a 500 that names the conflicting project key.
1354
+ const cause = err.cause instanceof DataikuError ? err.cause : undefined;
1355
+ const diagnostics = cause ? dssDiagnosticDetails(cause) : {};
1356
+ const details = { ...diagnostics, ...err.details, };
1308
1357
  return {
1309
1358
  type: "error",
1310
- ok: false,
1311
1359
  error: err.message,
1312
1360
  code: err.code,
1313
1361
  category: "dss",
1314
1362
  exitCode,
1363
+ ...(cause ? { status: cause.status, } : {}),
1315
1364
  retryable: false,
1316
1365
  ...(err.hint ? { hint: err.hint, } : {}),
1317
- ...(err.details ? { details: err.details, } : {}),
1318
- ...context,
1366
+ ...(Object.keys(details).length > 0 ? { details, } : {}),
1367
+ ...scope,
1319
1368
  };
1320
1369
  }
1321
1370
  if (err instanceof UsageError || err instanceof ClientValidationError) {
1322
1371
  return {
1323
1372
  type: "error",
1324
- ok: false,
1325
1373
  error: err.message,
1326
1374
  code: err.code,
1327
1375
  category: err.code === "target_absence_unverifiable"
@@ -1333,26 +1381,24 @@ function buildErrorReport(err, contextOverride) {
1333
1381
  ...(err.code === "target_absence_unverifiable" ? { retryable: false, } : {}),
1334
1382
  ...(err.hint ? { hint: err.hint, } : {}),
1335
1383
  ...(err.details ? { details: err.details, } : {}),
1336
- ...context,
1384
+ ...scope,
1337
1385
  };
1338
1386
  }
1339
1387
  if (err instanceof CommandResultFailure) {
1340
1388
  return {
1341
1389
  type: "error",
1342
- ok: false,
1343
1390
  error: err.message,
1344
1391
  code: err.code,
1345
1392
  category: "dss",
1346
1393
  exitCode: err.exitCode,
1347
1394
  details: boundedFailureResult(err.result),
1348
- ...context,
1395
+ ...scope,
1349
1396
  };
1350
1397
  }
1351
1398
  if (isAmbiguousMutationFailure(err, context)) {
1352
1399
  const method = err.retry?.method.toUpperCase() ?? "mutation";
1353
1400
  return {
1354
1401
  type: "error",
1355
- ok: false,
1356
1402
  error: `The ${method} request failed after dispatch; the mutation outcome is unknown.`,
1357
1403
  code: "ambiguous_outcome",
1358
1404
  category: "dss",
@@ -1369,7 +1415,7 @@ function buildErrorReport(err, contextOverride) {
1369
1415
  ...err.details,
1370
1416
  ...(err.retry ? { retry: err.retry, } : {}),
1371
1417
  },
1372
- ...context,
1418
+ ...scope,
1373
1419
  };
1374
1420
  }
1375
1421
  if (err instanceof DataikuError) {
@@ -1391,7 +1437,6 @@ function buildErrorReport(err, contextOverride) {
1391
1437
  : err.retryHint;
1392
1438
  return {
1393
1439
  type: "error",
1394
- ok: false,
1395
1440
  error: dssMessage ? `${err.safeMessage}: ${dssMessage}` : err.safeMessage,
1396
1441
  code: dataikuErrorCode(err.category),
1397
1442
  category: "dss",
@@ -1401,18 +1446,17 @@ function buildErrorReport(err, contextOverride) {
1401
1446
  retryable: err.retryable,
1402
1447
  requestId: err.requestId ?? requestIdFromBody(err.body),
1403
1448
  ...(Object.keys(details).length > 0 ? { details, } : {}),
1404
- ...context,
1449
+ ...scope,
1405
1450
  };
1406
1451
  }
1407
1452
  const message = err instanceof Error ? err.message : String(err);
1408
1453
  return {
1409
1454
  type: "error",
1410
- ok: false,
1411
1455
  error: message,
1412
1456
  code: "internal_error",
1413
1457
  category: "internal",
1414
1458
  exitCode,
1415
- ...context,
1459
+ ...scope,
1416
1460
  };
1417
1461
  }
1418
1462
  function writeErrorReport(err) {
@@ -1524,7 +1568,7 @@ async function main() {
1524
1568
  assertCleanupLedgerSupported(resource, action, flags);
1525
1569
  await preflightCleanupLedgerForFlags(client, flags);
1526
1570
  const handled = await actionMeta.handler(client, args, flags);
1527
- const result = action === "list" ? shapeListResult(resource, handled, flags) : handled;
1571
+ const result = shapeCommandResult(resource, action, handled, flags);
1528
1572
  await recordCleanupLedgerEntry(client, resource, action, args, flags, result, projectKey);
1529
1573
  const failureExitCode = commandFailureExitCode(result);
1530
1574
  if (failureExitCode !== undefined)
@@ -153,7 +153,13 @@ export declare class DataikuClient {
153
153
  private readonly maxResponseBodyBytes;
154
154
  private readonly retryMaxAttempts;
155
155
  private readonly verbose;
156
- private readonly tlsOptions;
156
+ /**
157
+ * Connection pool carrying this client's TLS settings (custom CA, disabled
158
+ * verification). A session never shares connections with plain `fetch` or
159
+ * another client, so a connection opened with relaxed TLS cannot serve a
160
+ * client that expects strict verification. Default-TLS clients use plain fetch.
161
+ */
162
+ private readonly session;
157
163
  private readonly onTrace;
158
164
  private readonly onValidationWarning;
159
165
  private projectsResource?;
@@ -294,7 +300,8 @@ export declare class DataikuClient {
294
300
  * When the response exceeds the cap, earlier bytes are discarded, only
295
301
  * the tail is kept and `truncated` is true — so callers that need the end
296
302
  * of a large document (e.g. job logs) stay bounded without losing the most
297
- * recent content. The read runs under the request deadline.
303
+ * recent content. The request timeout applies per received chunk (a stall
304
+ * deadline), not to the whole transfer.
298
305
  */
299
306
  getTextTailLimited(path: string, maxBytes: number): Promise<{
300
307
  text: string;
@@ -385,6 +392,11 @@ export declare class DataikuClient {
385
392
  * Returns `truncated: true` when bytes had to be dropped. Bytes that fall
386
393
  * in the middle of a UTF-8 character are dropped from both ends so the
387
394
  * decoded text never contains a replacement character.
395
+ *
396
+ * The request timeout applies per chunk (a stall deadline), like the
397
+ * streaming path: a large body that keeps arriving is read to EOF however
398
+ * long the whole transfer takes, while a body that stops sending for longer
399
+ * than the timeout fails with the body-read timeout error.
388
400
  */
389
401
  private readBodyTextTail;
390
402
  private buildResponseTooLargeError;
@@ -211,7 +211,13 @@ export class DataikuClient {
211
211
  maxResponseBodyBytes;
212
212
  retryMaxAttempts;
213
213
  verbose;
214
- tlsOptions;
214
+ /**
215
+ * Connection pool carrying this client's TLS settings (custom CA, disabled
216
+ * verification). A session never shares connections with plain `fetch` or
217
+ * another client, so a connection opened with relaxed TLS cannot serve a
218
+ * client that expects strict verification. Default-TLS clients use plain fetch.
219
+ */
220
+ session;
215
221
  onTrace;
216
222
  onValidationWarning;
217
223
  /* Resource namespaces — lazily initialized to break circular imports */
@@ -606,7 +612,8 @@ export class DataikuClient {
606
612
  const rawMax = config?.retryMaxAttempts ?? DEFAULT_RETRY_MAX_ATTEMPTS;
607
613
  this.retryMaxAttempts = Math.min(Math.max(1, rawMax), MAX_RETRY_ATTEMPTS_CAP);
608
614
  this.verbose = config?.verbose === true;
609
- this.tlsOptions = config ? buildFetchTlsOptions(config) : undefined;
615
+ const tls = config ? buildFetchTlsOptions(config) : undefined;
616
+ this.session = tls ? new Bun.FetchSession({ tls, }) : undefined;
610
617
  this.onTrace = config?.onTrace ?? defaultTrace;
611
618
  this.onValidationWarning = config?.onValidationWarning ?? defaultValidationWarning;
612
619
  }
@@ -691,7 +698,8 @@ export class DataikuClient {
691
698
  * When the response exceeds the cap, earlier bytes are discarded, only
692
699
  * the tail is kept and `truncated` is true — so callers that need the end
693
700
  * of a large document (e.g. job logs) stay bounded without losing the most
694
- * recent content. The read runs under the request deadline.
701
+ * recent content. The request timeout applies per received chunk (a stall
702
+ * deadline), not to the whole transfer.
695
703
  */
696
704
  async getTextTailLimited(path, maxBytes) {
697
705
  const limit = Math.max(0, Math.floor(maxBytes));
@@ -1080,6 +1088,11 @@ export class DataikuClient {
1080
1088
  * Returns `truncated: true` when bytes had to be dropped. Bytes that fall
1081
1089
  * in the middle of a UTF-8 character are dropped from both ends so the
1082
1090
  * decoded text never contains a replacement character.
1091
+ *
1092
+ * The request timeout applies per chunk (a stall deadline), like the
1093
+ * streaming path: a large body that keeps arriving is read to EOF however
1094
+ * long the whole transfer takes, while a body that stops sending for longer
1095
+ * than the timeout fails with the body-read timeout error.
1083
1096
  */
1084
1097
  async readBodyTextTail(res, maxBytes) {
1085
1098
  const limit = Math.max(0, Math.floor(maxBytes));
@@ -1090,13 +1103,9 @@ export class DataikuClient {
1090
1103
  let writeOffset = 0;
1091
1104
  let retainedBytes = 0;
1092
1105
  let truncated = false;
1093
- const startedAt = Date.now();
1094
1106
  try {
1095
1107
  while (true) {
1096
- const remainingMs = this.requestTimeoutMs - (Date.now() - startedAt);
1097
- if (remainingMs <= 0)
1098
- throw buildBodyReadTimeoutError(this.requestTimeoutMs);
1099
- const { done, value, } = await readChunkWithDeadline(reader, remainingMs, this.requestTimeoutMs);
1108
+ const { done, value, } = await readChunkWithDeadline(reader, this.requestTimeoutMs, this.requestTimeoutMs);
1100
1109
  if (done)
1101
1110
  break;
1102
1111
  if (limit === 0) {
@@ -1187,9 +1196,8 @@ export class DataikuClient {
1187
1196
  ...init,
1188
1197
  method,
1189
1198
  signal: controller.signal,
1199
+ ...(this.session ? { session: this.session, } : {}),
1190
1200
  };
1191
- if (this.tlsOptions)
1192
- requestInit.tls = this.tlsOptions;
1193
1201
  const res = await fetch(url, requestInit);
1194
1202
  this.logTrace({
1195
1203
  phase: "response",