dataiku-sdk 3.2.0 → 3.3.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 (120) hide show
  1. package/README.md +149 -46
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +102 -0
  4. package/dist/packages/types/src/index.js +76 -0
  5. package/dist/src/cli/commands/app.js +57 -15
  6. package/dist/src/cli/commands/bundle.js +1 -1
  7. package/dist/src/cli/commands/code-env.js +13 -7
  8. package/dist/src/cli/commands/connection.d.ts +9 -0
  9. package/dist/src/cli/commands/connection.js +267 -1
  10. package/dist/src/cli/commands/data-collection.d.ts +2 -0
  11. package/dist/src/cli/commands/data-collection.js +164 -0
  12. package/dist/src/cli/commands/dataset.js +161 -32
  13. package/dist/src/cli/commands/future.js +2 -2
  14. package/dist/src/cli/commands/group.d.ts +2 -0
  15. package/dist/src/cli/commands/group.js +109 -0
  16. package/dist/src/cli/commands/index.js +151 -78
  17. package/dist/src/cli/commands/knowledge-bank.d.ts +2 -0
  18. package/dist/src/cli/commands/knowledge-bank.js +80 -0
  19. package/dist/src/cli/commands/llm.d.ts +2 -0
  20. package/dist/src/cli/commands/llm.js +93 -0
  21. package/dist/src/cli/commands/macro.d.ts +2 -0
  22. package/dist/src/cli/commands/macro.js +221 -0
  23. package/dist/src/cli/commands/ml-task.js +18 -6
  24. package/dist/src/cli/commands/notebook.js +6 -2
  25. package/dist/src/cli/commands/plugin.d.ts +2 -0
  26. package/dist/src/cli/commands/plugin.js +799 -0
  27. package/dist/src/cli/commands/project-folder.d.ts +2 -0
  28. package/dist/src/cli/commands/project-folder.js +197 -0
  29. package/dist/src/cli/commands/project-git.js +14 -54
  30. package/dist/src/cli/commands/project.js +52 -6
  31. package/dist/src/cli/commands/recipe.js +36 -1
  32. package/dist/src/cli/commands/saved-model.d.ts +5 -0
  33. package/dist/src/cli/commands/saved-model.js +388 -1
  34. package/dist/src/cli/commands/scenario.js +138 -1
  35. package/dist/src/cli/commands/user.d.ts +8 -0
  36. package/dist/src/cli/commands/user.js +296 -0
  37. package/dist/src/cli/contract.d.ts +1 -2
  38. package/dist/src/cli/contract.js +1089 -103
  39. package/dist/src/cli/flags.js +30 -1
  40. package/dist/src/cli/helpers/app-successor.d.ts +19 -4
  41. package/dist/src/cli/helpers/app-successor.js +90 -30
  42. package/dist/src/cli/helpers/cleanup.js +3 -1
  43. package/dist/src/cli/helpers/flow-zone.js +1 -2
  44. package/dist/src/cli/output.d.ts +1 -1
  45. package/dist/src/cli/output.js +23 -10
  46. package/dist/src/cli/usage.d.ts +1 -0
  47. package/dist/src/cli/usage.js +47 -1
  48. package/dist/src/cli.js +147 -41
  49. package/dist/src/client.d.ts +83 -5
  50. package/dist/src/client.js +185 -29
  51. package/dist/src/errors.d.ts +3 -0
  52. package/dist/src/errors.js +2 -0
  53. package/dist/src/index.d.ts +16 -7
  54. package/dist/src/index.js +11 -2
  55. package/dist/src/resources/analyses.js +1 -8
  56. package/dist/src/resources/applications.d.ts +8 -6
  57. package/dist/src/resources/applications.js +21 -21
  58. package/dist/src/resources/base.d.ts +4 -0
  59. package/dist/src/resources/base.js +15 -0
  60. package/dist/src/resources/bundles.d.ts +9 -4
  61. package/dist/src/resources/bundles.js +10 -5
  62. package/dist/src/resources/connections.d.ts +78 -1
  63. package/dist/src/resources/connections.js +132 -1
  64. package/dist/src/resources/data-collections.d.ts +118 -0
  65. package/dist/src/resources/data-collections.js +49 -0
  66. package/dist/src/resources/datasets.d.ts +69 -0
  67. package/dist/src/resources/datasets.js +99 -1
  68. package/dist/src/resources/futures.d.ts +4 -1
  69. package/dist/src/resources/futures.js +38 -6
  70. package/dist/src/resources/groups.d.ts +47 -0
  71. package/dist/src/resources/groups.js +56 -0
  72. package/dist/src/resources/jobs.d.ts +8 -13
  73. package/dist/src/resources/jobs.js +41 -16
  74. package/dist/src/resources/knowledge-banks.d.ts +58 -0
  75. package/dist/src/resources/knowledge-banks.js +61 -0
  76. package/dist/src/resources/llms.d.ts +92 -0
  77. package/dist/src/resources/llms.js +62 -0
  78. package/dist/src/resources/macros.d.ts +121 -0
  79. package/dist/src/resources/macros.js +231 -0
  80. package/dist/src/resources/ml-tasks.d.ts +37 -2
  81. package/dist/src/resources/ml-tasks.js +100 -20
  82. package/dist/src/resources/model-evaluation-stores.js +1 -8
  83. package/dist/src/resources/notebooks.d.ts +43 -7
  84. package/dist/src/resources/notebooks.js +53 -13
  85. package/dist/src/resources/plugins.d.ts +265 -0
  86. package/dist/src/resources/plugins.js +458 -0
  87. package/dist/src/resources/project-folders.d.ts +71 -0
  88. package/dist/src/resources/project-folders.js +57 -0
  89. package/dist/src/resources/project-git.d.ts +6 -0
  90. package/dist/src/resources/project-git.js +69 -42
  91. package/dist/src/resources/project-library.d.ts +0 -1
  92. package/dist/src/resources/project-library.js +1 -17
  93. package/dist/src/resources/projects.d.ts +19 -1
  94. package/dist/src/resources/projects.js +27 -1
  95. package/dist/src/resources/recipes.d.ts +15 -1
  96. package/dist/src/resources/recipes.js +73 -6
  97. package/dist/src/resources/saved-models.d.ts +134 -1
  98. package/dist/src/resources/saved-models.js +214 -6
  99. package/dist/src/resources/scenarios.d.ts +105 -0
  100. package/dist/src/resources/scenarios.js +232 -106
  101. package/dist/src/resources/sql.js +5 -1
  102. package/dist/src/resources/users.d.ts +129 -0
  103. package/dist/src/resources/users.js +128 -0
  104. package/dist/src/resources/webapps.d.ts +15 -2
  105. package/dist/src/resources/webapps.js +18 -4
  106. package/dist/src/schemas.d.ts +2 -2
  107. package/dist/src/schemas.js +1 -1
  108. package/dist/src/utils/deep-merge.d.ts +1 -1
  109. package/dist/src/utils/polling.d.ts +20 -0
  110. package/dist/src/utils/polling.js +27 -0
  111. package/dist/src/utils/response-file.js +1 -1
  112. package/dist/src/utils/secret-sanitize.d.ts +54 -0
  113. package/dist/src/utils/secret-sanitize.js +101 -0
  114. package/package.json +6 -5
  115. package/packages/types/dist/index.d.ts +102 -0
  116. package/packages/types/dist/index.js +76 -0
  117. package/plugin.json +1 -1
  118. package/skills/dataiku-dss/SKILL.md +7 -5
  119. package/skills/dataiku-dss/references/app-releases.md +8 -7
  120. package/skills/dataiku-dss/references/discovery.md +4 -4
