dataiku-sdk 3.0.0 → 3.2.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 (109) hide show
  1. package/README.md +246 -19
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +39 -4
  4. package/dist/packages/types/src/index.js +31 -2
  5. package/dist/src/auth.js +1 -1
  6. package/dist/src/cli/coerce.js +11 -2
  7. package/dist/src/cli/commands/analysis.js +4 -3
  8. package/dist/src/cli/commands/api-service.js +66 -18
  9. package/dist/src/cli/commands/app.js +4 -3
  10. package/dist/src/cli/commands/bundle.js +51 -16
  11. package/dist/src/cli/commands/code-env.js +219 -57
  12. package/dist/src/cli/commands/code.d.ts +19 -0
  13. package/dist/src/cli/commands/code.js +87 -15
  14. package/dist/src/cli/commands/dashboard.js +7 -6
  15. package/dist/src/cli/commands/data-quality.js +9 -8
  16. package/dist/src/cli/commands/dataset.js +17 -11
  17. package/dist/src/cli/commands/doctor.js +4 -2
  18. package/dist/src/cli/commands/flow-zone.js +42 -13
  19. package/dist/src/cli/commands/folder.js +9 -8
  20. package/dist/src/cli/commands/future.js +2 -1
  21. package/dist/src/cli/commands/insight.js +8 -7
  22. package/dist/src/cli/commands/job.js +4 -3
  23. package/dist/src/cli/commands/meaning.js +5 -4
  24. package/dist/src/cli/commands/ml-task.js +3 -2
  25. package/dist/src/cli/commands/model-evaluation-store.js +4 -3
  26. package/dist/src/cli/commands/notebook.js +82 -50
  27. package/dist/src/cli/commands/project-git.js +8 -5
  28. package/dist/src/cli/commands/project-library.d.ts +21 -0
  29. package/dist/src/cli/commands/project-library.js +128 -56
  30. package/dist/src/cli/commands/project.js +19 -4
  31. package/dist/src/cli/commands/recipe.js +51 -25
  32. package/dist/src/cli/commands/saved-model.js +4 -3
  33. package/dist/src/cli/commands/scenario.js +9 -8
  34. package/dist/src/cli/commands/sql.d.ts +10 -0
  35. package/dist/src/cli/commands/sql.js +42 -19
  36. package/dist/src/cli/commands/statistics.js +7 -6
  37. package/dist/src/cli/commands/variable.js +2 -1
  38. package/dist/src/cli/commands/webapp.js +68 -13
  39. package/dist/src/cli/commands/wiki.js +7 -6
  40. package/dist/src/cli/contract.d.ts +16 -1
  41. package/dist/src/cli/contract.js +967 -186
  42. package/dist/src/cli/env.d.ts +9 -0
  43. package/dist/src/cli/env.js +20 -2
  44. package/dist/src/cli/flags.d.ts +4 -0
  45. package/dist/src/cli/flags.js +29 -1
  46. package/dist/src/cli/helpers/cleanup.js +104 -3
  47. package/dist/src/cli/helpers/flow-zone.d.ts +3 -0
  48. package/dist/src/cli/helpers/flow-zone.js +33 -3
  49. package/dist/src/cli/helpers/recipe.d.ts +20 -0
  50. package/dist/src/cli/helpers/recipe.js +158 -3
  51. package/dist/src/cli/output.d.ts +2 -0
  52. package/dist/src/cli/output.js +33 -6
  53. package/dist/src/cli/runtime.d.ts +35 -1
  54. package/dist/src/cli/runtime.js +125 -18
  55. package/dist/src/cli/types.d.ts +7 -0
  56. package/dist/src/cli.js +226 -95
  57. package/dist/src/client.d.ts +63 -1
  58. package/dist/src/client.js +305 -53
  59. package/dist/src/errors.d.ts +8 -2
  60. package/dist/src/errors.js +42 -0
  61. package/dist/src/index.d.ts +4 -3
  62. package/dist/src/index.js +2 -1
  63. package/dist/src/resources/api-services.d.ts +48 -6
  64. package/dist/src/resources/api-services.js +54 -8
  65. package/dist/src/resources/bundles.d.ts +51 -6
  66. package/dist/src/resources/bundles.js +39 -9
  67. package/dist/src/resources/code-envs.d.ts +25 -3
  68. package/dist/src/resources/code-envs.js +104 -2
  69. package/dist/src/resources/datasets.d.ts +20 -0
  70. package/dist/src/resources/datasets.js +232 -52
  71. package/dist/src/resources/folders.js +2 -1
  72. package/dist/src/resources/jobs.d.ts +35 -10
  73. package/dist/src/resources/jobs.js +92 -17
  74. package/dist/src/resources/notebooks.d.ts +77 -7
  75. package/dist/src/resources/notebooks.js +143 -19
  76. package/dist/src/resources/project-git.d.ts +12 -0
  77. package/dist/src/resources/project-git.js +39 -8
  78. package/dist/src/resources/project-library.d.ts +122 -7
  79. package/dist/src/resources/project-library.js +494 -25
  80. package/dist/src/resources/projects.d.ts +5 -4
  81. package/dist/src/resources/projects.js +21 -19
  82. package/dist/src/resources/scenarios.d.ts +30 -0
  83. package/dist/src/resources/scenarios.js +73 -27
  84. package/dist/src/resources/sql.d.ts +6 -0
  85. package/dist/src/resources/sql.js +23 -3
  86. package/dist/src/resources/webapps.d.ts +35 -4
  87. package/dist/src/resources/webapps.js +54 -5
  88. package/dist/src/schemas.d.ts +2 -2
  89. package/dist/src/schemas.js +1 -1
  90. package/dist/src/skill.d.ts +18 -0
  91. package/dist/src/skill.js +93 -9
  92. package/dist/src/utils/flow-analysis.d.ts +53 -0
  93. package/dist/src/utils/flow-analysis.js +382 -0
  94. package/dist/src/utils/project-archive.d.ts +41 -2
  95. package/dist/src/utils/project-archive.js +159 -16
  96. package/dist/src/utils/response-file.d.ts +2 -0
  97. package/dist/src/utils/response-file.js +34 -0
  98. package/package.json +9 -7
  99. package/packages/types/dist/index.d.ts +39 -4
  100. package/packages/types/dist/index.js +31 -2
  101. package/plugin.json +1 -1
  102. package/skills/dataiku-dss/SKILL.md +44 -197
  103. package/skills/dataiku-dss/references/app-releases.md +27 -0
  104. package/skills/dataiku-dss/references/authentication.md +39 -0
  105. package/skills/dataiku-dss/references/coding.md +8 -0
  106. package/skills/dataiku-dss/references/discovery.md +30 -0
  107. package/skills/dataiku-dss/references/flow-maps.md +12 -0
  108. package/skills/dataiku-dss/references/mutations.md +33 -0
  109. package/skills/dataiku-dss/references/troubleshooting.md +29 -0