@@ -3,18 +3,18 @@ import { projectIncarnationHash, } from "../../utils/project-incarnation.js";
3
3
  import { jsonInput, num, parseBooleanOption, plainRecord, requiredJsonInput, requiredStringFlag, stringField, } from "../coerce.js";
4
4
  import { executionMode, } from "../flags.js";
5
5
  import { appPermissionsHash, appPermissionsVerificationError, assertAppPermissionsSnapshotBinding, buildAppPermissionsSnapshot, diffAppPermissions, readAppPermissionsSnapshot, writeAppPermissionsSnapshot, } from "../helpers/app-permissions.js";
6
- import { createSuccessorInstance, futureTargetIdentity, isDefinitiveRejection, preflightSuccessorInstance, requireAbsentProjectTarget, safeErrorSummary, verifyAppInstance, VISUAL_UI_GATE, } from "../helpers/app-successor.js";
6
+ import { createSuccessorInstance, futureTargetIdentity, generatedInstanceKey, isDefinitiveRejection, preflightSuccessorInstance, requireAbsentProjectTarget, safeErrorSummary, verifyAppInstance, VISUAL_UI_GATE, } from "../helpers/app-successor.js";
7
7
  import { CommandResultFailure, } from "../output.js";
8
8
  import { requireArgs, requireNoArgs, UsageError, } from "../usage.js";
9
9
  const CREATE_INSTANCE_USAGE = "dss app create-instance <appId> (--data JSON|--data-file PATH|--stdin) [--wait] [--timeout MS] [--poll-interval MS]";
10
10
  const MANIFEST_VERSION_USAGE = "dss app manifest-version [--project-key KEY]";
11
11
  const SET_MANIFEST_VERSION_USAGE = "dss app set-manifest-version (--manifest-version V|--version-notes NOTES) [--expect-hash SHA256] [--dry-run] [--project-key KEY]";
12
- const CREATE_SUCCESSOR_USAGE = "dss app create-successor-instance <appId> --from KEY --to KEY [--name NAME] [--copy-permissions] [--timeout MS] [--poll-interval MS] [--dry-run] [--record-cleanup PATH]";
13
- const SUCCESSOR_PREFLIGHT_USAGE = "dss app successor-preflight <appId> --from KEY --to KEY [--name NAME] [--copy-permissions]";
12
+ const CREATE_SUCCESSOR_USAGE = "dss app create-successor-instance <appId> --from KEY [--to KEY] [--name NAME] [--copy-permissions] [--timeout MS] [--poll-interval MS] [--dry-run] [--record-cleanup PATH]";
13
+ const SUCCESSOR_PREFLIGHT_USAGE = "dss app successor-preflight <appId> --from KEY [--to KEY] [--name NAME] [--copy-permissions]";
14
14
  const VERIFY_INSTANCE_USAGE = "dss app verify-instance <appId> --project-key KEY [--expect-version V]";
15
15
  const DELETE_INSTANCE_USAGE = "dss app delete-instance --project-key KEY [--future-id ID] [--expect-project-incarnation SHA256] [--unconfirmed-creation] [--timeout MS] [--poll-interval MS]";
16
16
  const CREATE_INSTANCE_INDETERMINATE_REMEDIATION = "DSS may have accepted the creation, but no future ID or project incarnation authorizes cleanup. Inspect `dss app instances <appId>` and DSS task history; delete only after creation is known terminal and the target identity is verified.";
17
- const CREATE_INSTANCE_REJECTED_REMEDIATION = "DSS refused the creation request, so no instance project was created: fix the reported problem and retry.";
17
+ const CREATE_INSTANCE_REJECTED_REMEDIATION = "DSS refused the creation request, so no instance project was created: fix the reported problem and retry. If the target key itself is the problem, rerun create-instance with targetProjectKey omitted to generate a fresh random key (the generated key is a new key, never the rejected one).";
18
18
  const DELETE_INSTANCE_FUTURE_REMEDIATION = "The supplied creation future could not authorize deletion. Inspect that future and the target project, then retry only with a future whose terminal result reports this project.";
19
19
  const DELETE_INSTANCE_UNCONFIRMED_REMEDIATION = "Creation may still be running and DSS returned no future ID. Inspect the app's instances and wait until creation is known terminal before running delete-instance without --unconfirmed-creation.";
20
20
  const DELETE_INSTANCE_DELETE_REMEDIATION = "Future safety checks passed, but the project DELETE outcome is unresolved. Inspect the target project; if it still exists, retry delete-instance.";
@@ -52,6 +52,21 @@ async function assertProjectIncarnationHash(client, projectKey, expected) {
52
52
  return;
53
53
  throw new ClientValidationError(`Project ${projectKey} is not the same project incarnation recorded for this operation.`, "validation_failed", "Refusing to continue after project-key reuse. Re-read the current project and retry only with an artifact captured from that incarnation.", { projectKey, expectedProjectIncarnationHash: expected, currentProjectIncarnationHash: current, });
54
54
  }