@@ -1,18 +1,43 @@
1
1
  import { readFileSync, } from "node:fs";
2
- import { mkdir, writeFile, } from "node:fs/promises";
2
+ import { writeFile, } from "node:fs/promises";
3
3
  import { join, } from "node:path";
4
4
  import { deepMerge, } from "../../utils/deep-merge.js";
5
5
  import { jobLogFilterFromFlag, jsonInput, maxLogLinesFromFlags, normalizeLineEndings, num, recipeInputDatasetsFromFlags, requiredStringFlag, rewritePairsFromFlags, sha256Hex, splitCsvFlag, stableHash, } from "../coerce.js";
6
+ import { executionMode, } from "../flags.js";
6
7
  import { moveCreatedItemsToZone, resolveFlowZoneIdFromFlags, } from "../helpers/flow-zone.js";
7
- import { readRecipeBackup, recipeBackupDocument, recipeBackupPath, recipeCodeEnv, recipeGraph, recipeInputItemRef, recipeRoleInputItems, recipeRunShouldWait, } from "../helpers/recipe.js";
8
+ import { ensureRecipeBackupDir, readRecipeBackup, recipeBackupDocument, recipeBackupPath, recipeCodeEnv, recipeGraph, recipeInputItemRef, recipeRoleInputItems, recipeRunShouldWait, writeRecipeBackup, } from "../helpers/recipe.js";
8
9
  import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
9
10
  import { requireArgs, UsageError, } from "../usage.js";
11
+ /** Read a local recipe file before any DSS request, mapping read failures to usage validation. */
12
+ function readRecipeFile(filePath, flagName) {
13
+ try {
14
+ return readFileSync(filePath, "utf-8");
15
+ }
16
+ catch (error) {
17
+ const reason = error instanceof Error ? error.message : String(error);
18
+ throw new UsageError(`Could not read ${flagName} ${filePath}: ${reason}.`, "not_found", "Verify the file path and that it is readable.");
19
+ }
20
+ }
21
+ /** Read a set-payload backup file before any DSS request, mapping read failures to usage validation. */
22
+ function readRecipeBackupFile(backupPath) {
23
+ try {
24
+ return readRecipeBackup(backupPath);
25
+ }
26
+ catch (error) {
27
+ const reason = error instanceof Error ? error.message : String(error);
28
+ throw new UsageError(`Could not read --backup ${backupPath}: ${reason}.`, "not_found", "Verify the backup file path and that it is readable.");
29
+ }
30
+ }
10
31
  function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