55
+ /**
56
+ * Optional caller-chosen successor key. Absent selects generated-key mode: the
57
+ * key is generated exactly once during apply (never during plan, preflight or
58
+ * dry-run) and no absence probe runs. A present-but-empty value is an input
59
+ * error, never a silent fallback to a generated key.
60
+ */
61
+ function optionalSuccessorTarget(flags, usage) {
62
+ const value = flags["to"];
63
+ if (value === undefined)
64
+ return undefined;
65
+ if (typeof value !== "string" || value.trim().length === 0) {
66
+ throw new UsageError("--to must not be empty when supplied; omit --to entirely to create the successor with a generated key.", "validation_failed", `Usage: ${usage}`);
67
+ }
68
+ return value.trim();
69
+ }
55
70
  export const appCommands = {
56
71
  list: {
57
72
  handler: (c) => c.applications.listApps(),
@@ -113,14 +128,36 @@ export const appCommands = {
113
128
  handler: async (c, a, f) => {
114
129
  requireArgs(a, 1, CREATE_INSTANCE_USAGE);
115
130
  const body = requiredJsonInput(f, "--data, --data-file, or --stdin is required (instance creation payload).");
116
- const requestedProjectKey = stringField(body, ["targetProjectKey",])?.trim();
117
- if (!requestedProjectKey) {
118
- throw new UsageError("Instance creation payload must include a non-empty targetProjectKey.", "validation_failed", `Usage: ${CREATE_INSTANCE_USAGE}`);
131
+ let requestedProjectKey;
132
+ if (body["targetProjectKey"] === undefined) {
133
+ // Designer-friendly generated mode, mirroring the official Python
134
+ // client and the successor command's generated mode: one shared
135
+ // generator produces a random APP_ key (122 random bits of
136
+ // UUIDv4 hex, collisions negligible) that is POSTed as the
137
+ // authoritative targetProjectKey. No absent-project probe runs
138
+ // for a generated key, and errors never retry or fall back to
139
+ // an alternate key.
140
+ requestedProjectKey = generatedInstanceKey();
141
+ // Official key-as-name convenience: the display name defaults to
142
+ // the generated key when no targetProjectName is supplied.
143
+ if (body["targetProjectName"] === undefined) {
144
+ body.targetProjectName = requestedProjectKey;
145
+ }
146
+ }
147
+ else {
148
+ // Explicit keys keep the strict caller-chosen-key contract: the
149
+ // payload value must be a non-empty string, and the target must
150
+ // be proven absent before the POST.
151
+ const explicitKey = stringField(body, ["targetProjectKey",])?.trim();
152
+ if (!explicitKey) {
153
+ throw new UsageError("Instance creation payload must include a non-empty targetProjectKey.", "validation_failed", `Usage: ${CREATE_INSTANCE_USAGE}`);
154
+ }
155
+ requestedProjectKey = explicitKey;
156
+ await requireAbsentProjectTarget(c, requestedProjectKey, "targetProjectKey", {
157
+ appId: a[0],
158
+ });
119
159
  }
120
160
  body.targetProjectKey = requestedProjectKey;
121
- await requireAbsentProjectTarget(c, requestedProjectKey, "targetProjectKey", {
122
- appId: a[0],
123
- });
124
161
  let created;
125
162
  try {
126
163
  created = await c.applications.createInstance(a[0], body);
@@ -352,38 +389,42 @@ export const appCommands = {
352
389
  };
353
390
  },
354
391
  usage: CREATE_INSTANCE_USAGE,
355
- description: "Create an app instance from a Dataiku App template, optionally waiting for the instance-creation future returned by DSS.",
392
+ description: "Create an app instance from a Dataiku App template, optionally waiting for the creation future. Omit targetProjectKey to generate a random APP_ key (the default display name); explicit keys are trimmed and must be absent.",
356
393
  examples: [
357
394
  'dss app create-instance my-app --data \'{"targetProjectKey":"NEWPROJ"}\'',
358
395
  'dss app create-instance my-app --data \'{"targetProjectKey":"NEWPROJ"}\' --wait --timeout 120000 --poll-interval 2000',
396
+ "dss app create-instance my-app --data '{}' --wait",
359
397
  ],
360
398
  },
361
399
  "successor-preflight": {
362
400
  handler: async (c, a, f) => {
363
401
  requireArgs(a, 1, SUCCESSOR_PREFLIGHT_USAGE);
402
+ const to = optionalSuccessorTarget(f, SUCCESSOR_PREFLIGHT_USAGE);
364
403
  return preflightSuccessorInstance(c, {
365
404
  appId: a[0],
366
405
  from: requiredStringFlag(f, "from", SUCCESSOR_PREFLIGHT_USAGE),
367
- to: requiredStringFlag(f, "to", SUCCESSOR_PREFLIGHT_USAGE),
406
+ ...(to !== undefined ? { to, } : {}),
368
407
  ...(f["name"] !== undefined ? { name: f["name"], } : {}),
369
408
  copyPermissions: parseBooleanOption(f["copy-permissions"], "--copy-permissions") ?? false,
370
409
  dryRun: true,
371
410
  }, SUCCESSOR_PREFLIGHT_USAGE);
372
411
  },
373
412
  usage: SUCCESSOR_PREFLIGHT_USAGE,
374
- description: "Run every read-only successor gate before changing the template version: validate the template, verify the predecessor, prove target absence, and optionally snapshot the predecessor ACL.",
413
+ description: "Run every read-only successor gate before changing the template version: validate the template, verify the predecessor, prove an explicit --to target absent, and optionally snapshot the predecessor ACL. Omit --to to preflight generated-key mode, where no target exists yet and nothing is probed or allocated.",
375
414
  examples: [
376
415
  "dss app successor-preflight my-app --from OLD_INSTANCE --to NEW_INSTANCE",
377
416
  "dss app successor-preflight my-app --from OLD_INSTANCE --to NEW_INSTANCE --copy-permissions",
417
+ "dss app successor-preflight my-app --from OLD_INSTANCE",
378
418
  ],
379
419
  },
380
420
  "create-successor-instance": {
381
421
  handler: async (c, a, f) => {
382
422
  requireArgs(a, 1, CREATE_SUCCESSOR_USAGE);
423
+ const to = optionalSuccessorTarget(f, CREATE_SUCCESSOR_USAGE);
383
424
  return createSuccessorInstance(c, {
384
425
  appId: a[0],
385
426
  from: requiredStringFlag(f, "from", CREATE_SUCCESSOR_USAGE),
386
- to: requiredStringFlag(f, "to", CREATE_SUCCESSOR_USAGE),
427
+ ...(to !== undefined ? { to, } : {}),
387
428
  ...(f["name"] !== undefined ? { name: f["name"], } : {}),
388
429
  copyPermissions: parseBooleanOption(f["copy-permissions"], "--copy-permissions") ?? false,
389
430
  dryRun: executionMode(f).dryRun,
@@ -392,10 +433,11 @@ export const appCommands = {
392
433
  }, CREATE_SUCCESSOR_USAGE);
393
434
  },
394
435
  usage: CREATE_SUCCESSOR_USAGE,
395
- description: "Create a new app instance from the current template version alongside an existing instance. The predecessor is never modified and is retired separately and deliberately; the creation future is always awaited.",
436
+ description: "Create a new app instance from the current template version alongside an existing instance. An explicit --to keeps the strict caller-chosen-key contract (proven absent before the single POST); omit --to to generate the successor key once during apply and let the terminal future name it. The predecessor is never modified and is retired separately and deliberately; the creation future is always awaited.",
396
437
  examples: [
397
438
  "dss app create-successor-instance my-app --from OLD_INSTANCE --to NEW_INSTANCE",
398
439
  'dss app create-successor-instance my-app --from OLD_INSTANCE --to NEW_INSTANCE --name "Release 2" --copy-permissions',
440
+ "dss app create-successor-instance my-app --from OLD_INSTANCE",
399
441
  ],
400
442
  },
401
443
  "instance-manifest": {
@@ -23,7 +23,7 @@ export const bundleCommands = {
23
23
  return { exported: a[0], };
24
24
  },
25
25
  usage: "dss bundle export <bundleId> [--release-notes TEXT] [--evaluate-standards-checks true|false] [--project-key KEY]",
26
- description: "Create (or overwrite) an exported Design-node bundle. --release-notes forwards the documented releaseNotes parameter; --evaluate-standards-checks toggles the documented Project Standards Checks evaluation (default true).",
26
+ description: "Create an exported Design-node bundle. --release-notes forwards the documented releaseNotes parameter; --evaluate-standards-checks toggles the documented Project Standards Checks evaluation (default true; this instance forbids disabling it). DSS refuses re-exporting an existing bundle id — delete-exported first to replace one.",
27
27
  examples: [
28
28
  "dss bundle export v1",
29
29
  "dss bundle export v1 --release-notes='Adds churn model' --evaluate-standards-checks=false",
@@ -42,19 +42,25 @@ function codeEnvParams(flags) {
42
42
  function splitPackageSpec(raw) {
43
43
  return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
44
44
  }
45
+ /**
46
+ * Resolve requested package specs from --file/--packages/--package. An
47
+ * explicit source that resolves to zero specs is a legitimate clear
48
+ * (set-packages replaces specPackageList wholesale); only a call with no
49
+ * package source flag at all is a usage error.
50
+ */
45
51
  function codeEnvPackageList(flags) {
46
- const packages = [];
47
- if (typeof flags["file"] === "string") {
48
- packages.push(...splitPackageSpec(readFileSync(flags["file"], "utf-8")));
49
- }
52
+ const file = flags["file"];
53
+ const packages = typeof file === "string" ? splitPackageSpec(readFileSync(file, "utf-8")) : [];
50
54
  if (typeof flags["packages"] === "string") {
51
55
  packages.push(...splitPackageSpec(flags["packages"]));
52
56
  }
53
57
  if (typeof flags["package"] === "string") {
54
58
  packages.push(...splitPackageSpec(flags["package"]));
55
59
  }
56
- if (packages.length === 0) {
57
- throw new UsageError("--packages, --package, or --file is required. Use newline-separated package specs for version constraints.");
60
+ if (typeof file !== "string"
61
+ && typeof flags["packages"] !== "string"
62
+ && typeof flags["package"] !== "string") {
63
+ throw new UsageError("--packages, --package, or --file is required. Use newline-separated package specs for version constraints; an explicitly empty value (or empty file) clears the requested package list.");
58
64
  }
59
65
  return packages;
60
66
  }
@@ -276,7 +282,7 @@ export const codeEnvCommands = {
276
282
  return c.codeEnvs.setPackages(envLang, a[1], packages, { installCorePackages, expectHash, });
277
283
  },
278
284
  usage: "dss code-env set-packages <lang> <name> (--packages PKGS|--package PKG|--file PATH) [--install-core-packages true|false] [--expect-hash SHA256] [--dry-run]",
279
- description: "Fetch the current definition, merge the requested package specs (and --install-core-packages) into it, then PUT the merged definition, so fields outside the package list are never dropped; --expect-hash refuses the merge when DSS changed since the hash was captured.",
285
+ description: "Fetch the current definition, merge the requested package specs (and --install-core-packages) into it, then PUT the merged definition, so fields outside the package list are never dropped; --expect-hash refuses the merge when DSS changed since the hash was captured. The package list is replaced wholesale, so an explicitly empty --packages value or an empty --file clears the requested packages.",
280
286
  examples: [
281
287
  "dss code-env set-packages PYTHON my_env --packages 'tabulate\\nnameparser' --dry-run",
282
288
  "dss code-env set-packages PYTHON my_env --file requirements.txt",
@@ -1,2 +1,11 @@
1
1
  import type { CommandMeta } from "../types.js";
2
+ export declare function sanitizeConnectionSecrets<T>(value: T, secrets?: string[]): T;
3
+ /** Extract exact secret values (e.g. `params.password`) for error scrubbing. */
4
+ export declare function connectionSecretsFromBody(body: unknown): string[];
5
+ /**
6
+ * DSS validation errors can echo the submitted connection body (including
7
+ * credentials). Scrub error text and body before it reaches stderr or a
8
+ * structured failure.
9
+ */
10
+ export declare function sanitizeConnectionError(error: unknown, secrets: string[]): unknown;
2
11
  export declare const connectionCommands: Record<string, CommandMeta>;
@@ -1,4 +1,81 @@
1
- import { UsageError, } from "../usage.js";
1
+ import { sanitizeErrorSecrets, sanitizeSecrets, } from "../../utils/secret-sanitize.js";
2
+ import { requiredJsonInput, } from "../coerce.js";
3
+ import { executionMode, } from "../flags.js";
4
+ import { readIfExists, skipResult, } from "../output.js";
5
+ import { requireArgs, UsageError, } from "../usage.js";
6
+ /**
7
+ * Keys of a connection payload that carry credentials. CLI plan/error/output
8
+ * layers replace these with `[redacted]`; the SDK resource layer forwards
9
+ * values verbatim to DSS (over TLS) and returns server responses unmodified.
10
+ * Keys are matched after stripping `-`/`_` and lowercasing. The connection
11
+ * policy is deliberately broader than the user/project-git tables.
12
+ */
13
+ const SENSITIVE_CONNECTION_KEYS = {
14
+ accesstoken: true,
15
+ accesskey: true,
16
+ apikey: true,
17
+ apitoken: true,
18
+ apisecret: true,
19
+ authorization: true,
20
+ clientsecret: true,
21
+ credential: true,
22
+ credentials: true,
23
+ password: true,
24
+ privatekey: true,
25
+ refreshtoken: true,
26
+ secret: true,
27
+ secretkey: true,
28
+ sharesecret: true,
29
+ token: true,
30
+ };
31
+ export function sanitizeConnectionSecrets(value, secrets = []) {
32
+ return sanitizeSecrets(value, { sensitiveKeys: SENSITIVE_CONNECTION_KEYS, secrets, });
33
+ }
34
+ /** Extract exact secret values (e.g. `params.password`) for error scrubbing. */
35
+ export function connectionSecretsFromBody(body) {
36
+ if (body === undefined || body === null || typeof body !== "object" || Array.isArray(body)) {
37
+ return [];
38
+ }
39
+ const found = [];
40
+ const visit = (value) => {
41
+ if (Array.isArray(value)) {
42
+ value.forEach(visit);
43
+ return;
44
+ }
45
+ if (value !== null && typeof value === "object") {
46
+ for (const [key, item,] of Object.entries(value)) {
47
+ const normalizedKey = key.replace(/[-_]/g, "").toLowerCase();
48
+ if (SENSITIVE_CONNECTION_KEYS[normalizedKey] === true && typeof item === "string") {
49
+ found.push(item);
50
+ }
51
+ else {
52
+ visit(item);
53
+ }
54
+ }
55
+ }
56
+ };
57
+ visit(body);
58
+ return found;
59
+ }
60
+ /**
61
+ * DSS validation errors can echo the submitted connection body (including
62
+ * credentials). Scrub error text and body before it reaches stderr or a
63
+ * structured failure.
64
+ */
65
+ export function sanitizeConnectionError(error, secrets) {
66
+ return sanitizeErrorSecrets(error, { sensitiveKeys: SENSITIVE_CONNECTION_KEYS, secrets, });
67
+ }
68
+ function requiredConnectionJson(flags, usage) {
69
+ return requiredJsonInput(flags, `--data, --data-file, or --stdin is required (connection definition). Usage: ${usage}`);
70
+ }
71
+ /** Connection identifiers must be non-empty before any request or dry-run preview. */
72
+ function requireConnectionName(value, usage) {
73
+ const trimmed = value?.trim() ?? "";
74
+ if (trimmed.length === 0) {
75
+ throw new UsageError(`connection name must be a non-empty string.\nUsage: ${usage}`, "validation_failed");
76
+ }
77
+ return trimmed;
78
+ }
2
79
  export const connectionCommands = {
3
80
  list: {
4
81
  handler: (c, _a, f) => c.connections.list({
@@ -17,6 +94,118 @@ export const connectionCommands = {
17
94
  description: "List connections with inferred types and metadata.",
18
95
  examples: ["dss connection infer", "dss connection infer --mode rich",],
19
96
  },
97
+ get: {
98
+ handler: async (c, a) => {
99
+ const usage = "dss connection get <name>";
100
+ requireArgs(a, 1, usage);
101
+ const details = await c.connections.adminGet(requireConnectionName(a[0], usage));
102
+ return sanitizeConnectionSecrets(details);
103
+ },
104
+ usage: "dss connection get <name>",
105
+ description: "Get a connection by name (admin). Credential-bearing params (e.g. params.password) are redacted in output.",
106
+ examples: ["dss connection get postgres",],
107
+ },
108
+ create: {
109
+ handler: async (c, _a, f) => {
110
+ const usage = "dss connection create (--data JSON|--data-file PATH|--stdin)";
111
+ const body = requiredConnectionJson(f, usage);
112
+ if (executionMode(f).dryRun) {
113
+ return {
114
+ dryRun: true,
115
+ action: "create",
116
+ resource: "connection",
117
+ connectionName: typeof body.name === "string" ? body.name : undefined,
118
+ method: "POST",
119
+ endpoint: "/public/api/admin/connections",
120
+ payload: sanitizeConnectionSecrets(body),
121
+ };
122
+ }
123
+ try {
124
+ return await c.connections.adminCreate(body);
125
+ }
126
+ catch (error) {
127
+ throw sanitizeConnectionError(error, connectionSecretsFromBody(body));
128
+ }
129
+ },
130
+ usage: "dss connection create (--data JSON|--data-file PATH|--stdin) [--dry-run]",
131
+ description: 'Create a connection (admin). Body is the Connection definition, e.g. {"name":"new-connection","type":"PostgreSQL","params":{"host":"...","user":"...","password":"..."}}. Secrets are never echoed in errors or output.',
132
+ examples: [
133
+ `dss connection create --data '{"name":"new-connection","type":"PostgreSQL","params":{"db":"dbname","user":"myuser","password":"s3cret"}}'`,
134
+ ],
135
+ },
136
+ update: {
137
+ handler: async (c, a, f) => {
138
+ const usage = "dss connection update <name> (--data JSON|--data-file PATH|--stdin) [--dry-run]";
139
+ requireArgs(a, 1, usage);
140
+ const name = requireConnectionName(a[0], usage);
141
+ const body = requiredConnectionJson(f, usage);
142
+ const secrets = connectionSecretsFromBody(body);
143
+ if (executionMode(f).dryRun) {
144
+ return {
145
+ dryRun: true,
146
+ action: "update",
147
+ resource: "connection",
148
+ name,
149
+ method: "PUT",
150
+ endpoint: `/public/api/admin/connections/${encodeURIComponent(name)}`,
151
+ payload: sanitizeConnectionSecrets(body),
152
+ note: "PUT replaces the whole Connection object; the body must come from `dss connection get`. No GET was issued in dry-run.",
153
+ };
154
+ }
155
+ try {
156
+ const result = await c.connections.adminUpdate(name, body);
157
+ return sanitizeConnectionSecrets(result, secrets);
158
+ }
159
+ catch (error) {
160
+ throw sanitizeConnectionError(error, secrets);
161
+ }
162
+ },
163
+ usage: "dss connection update <name> (--data JSON|--data-file PATH|--stdin) [--dry-run]",
164
+ description: "Update a connection (admin). The body MUST be the Connection object obtained from `dss connection get` (PUT semantics); type and name cannot be modified. Use --dry-run to preview the merged result without writing.",
165
+ examples: [
166
+ `dss connection update postgres --data '{"type":"PostgreSQL","name":"postgres","allowWrite":true,"params":{"db":"dbname","user":"myuser","password":"s3cret"}}'`,
167
+ ],
168
+ },
169
+ delete: {
170
+ handler: async (c, a, f) => {
171
+ const usage = "dss connection delete <name> [--if-exists] [--dry-run]";
172
+ requireArgs(a, 1, usage);
173
+ const name = requireConnectionName(a[0], usage);
174
+ if (executionMode(f).dryRun) {
175
+ return {
176
+ dryRun: true,
177
+ action: "delete",
178
+ resource: "connection",
179
+ name,
180
+ method: "DELETE",
181
+ endpoint: `/public/api/admin/connections/${encodeURIComponent(name)}`,
182
+ ...(f["if-exists"] === true
183
+ ? { note: "Existence not checked in dry-run; live --if-exists issues a GET probe first.", }
184
+ : {}),
185
+ };
186
+ }
187
+ if (f["if-exists"] === true) {
188
+ const current = await readIfExists(() => c.connections.adminGet(name));
189
+ if (!current)
190
+ return skipResult("connection", name, "missing");
191
+ }
192
+ return c.connections.adminDelete(name);
193
+ },
194
+ usage: "dss connection delete <name> [--if-exists] [--dry-run]",
195
+ description: "Delete a connection (admin). Destructive: per the DSS docs no check is performed that the connection is not in use by a dataset.",
196
+ examples: ["dss connection delete old-conn", "dss connection delete old-conn --if-exists",],
197
+ },
198
+ test: {
199
+ handler: async (c, a) => {
200
+ const usage = "dss connection test <name>";
201
+ requireArgs(a, 1, usage);
202
+ const result = await c.connections.adminTest(requireConnectionName(a[0], usage));
203
+ return sanitizeConnectionSecrets(result);
204
+ },
205
+ usage: "dss connection test <name>",
206
+ description: "Test whether a connection is available (uses the same GET /connections/{name}/test route as the official Python client; returns connectionOK, errors when testing is unsupported for the type). Credential-bearing fields are redacted in output.",
207
+ examples: ["dss connection test postgres",],
208
+ },
20
209
  schemas: {
21
210
  handler: (c, _a, f) => {
22
211
  const connection = f["connection"];
@@ -51,4 +240,81 @@ export const connectionCommands = {
51
240
  "dss connection tables --connection ATHENA_CONN --schema analytics --project-key MYPROJ",
52
241
  ],
53
242
  },
243
+ "prepare-import": {
244
+ handler: async (c, _a, f) => {
245
+ const usage = "dss connection prepare-import (--data JSON|--data-file PATH|--stdin) [--project-key KEY]";
246
+ const body = requiredJsonInput(f, `--data, --data-file, or --stdin is required (tables-import request). Usage: ${usage}`);
247
+ const keys = body["keys"];
248
+ if (!Array.isArray(keys) || keys.length === 0) {
249
+ throw new UsageError('Body must include a non-empty keys array (e.g. [{"connectionName":"postgres","name":"my_table"}]).', "validation_failed", `Usage: ${usage}`);
250
+ }
251
+ for (const key of keys) {
252
+ const k = key;
253
+ const connectionName = k["connectionName"];
254
+ const name = k["name"];
255
+ if (typeof connectionName !== "string" || connectionName.trim().length === 0
256
+ || typeof name !== "string" || name.trim().length === 0) {
257
+ throw new UsageError('Each key must carry non-empty string connectionName and name (e.g. {"connectionName":"postgres","name":"my_table"}).', "validation_failed", `Usage: ${usage}`);
258
+ }
259
+ }
260
+ if (executionMode(f).dryRun) {
261
+ return {
262
+ dryRun: true,
263
+ action: "prepare-import",
264
+ resource: "connection",
265
+ keyCount: keys.length,
266
+ method: "POST",
267
+ endpoint: `/public/api/projects/${encodeURIComponent(c.resolveProjectKey(f["project-key"]))}/datasets/tables-import/actions/prepare-from-keys`,
268
+ payload: { keys, },
269
+ };
270
+ }
271
+ return c.connections.prepareTablesImport({
272
+ keys: keys,
273
+ projectKey: f["project-key"],
274
+ });
275
+ },
276
+ usage: "dss connection prepare-import (--data JSON|--data-file PATH|--stdin) [--project-key KEY] [--dry-run]",
277
+ description: 'Prepare the import of selected SQL or Hive tables (WRITE_CONF). Body is {"keys":[...]} per the DSS tables-import docs. Returns a DSS future; poll with `dss future wait <jobId>`.',
278
+ examples: [
279
+ `dss connection prepare-import --data '{"keys":[{"connectionName":"postgres","name":"my_table"}]}' --project-key MYPROJ`,
280
+ ],
281
+ },
282
+ "execute-import": {
283
+ handler: async (c, _a, f) => {
284
+ const usage = "dss connection execute-import (--data JSON|--data-file PATH|--stdin) [--project-key KEY]";
285
+ const body = requiredJsonInput(f, `--data, --data-file, or --stdin is required (tables-import request). Usage: ${usage}`);
286
+ const sql = body["sqlImportCandidates"];
287
+ const hive = body["hiveImportCandidates"];
288
+ const hasSql = Array.isArray(sql) && sql.length > 0;
289
+ const hasHive = Array.isArray(hive) && hive.length > 0;
290
+ if (!hasSql && !hasHive) {
291
+ throw new UsageError('Body must include sqlImportCandidates and/or hiveImportCandidates arrays (e.g. {"sqlImportCandidates":[{"connectionName":"pgsql","table":"t","checked":false,"datasetName":"imported","existingDatasetsNames":[]}]}).', "validation_failed", `Usage: ${usage}`);
292
+ }
293
+ if (executionMode(f).dryRun) {
294
+ return {
295
+ dryRun: true,
296
+ action: "execute-import",
297
+ resource: "connection",
298
+ sqlCandidates: hasSql ? sql.length : undefined,
299
+ hiveCandidates: hasHive ? hive.length : undefined,
300
+ method: "POST",
301
+ endpoint: `/public/api/projects/${encodeURIComponent(c.resolveProjectKey(f["project-key"]))}/datasets/tables-import/actions/execute-from-candidates`,
302
+ payload: {
303
+ ...(hasSql ? { sqlImportCandidates: sql, } : {}),
304
+ ...(hasHive ? { hiveImportCandidates: hive, } : {}),
305
+ },
306
+ };
307
+ }
308
+ return c.connections.executeTablesImport({
309
+ sqlImportCandidates: hasSql ? sql : undefined,
310
+ hiveImportCandidates: hasHive ? hive : undefined,
311
+ projectKey: f["project-key"],
312
+ });
313
+ },
314
+ usage: "dss connection execute-import (--data JSON|--data-file PATH|--stdin) [--project-key KEY] [--dry-run]",
315
+ description: "Perform an import from SQL/Hive table candidates (WRITE_CONF). Body carries sqlImportCandidates and/or hiveImportCandidates per the DSS tables-import docs. Returns a DSS future.",
316
+ examples: [
317
+ `dss connection execute-import --data '{"sqlImportCandidates":[{"connectionName":"pgsql","table":"my_table","checked":false,"datasetName":"imported_from_db","existingDatasetsNames":[]}]}' --project-key MYPROJ`,
318
+ ],
319
+ },
54
320
  };
@@ -0,0 +1,2 @@
1
+ import type { CommandMeta } from "../types.js";
2
+ export declare const dataCollectionCommands: Record<string, CommandMeta>;