11
- if (localContent === remoteContent) {
32
+ // Line-ending differences are not content differences: compare and split on
33
+ // CRLF-normalized text, mirroring assert-unchanged's normalized payload hash.
34
+ const normalizedLocal = normalizeLineEndings(localContent);
35
+ const normalizedRemote = normalizeLineEndings(remoteContent);
36
+ if (normalizedLocal === normalizedRemote) {
12
37
  return "No differences.";
13
38
  }
14
- const localLines = localContent.split("\n");
15
- const remoteLines = remoteContent.split("\n");
39
+ const localLines = normalizedLocal.split("\n");
40
+ const remoteLines = normalizedRemote.split("\n");
16
41
  const lines = [`--- remote:${remoteName}`, `+++ local:${localPath}`, "",];
17
42
  const maxLen = Math.max(localLines.length, remoteLines.length);
18
43
  for (let i = 0; i < maxLen; i++) {
@@ -85,7 +110,7 @@ export const recipeCommands = {
85
110
  summary: f["summary"] === true,
86
111
  wait,
87
112
  };
88
- if (f["dry-run"] === true) {
113
+ if (executionMode(f).dryRun) {
89
114
  const outputs = await c.recipes.resolveRunOutputs(a[0], {
90
115
  partition: options.partition,
91
116
  projectKey: pk,
@@ -104,7 +129,7 @@ export const recipeCommands = {
104
129
  return c.recipes.run(a[0], options);
105
130
  },
106
131
  usage: "dss recipe run <name> [--wait|--no-wait] [--build-mode MODE] [--include-logs] [--log-filter stdout|stderr|user|errors] [--summary] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--partition PARTITION] [--dry-run] [--project-key KEY]",
107
- description: "Run a recipe by resolving its outputs and submitting the correct dataset or managed-folder build job.",
132
+ description: "Run a recipe by resolving its outputs and submitting the correct dataset or managed-folder build job. --timeout defaults to 120000 ms and --poll-interval to 2000 ms.",
108
133
  examples: [
109
134
  "dss recipe run compute_orders --wait",
110
135
  "dss recipe run compute_exports --include-logs --log-filter stdout --summary --timeout 600000",
@@ -115,11 +140,11 @@ export const recipeCommands = {
115
140
  handler: async (c, a, f) => {
116
141
  requireArgs(a, 1, "dss recipe delete <name>");
117
142
  const pk = f["project-key"];
118
- if (f["dry-run"] === true || f["if-exists"] === true) {
143
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
119
144
  const current = await readIfExists(() => c.recipes.get(a[0], { projectKey: pk, includePayload: true, }));
120
145
  if (!current)
121
146
  return skipResult("recipe", a[0], "missing");
122
- if (f["dry-run"] === true) {
147
+ if (executionMode(f).dryRun) {
123
148
  return { dryRun: true, action: "delete", resource: "recipe", name: a[0], current, };
124
149
  }
125
150
  }
@@ -217,13 +242,13 @@ export const recipeCommands = {
217
242
  const zoneMove = zoneId && name
218
243
  ? [{ objectId: name, objectType: "RECIPE", },]
219
244
  : undefined;
220
- if ((f["if-not-exists"] === true || f["dry-run"] === true) && name) {
245
+ if ((f["if-not-exists"] === true || executionMode(f).dryRun) && name) {
221
246
  const list = await c.recipes.list(pk);
222
247
  const existing = list.find((r) => r.name === name);
223
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
248
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
224
249
  return skipResult("recipe", name, "exists", { current: existing, });
225
250
  }
226
- if (f["dry-run"] === true) {
251
+ if (executionMode(f).dryRun) {
227
252
  return {
228
253
  dryRun: true,
229
254
  action: "create",
@@ -235,7 +260,7 @@ export const recipeCommands = {
235
260
  };
236
261
  }
237
262
  }
238
- if (f["dry-run"] === true) {
263
+ if (executionMode(f).dryRun) {
239
264
  return {
240
265
  dryRun: true,
241
266
  action: "create",
@@ -304,7 +329,7 @@ export const recipeCommands = {
304
329
  throw new UsageError("Cannot reuse --path or --metastore-table for multiple cloned output datasets.", "invalid_enum");
305
330
  }
306
331
  const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
307
- if (f["dry-run"] === true) {
332
+ if (executionMode(f).dryRun) {
308
333
  return {
309
334
  dryRun: true,
310
335
  action: "clone",
@@ -344,6 +369,8 @@ export const recipeCommands = {
344
369
  if (!filePath) {
345
370
  throw new UsageError("--file is required. Usage: dss recipe diff <name> --file PATH");
346
371
  }
372
+ // Read and validate the local file before any DSS request.
373
+ const localContent = readRecipeFile(filePath, "--file");
347
374
  const result = await c.recipes.get(a[0], {
348
375
  includePayload: true,
349
376
  projectKey: f["project-key"],
@@ -351,7 +378,6 @@ export const recipeCommands = {
351
378
  if (!result.payload) {
352
379
  throw new Error(`Recipe "${a[0]}" has no code payload to diff.`);
353
380
  }
354
- const localContent = readFileSync(filePath, "utf-8");
355
381
  return formatLineDiff(a[0], filePath, result.payload, localContent);
356
382
  },
357
383
  usage: "dss recipe diff <name> --file PATH [--project-key KEY]",
@@ -366,7 +392,7 @@ export const recipeCommands = {
366
392
  throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin]");
367
393
  }
368
394
  const pk = f["project-key"];
369
- if (f["dry-run"] === true) {
395
+ if (executionMode(f).dryRun) {
370
396
  const current = await c.recipes.get(a[0], { projectKey: pk, includePayload: true, });
371
397
  const currentRecipe = current.recipe;
372
398
  const next = {
@@ -405,7 +431,7 @@ export const recipeCommands = {
405
431
  }
406
432
  const nextItems = [...items, { ref: a[1], deps: [], },];
407
433
  const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
408
- if (f["dry-run"] === true) {
434
+ if (executionMode(f).dryRun) {
409
435
  return {
410
436
  dryRun: true,
411
437
  action: "add-input",
@@ -442,7 +468,7 @@ export const recipeCommands = {
442
468
  }
443
469
  const nextItems = items.filter((item) => recipeInputItemRef(item) !== a[1]);
444
470
  const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
445
- if (f["dry-run"] === true) {
471
+ if (executionMode(f).dryRun) {
446
472
  return {
447
473
  dryRun: true,
448
474
  action: "remove-input",
@@ -506,7 +532,7 @@ export const recipeCommands = {
506
532
  const filePath = f["file"];
507
533
  if (!filePath)
508
534
  throw new UsageError("--file is required.");
509
- const content = readFileSync(filePath, "utf-8");
535
+ const content = readRecipeFile(filePath, "--file");
510
536
  const pk = f["project-key"];
511
537
  const shouldBackup = f["no-backup"] !== true;
512
538
  const backupDir = shouldBackup
@@ -517,7 +543,7 @@ export const recipeCommands = {
517
543
  projectKey: pk,
518
544
  includePayload: true,
519
545
  });
520
- if (f["dry-run"] === true) {
546
+ if (executionMode(f).dryRun) {
521
547
  return {
522
548
  dryRun: true,
523
549
  action: "set-payload",
@@ -530,8 +556,8 @@ export const recipeCommands = {
530
556
  };
531
557
  }
532
558
  if (backupDir && backupPath) {
533
- await mkdir(backupDir, { recursive: true, });
534
- await writeFile(backupPath, `${JSON.stringify(recipeBackupDocument(a[0], pk, current), null, 2)}\n`, "utf-8");
559
+ ensureRecipeBackupDir(backupDir);
560
+ writeRecipeBackup(backupPath, `${JSON.stringify(recipeBackupDocument(a[0], pk, current), null, 2)}\n`);
535
561
  }
536
562
  await c.recipes.replace(a[0], { ...current, payload: content, }, pk);
537
563
  return {
@@ -555,7 +581,7 @@ export const recipeCommands = {
555
581
  const usage = "dss recipe restore <name> --backup FILE [--payload-only] [--dry-run] [--project-key KEY]";
556
582
  requireArgs(a, 1, usage);
557
583
  const backupPath = requiredStringFlag(f, "backup", usage);
558
- const backup = readRecipeBackup(backupPath);
584
+ const backup = readRecipeBackupFile(backupPath);
559
585
  const payload = typeof backup.payload === "string" ? backup.payload : "";
560
586
  const pk = f["project-key"];
561
587
  const current = await c.recipes.get(a[0], { includePayload: true, projectKey: pk, });
@@ -568,7 +594,7 @@ export const recipeCommands = {
568
594
  const next = f["payload-only"] === true || !restoredRecipe
569
595
  ? { ...current, payload, }
570
596
  : { ...current, recipe: restoredRecipe, payload, };
571
- if (f["dry-run"] === true) {
597
+ if (executionMode(f).dryRun) {
572
598
  return {
573
599
  dryRun: true,
574
600
  action: "restore",
@@ -598,7 +624,7 @@ export const recipeCommands = {
598
624
  const usage = "dss recipe assert-unchanged <name> --since BACKUP [--project-key KEY]";
599
625
  requireArgs(a, 1, usage);
600
626
  const backupPath = requiredStringFlag(f, "since", usage);
601
- const backup = readRecipeBackup(backupPath);
627
+ const backup = readRecipeBackupFile(backupPath);
602
628
  const current = await c.recipes.get(a[0], {
603
629
  includePayload: true,
604
630
  projectKey: f["project-key"],
@@ -1,3 +1,4 @@
1
+ import { executionMode, } from "../flags.js";
1
2
  import { readIfExists, skipResult, } from "../output.js";
2
3
  import { requireArgs, } from "../usage.js";
3
4
  export const savedModelCommands = {
@@ -40,7 +41,7 @@ export const savedModelCommands = {
40
41
  handler: async (c, a, f) => {
41
42
  requireArgs(a, 2, "dss saved-model set-active <modelId> <versionId> [--dry-run] [--project-key KEY]");
42
43
  const projectKey = f["project-key"];
43
- if (f["dry-run"] === true) {
44
+ if (executionMode(f).dryRun) {
44
45
  return {
45
46
  dryRun: true,
46
47
  action: "set-active",
@@ -64,11 +65,11 @@ export const savedModelCommands = {
64
65
  handler: async (c, a, f) => {
65
66
  requireArgs(a, 1, "dss saved-model delete <modelId> [--if-exists] [--dry-run] [--project-key KEY]");
66
67
  const projectKey = f["project-key"];
67
- if (f["dry-run"] === true || f["if-exists"] === true) {
68
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
68
69
  const current = await readIfExists(() => c.savedModels.get(a[0], projectKey));
69
70
  if (!current)
70
71
  return skipResult("saved-model", a[0], "missing");
71
- if (f["dry-run"] === true) {
72
+ if (executionMode(f).dryRun) {
72
73
  return { dryRun: true, action: "delete", resource: "saved-model", id: a[0], current, };
73
74
  }
74
75
  }
@@ -1,5 +1,6 @@
1
1
  import { scenarioUpdatePreview, } from "../../resources/scenarios.js";
2
2
  import { jsonInput, num, } from "../coerce.js";
3
+ import { executionMode, } from "../flags.js";
3
4
  import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
4
5
  import { requireArgs, UsageError, } from "../usage.js";
5
6
  export const scenarioCommands = {
@@ -26,7 +27,7 @@ export const scenarioCommands = {
26
27
  pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
27
28
  timeoutMs: num(f["timeout"], "--timeout"),
28
29
  };
29
- if (f["dry-run"] === true) {
30
+ if (executionMode(f).dryRun) {
30
31
  return {
31
32
  dryRun: true,
32
33
  action: "run",
@@ -54,7 +55,7 @@ export const scenarioCommands = {
54
55
  pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
55
56
  timeoutMs: num(f["timeout"], "--timeout"),
56
57
  };
57
- if (f["dry-run"] === true) {
58
+ if (executionMode(f).dryRun) {
58
59
  return {
59
60
  dryRun: true,
60
61
  action: "run-and-wait",
@@ -87,11 +88,11 @@ export const scenarioCommands = {
87
88
  handler: async (c, a, f) => {
88
89
  requireArgs(a, 1, "dss scenario delete <id>");
89
90
  const pk = f["project-key"];
90
- if (f["dry-run"] === true || f["if-exists"] === true) {
91
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
91
92
  const current = await readIfExists(() => c.scenarios.get(a[0], { projectKey: pk, }));
92
93
  if (!current)
93
94
  return skipResult("scenario", a[0], "missing");
94
- if (f["dry-run"] === true) {
95
+ if (executionMode(f).dryRun) {
95
96
  return { dryRun: true, action: "delete", resource: "scenario", id: a[0], current, };
96
97
  }
97
98
  }
@@ -112,13 +113,13 @@ export const scenarioCommands = {
112
113
  scenarioType: f["type"],
113
114
  projectKey: pk,
114
115
  };
115
- if (f["if-not-exists"] === true || f["dry-run"] === true) {
116
+ if (f["if-not-exists"] === true || executionMode(f).dryRun) {
116
117
  const list = await c.scenarios.list(pk);
117
118
  const existing = list.find((s) => s.id === a[0]);
118
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
119
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
119
120
  return skipResult("scenario", a[0], "exists", { current: existing, });
120
121
  }
121
- if (f["dry-run"] === true) {
122
+ if (executionMode(f).dryRun) {
122
123
  return {
123
124
  dryRun: true,
124
125
  action: "create",
@@ -150,7 +151,7 @@ export const scenarioCommands = {
150
151
  throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
151
152
  }
152
153
  const pk = f["project-key"];
153
- if (f["dry-run"] === true) {
154
+ if (executionMode(f).dryRun) {
154
155
  const current = await c.scenarios.get(a[0], { projectKey: pk, });
155
156
  const preview = scenarioUpdatePreview(current, data);
156
157
  return {
@@ -1,2 +1,12 @@
1
1
  import type { CommandMeta } from "../types.js";
2
+ export declare function resolveSqlInput(args: string[], flags: Record<string, string | boolean>): string;
3
+ export interface SqlQueryInvocation {
4
+ query: string;
5
+ connection?: string;
6
+ datasetFullName?: string;
7
+ database?: string;
8
+ projectKey?: string;
9
+ type: "sql";
10
+ }
11
+ export declare function resolveSqlQueryInvocation(args: string[], flags: Record<string, string | boolean>, projectKeyOverride?: string): SqlQueryInvocation;
2
12
  export declare const sqlCommands: Record<string, CommandMeta>;
@@ -1,7 +1,9 @@
1
1
  import { readFileSync, } from "node:fs";
2
- import { mkdir, writeFile, } from "node:fs/promises";
2
+ import { mkdir, } from "node:fs/promises";
3
3
  import { dirname, resolve, } from "node:path";
4
+ import { writeResponseToFile, } from "../../utils/response-file.js";
4
5
  import { readStdinText, stripUtf8Bom, } from "../coerce.js";
6
+ import { enqueueCliWarning, } from "../output.js";
5
7
  import { UsageError, } from "../usage.js";
6
8
  const SQL_QUERY_USAGE = "dss sql query (SQL | --sql QUERY | --sql-file PATH | --sql - | --stdin) (--connection CONN | --dataset FULL_NAME) [--database DB] [--output PATH|--output-file PATH] [--preview N] [--start-retries N] [--request-timeout MS] [--project-key KEY]";
7
9
  const DEFAULT_SQL_PREVIEW_ROWS = 5;
@@ -34,7 +36,7 @@ function parseSqlStartRetries(value) {
34
36
  }
35
37
  return parsed;
36
38
  }
37
- function resolveSqlInput(args, flags) {
39
+ export function resolveSqlInput(args, flags) {
38
40
  const sources = [];
39
41
  if (typeof flags["sql"] === "string") {
40
42
  sources.push({
@@ -69,37 +71,58 @@ function resolveSqlInput(args, flags) {
69
71
  }
70
72
  return query;
71
73
  }
74
+ export function resolveSqlQueryInvocation(args, flags, projectKeyOverride) {
75
+ const connection = flags["connection"];
76
+ const datasetFullName = flags["dataset"];
77
+ if ((connection ? 1 : 0) + (datasetFullName ? 1 : 0) !== 1) {
78
+ throw new UsageError(`Pass exactly one of --connection or --dataset. Usage: ${SQL_QUERY_USAGE}`);
79
+ }
80
+ return {
81
+ query: resolveSqlInput(args, flags),
82
+ connection,
83
+ datasetFullName,
84
+ database: flags["database"],
85
+ projectKey: projectKeyOverride ?? flags["project-key"],
86
+ type: "sql",
87
+ };
88
+ }
72
89
  export const sqlCommands = {
73
90
  query: {
74
91
  handler: async (c, a, f) => {
75
- const connection = f["connection"];
76
- const datasetFullName = f["dataset"];
77
- if ((connection ? 1 : 0) + (datasetFullName ? 1 : 0) !== 1) {
78
- throw new UsageError(`Pass exactly one of --connection or --dataset. Usage: ${SQL_QUERY_USAGE}`);
79
- }
80
92
  const outputFile = f["output"]
81
93
  ?? f["output-file"];
82
94
  const previewProvided = f["preview"] !== undefined;
83
- if (previewProvided && !outputFile) {
84
- throw new UsageError(`--preview requires --output or --output-file. Usage: ${SQL_QUERY_USAGE}`, "validation_failed");
85
- }
86
95
  const previewCount = previewProvided
87
96
  ? parseSqlPreviewCount(f["preview"])
88
97
  : DEFAULT_SQL_PREVIEW_ROWS;
89
- const query = resolveSqlInput(a, f);
90
98
  const result = await c.sql.query({
91
- query,
92
- connection,
93
- datasetFullName,
94
- database: f["database"],
95
- projectKey: f["project-key"],
99
+ ...resolveSqlQueryInvocation(a, f),
96
100
  retryMaxAttempts: parseSqlStartRetries(f["start-retries"]),
97
101
  });
98
- if (!outputFile)
102
+ if (!outputFile && !previewProvided)
99
103
  return result;
104
+ if (!outputFile) {
105
+ const truncated = result.rows.length > previewCount;
106
+ if (truncated) {
107
+ enqueueCliWarning({
108
+ code: "sql_preview_truncated",
109
+ rowCount: result.rows.length,
110
+ previewRows: previewCount,
111
+ hint: "Use --output PATH to write the full result without placing it on stdout.",
112
+ });
113
+ }
114
+ return {
115
+ queryId: result.queryId,
116
+ schema: result.schema,
117
+ columns: result.columns ?? result.schema,
118
+ rowCount: result.rows.length,
119
+ preview: result.rows.slice(0, previewCount),
120
+ truncated,
121
+ };
122
+ }
100
123
  const outputPath = resolve(outputFile);
101
124
  await mkdir(dirname(outputPath), { recursive: true, });
102
- await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
125
+ await writeResponseToFile(outputPath, new Response(`${JSON.stringify(result, null, 2)}\n`), "preserve");
103
126
  return {
104
127
  queryId: result.queryId,
105
128
  schema: result.schema,
@@ -111,7 +134,7 @@ export const sqlCommands = {
111
134
  };
112
135
  },
113
136
  usage: SQL_QUERY_USAGE,
114
- description: "Run a SQL query against a DSS connection or dataset. --start-retries may execute the SQL more than once; use it only when repetition is safe.",
137
+ description: "Run potentially mutating SQL against a DSS connection or dataset. Use --preview N for bounded stdout or --output PATH for full rows. --start-retries may execute SQL more than once; use it only when repetition is safe.",
115
138
  examples: [
116
139
  "dss sql query 'SELECT * FROM orders LIMIT 10' --connection my_pg",
117
140
  "dss sql query --sql-file query.sql --connection my_pg",
@@ -1,4 +1,5 @@
1
1
  import { requiredJsonInput, } from "../coerce.js";
2
+ import { executionMode, } from "../flags.js";
2
3
  import { encodedProjectEndpoint, planResult, } from "../output.js";
3
4
  import { requireArgs, } from "../usage.js";
4
5
  const STATISTICS_PLAN_EXIT_CODES = { usage: 1, error: 2, transient: 3, };
@@ -51,7 +52,7 @@ export const statisticsCommands = {
51
52
  requireArgs(a, 1, "dss statistics create-worksheet <dataset> (--data JSON|--data-file PATH|--stdin) [--dry-run] [--project-key KEY]");
52
53
  const body = requiredJsonInput(f, "--data, --data-file, or --stdin is required (worksheet definition).");
53
54
  const projectKey = f["project-key"];
54
- if (f["dry-run"] === true) {
55
+ if (executionMode(f).dryRun) {
55
56
  return statisticsPlan("create-worksheet", {
56
57
  method: "POST",
57
58
  endpoint: statisticsWorksheetsEndpoint(c, a[0], projectKey),
@@ -72,7 +73,7 @@ export const statisticsCommands = {
72
73
  requireArgs(a, 2, "dss statistics update-worksheet <dataset> <worksheetId> (--data JSON|--data-file PATH|--stdin) [--dry-run] [--project-key KEY]");
73
74
  const body = requiredJsonInput(f, "--data, --data-file, or --stdin is required (worksheet definition).");
74
75
  const projectKey = f["project-key"];
75
- if (f["dry-run"] === true) {
76
+ if (executionMode(f).dryRun) {
76
77
  return statisticsPlan("update-worksheet", {
77
78
  method: "PUT",
78
79
  endpoint: statisticsWorksheetEndpoint(c, a[0], a[1], projectKey),
@@ -90,7 +91,7 @@ export const statisticsCommands = {
90
91
  handler: async (c, a, f) => {
91
92
  requireArgs(a, 2, "dss statistics delete-worksheet <dataset> <worksheetId> [--dry-run] [--project-key KEY]");
92
93
  const projectKey = f["project-key"];
93
- if (f["dry-run"] === true) {
94
+ if (executionMode(f).dryRun) {
94
95
  return statisticsPlan("delete-worksheet", {
95
96
  method: "DELETE",
96
97
  endpoint: statisticsWorksheetEndpoint(c, a[0], a[1], projectKey),
@@ -108,7 +109,7 @@ export const statisticsCommands = {
108
109
  handler: async (c, a, f) => {
109
110
  requireArgs(a, 2, "dss statistics run-worksheet <dataset> <worksheetId> [--dry-run] [--project-key KEY]");
110
111
  const projectKey = f["project-key"];
111
- if (f["dry-run"] === true) {
112
+ if (executionMode(f).dryRun) {
112
113
  return statisticsPlan("run-worksheet", {
113
114
  method: "POST",
114
115
  endpoint: `${statisticsWorksheetEndpoint(c, a[0], a[1], projectKey)}/actions/run-card`,
@@ -127,7 +128,7 @@ export const statisticsCommands = {
127
128
  requireArgs(a, 2, "dss statistics run-card <dataset> <worksheetId> (--data JSON|--data-file PATH|--stdin) [--dry-run] [--project-key KEY]");
128
129
  const card = requiredJsonInput(f, "--data, --data-file, or --stdin is required (card settings).");
129
130
  const projectKey = f["project-key"];
130
- if (f["dry-run"] === true) {
131
+ if (executionMode(f).dryRun) {
131
132
  return statisticsPlan("run-card", {
132
133
  method: "POST",
133
134
  endpoint: `${statisticsWorksheetEndpoint(c, a[0], a[1], projectKey)}/actions/run-card`,
@@ -147,7 +148,7 @@ export const statisticsCommands = {
147
148
  requireArgs(a, 2, "dss statistics run-computation <dataset> <worksheetId> (--data JSON|--data-file PATH|--stdin) [--dry-run] [--project-key KEY]");
148
149
  const computation = requiredJsonInput(f, "--data, --data-file, or --stdin is required (computation settings).");
149
150
  const projectKey = f["project-key"];
150
- if (f["dry-run"] === true) {
151
+ if (executionMode(f).dryRun) {
151
152
  return statisticsPlan("run-computation", {
152
153
  method: "POST",
153
154
  endpoint: `${statisticsWorksheetEndpoint(c, a[0], a[1], projectKey)}/actions/run-computation`,
@@ -1,4 +1,5 @@
1
1
  import { json, } from "../coerce.js";
2
+ import { executionMode, } from "../flags.js";
2
3
  import { UsageError, } from "../usage.js";
3
4
  export const variableCommands = {
4
5
  get: {
@@ -15,7 +16,7 @@ export const variableCommands = {
15
16
  if (standard === undefined && local === undefined) {
16
17
  throw new UsageError("--standard and/or --local is required.");
17
18
  }
18
- if (f["dry-run"] === true) {
19
+ if (executionMode(f).dryRun) {
19
20
  const current = await c.variables.get(pk);
20
21
  const next = f["replace"] === true
21
22
  ? { standard: standard ?? {}, local: local ?? {}, }
@@ -1,5 +1,19 @@
1
- import { requiredJsonInput, } from "../coerce.js";
1
+ import { ClientValidationError, } from "../../errors.js";
2
+ import { deepMerge, } from "../../utils/deep-merge.js";
3
+ import { stableHash, } from "../../utils/stable-hash.js";
4
+ import { num, requiredJsonInput, } from "../coerce.js";
5
+ import { executionMode, } from "../flags.js";
2
6
  import { requireArgs, } from "../usage.js";
7
+ const UPDATE_SETTINGS_USAGE = "dss webapp update-settings <webappId> (--data JSON|--data-file PATH|--stdin) [--expect-hash SHA256] [--dry-run] [--project-key KEY]";
8
+ const RESTART_USAGE = "dss webapp restart-backend <webappId> [--wait] [--timeout MS] [--poll-interval MS] [--project-key KEY]";
9
+ function validateExpectHash(value) {
10
+ if (value === undefined || value === false)
11
+ return undefined;
12
+ if (typeof value !== "string" || !/^[0-9a-fA-F]{64}$/.test(value)) {
13
+ throw new ClientValidationError("Expected webapp hash must be a 64-character SHA-256 hex digest.", "validation_failed", "Run this update with --dry-run and capture currentHash before retrying.");
14
+ }
15
+ return value;
16
+ }
3
17
  export const webappCommands = {
4
18
  list: {
5
19
  handler: (c, _a, f) => c.webapps.list(f["project-key"]),
@@ -26,14 +40,45 @@ export const webappCommands = {
26
40
  examples: ["dss webapp create --data-file webapp.json",],
27
41
  },
28
42
  "update-settings": {
29
- handler: (c, a, f) => {
30
- requireArgs(a, 1, "dss webapp update-settings <webappId> (--data JSON|--data-file PATH|--stdin) [--project-key KEY]");
43
+ handler: async (c, a, f) => {
44
+ requireArgs(a, 1, UPDATE_SETTINGS_USAGE);
31
45
  const body = requiredJsonInput(f, "--data, --data-file, or --stdin is required (webapp settings).");
32
- return c.webapps.updateSettings(a[0], body, f["project-key"]);
46
+ const projectKey = f["project-key"];
47
+ const expectHash = validateExpectHash(f["expect-hash"]);
48
+ if (executionMode(f).dryRun) {
49
+ const current = await c.webapps.getSettings(a[0], projectKey);
50
+ const currentHash = stableHash(current);
51
+ if (expectHash !== undefined && currentHash !== expectHash.toLowerCase()) {
52
+ throw new ClientValidationError(`The webapp ${JSON.stringify(a[0])} changed since it was read.`, "validation_failed", "Re-read the webapp settings and retry with the current hash value.", {
53
+ id: a[0],
54
+ expectedHash: expectHash.toLowerCase(),
55
+ actualHash: currentHash,
56
+ });
57
+ }
58
+ const next = deepMerge(current, body);
59
+ return {
60
+ dryRun: true,
61
+ action: "update-settings",
62
+ resource: "webapp",
63
+ id: a[0],
64
+ current,
65
+ next,
66
+ currentHash,
67
+ nextHash: stableHash(next),
68
+ ...(expectHash !== undefined
69
+ ? { expectHash: expectHash.toLowerCase(), provenanceVerified: true, }
70
+ : {}),
71
+ };
72
+ }
73
+ const updated = await c.webapps.updateSettings(a[0], body, projectKey, { expectHash, });
74
+ return { updated: a[0], ...updated, };
33
75
  },
34
- usage: "dss webapp update-settings <webappId> (--data JSON|--data-file PATH|--stdin) [--project-key KEY]",
35
- description: "Replace a webapp's settings.",
36
- examples: ["dss webapp update-settings WEBAPP_ID --data-file webapp.json",],
76
+ usage: UPDATE_SETTINGS_USAGE,
77
+ description: "Merge supplied fields into a webapp's settings (GET-merge-PUT) and PUT the full object, so fields outside the patch are never dropped. --expect-hash refuses the write when the object changed since it was fetched; --dry-run reports current, next, and both hashes without writing.",
78
+ examples: [
79
+ "dss webapp update-settings WEBAPP_ID --data-file webapp.json",
80
+ 'dss webapp update-settings WEBAPP_ID --dry-run --data=\'{"params":{"designOptions":{"namingRule":"x"}}}\'',
81
+ ],
37
82
  },
38
83
  "stop-backend": {
39
84
  handler: async (c, a, f) => {
@@ -47,13 +92,23 @@ export const webappCommands = {
47
92
  },
48
93
  "restart-backend": {
49
94
  handler: async (c, a, f) => {
50
- requireArgs(a, 1, "dss webapp restart-backend <webappId> [--project-key KEY]");
51
- await c.webapps.startOrRestartBackend(a[0], f["project-key"]);
52
- return { restarted: true, };
95
+ requireArgs(a, 1, RESTART_USAGE);
96
+ const projectKey = f["project-key"];
97
+ if (f["wait"] === true) {
98
+ return c.webapps.restartBackendAndWait(a[0], projectKey, {
99
+ pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
100
+ timeoutMs: num(f["timeout"], "--timeout"),
101
+ });
102
+ }
103
+ const future = await c.webapps.startOrRestartBackend(a[0], projectKey);
104
+ return { restarted: true, future, };
53
105
  },
54
- usage: "dss webapp restart-backend <webappId> [--project-key KEY]",
55
- description: "Start or restart a webapp's backend.",
56
- examples: ["dss webapp restart-backend WEBAPP_ID",],
106
+ usage: RESTART_USAGE,
107
+ description: "Start or restart a webapp's backend. The documented endpoint returns a restart future; --wait settles it and returns the FutureWaitResult.",
108
+ examples: [
109
+ "dss webapp restart-backend WEBAPP_ID",
110
+ "dss webapp restart-backend WEBAPP_ID --wait --timeout=60000",
111
+ ],
57
112
  },
58
113
  "backend-state": {
59
114
  handler: (c, a, f) => {
@@ -1,5 +1,6 @@
1
1
  import { deepMerge, } from "../../utils/deep-merge.js";
2
2
  import { jsonInput, textInput, } from "../coerce.js";
3
+ import { executionMode, } from "../flags.js";
3
4
  import { readIfExists, skipResult, } from "../output.js";
4
5
  import { requireArgs, UsageError, } from "../usage.js";
5
6
  export const wikiCommands = {
@@ -31,12 +32,12 @@ export const wikiCommands = {
31
32
  throw new UsageError("--name is required. Usage: dss wiki create --name NAME");
32
33
  const content = textInput(f);
33
34
  const pk = f["project-key"];
34
- if (f["if-not-exists"] === true || f["dry-run"] === true) {
35
+ if (f["if-not-exists"] === true || executionMode(f).dryRun) {
35
36
  const existing = (await c.wiki.list(pk)).find((article) => article.article.name === name);
36
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
37
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
37
38
  return skipResult("wiki", existing.article.id, "exists", { current: existing, });
38
39
  }
39
- if (f["dry-run"] === true) {
40
+ if (executionMode(f).dryRun) {
40
41
  return {
41
42
  dryRun: true,
42
43
  action: "create",
@@ -75,7 +76,7 @@ export const wikiCommands = {
75
76
  if (!data && content === undefined && name === undefined) {
76
77
  throw new UsageError("--name, --content, --file, --data, --data-file, or --stdin is required.");
77
78
  }
78
- if (f["dry-run"] === true) {
79
+ if (executionMode(f).dryRun) {
79
80
  const current = await c.wiki.get(a[0], f["project-key"]);
80
81
  const next = deepMerge(current, data ?? {});
81
82
  if (name !== undefined) {
@@ -105,11 +106,11 @@ export const wikiCommands = {
105
106
  handler: async (c, a, f) => {
106
107
  requireArgs(a, 1, "dss wiki delete <id-or-name>");
107
108
  const pk = f["project-key"];
108
- if (f["dry-run"] === true || f["if-exists"] === true) {
109
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
109
110
  const current = await readIfExists(() => c.wiki.get(a[0], pk));
110
111
  if (!current)
111
112
  return skipResult("wiki", a[0], "missing");
112
- if (f["dry-run"] === true) {
113
+ if (executionMode(f).dryRun) {
113
114
  return { dryRun: true, action: "delete", resource: "wiki", article: a[0], current, };
114
115
  }
115
116
  }