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.
- package/README.md +246 -19
- package/dist/build-metadata.json +1 -1
- package/dist/packages/types/src/index.d.ts +39 -4
- package/dist/packages/types/src/index.js +31 -2
- package/dist/src/auth.js +1 -1
- package/dist/src/cli/coerce.js +11 -2
- package/dist/src/cli/commands/analysis.js +4 -3
- package/dist/src/cli/commands/api-service.js +66 -18
- package/dist/src/cli/commands/app.js +4 -3
- package/dist/src/cli/commands/bundle.js +51 -16
- package/dist/src/cli/commands/code-env.js +219 -57
- package/dist/src/cli/commands/code.d.ts +19 -0
- package/dist/src/cli/commands/code.js +87 -15
- package/dist/src/cli/commands/dashboard.js +7 -6
- package/dist/src/cli/commands/data-quality.js +9 -8
- package/dist/src/cli/commands/dataset.js +17 -11
- package/dist/src/cli/commands/doctor.js +4 -2
- package/dist/src/cli/commands/flow-zone.js +42 -13
- package/dist/src/cli/commands/folder.js +9 -8
- package/dist/src/cli/commands/future.js +2 -1
- package/dist/src/cli/commands/insight.js +8 -7
- package/dist/src/cli/commands/job.js +4 -3
- package/dist/src/cli/commands/meaning.js +5 -4
- package/dist/src/cli/commands/ml-task.js +3 -2
- package/dist/src/cli/commands/model-evaluation-store.js +4 -3
- package/dist/src/cli/commands/notebook.js +82 -50
- package/dist/src/cli/commands/project-git.js +8 -5
- package/dist/src/cli/commands/project-library.d.ts +21 -0
- package/dist/src/cli/commands/project-library.js +128 -56
- package/dist/src/cli/commands/project.js +19 -4
- package/dist/src/cli/commands/recipe.js +51 -25
- package/dist/src/cli/commands/saved-model.js +4 -3
- package/dist/src/cli/commands/scenario.js +9 -8
- package/dist/src/cli/commands/sql.d.ts +10 -0
- package/dist/src/cli/commands/sql.js +42 -19
- package/dist/src/cli/commands/statistics.js +7 -6
- package/dist/src/cli/commands/variable.js +2 -1
- package/dist/src/cli/commands/webapp.js +68 -13
- package/dist/src/cli/commands/wiki.js +7 -6
- package/dist/src/cli/contract.d.ts +16 -1
- package/dist/src/cli/contract.js +967 -186
- package/dist/src/cli/env.d.ts +9 -0
- package/dist/src/cli/env.js +20 -2
- package/dist/src/cli/flags.d.ts +4 -0
- package/dist/src/cli/flags.js +29 -1
- package/dist/src/cli/helpers/cleanup.js +104 -3
- package/dist/src/cli/helpers/flow-zone.d.ts +3 -0
- package/dist/src/cli/helpers/flow-zone.js +33 -3
- package/dist/src/cli/helpers/recipe.d.ts +20 -0
- package/dist/src/cli/helpers/recipe.js +158 -3
- package/dist/src/cli/output.d.ts +2 -0
- package/dist/src/cli/output.js +33 -6
- package/dist/src/cli/runtime.d.ts +35 -1
- package/dist/src/cli/runtime.js +125 -18
- package/dist/src/cli/types.d.ts +7 -0
- package/dist/src/cli.js +226 -95
- package/dist/src/client.d.ts +63 -1
- package/dist/src/client.js +305 -53
- package/dist/src/errors.d.ts +8 -2
- package/dist/src/errors.js +42 -0
- package/dist/src/index.d.ts +4 -3
- package/dist/src/index.js +2 -1
- package/dist/src/resources/api-services.d.ts +48 -6
- package/dist/src/resources/api-services.js +54 -8
- package/dist/src/resources/bundles.d.ts +51 -6
- package/dist/src/resources/bundles.js +39 -9
- package/dist/src/resources/code-envs.d.ts +25 -3
- package/dist/src/resources/code-envs.js +104 -2
- package/dist/src/resources/datasets.d.ts +20 -0
- package/dist/src/resources/datasets.js +232 -52
- package/dist/src/resources/folders.js +2 -1
- package/dist/src/resources/jobs.d.ts +35 -10
- package/dist/src/resources/jobs.js +92 -17
- package/dist/src/resources/notebooks.d.ts +77 -7
- package/dist/src/resources/notebooks.js +143 -19
- package/dist/src/resources/project-git.d.ts +12 -0
- package/dist/src/resources/project-git.js +39 -8
- package/dist/src/resources/project-library.d.ts +122 -7
- package/dist/src/resources/project-library.js +494 -25
- package/dist/src/resources/projects.d.ts +5 -4
- package/dist/src/resources/projects.js +21 -19
- package/dist/src/resources/scenarios.d.ts +30 -0
- package/dist/src/resources/scenarios.js +73 -27
- package/dist/src/resources/sql.d.ts +6 -0
- package/dist/src/resources/sql.js +23 -3
- package/dist/src/resources/webapps.d.ts +35 -4
- package/dist/src/resources/webapps.js +54 -5
- package/dist/src/schemas.d.ts +2 -2
- package/dist/src/schemas.js +1 -1
- package/dist/src/skill.d.ts +18 -0
- package/dist/src/skill.js +93 -9
- package/dist/src/utils/flow-analysis.d.ts +53 -0
- package/dist/src/utils/flow-analysis.js +382 -0
- package/dist/src/utils/project-archive.d.ts +41 -2
- package/dist/src/utils/project-archive.js +159 -16
- package/dist/src/utils/response-file.d.ts +2 -0
- package/dist/src/utils/response-file.js +34 -0
- package/package.json +9 -7
- package/packages/types/dist/index.d.ts +39 -4
- package/packages/types/dist/index.js +31 -2
- package/plugin.json +1 -1
- package/skills/dataiku-dss/SKILL.md +44 -197
- package/skills/dataiku-dss/references/app-releases.md +27 -0
- package/skills/dataiku-dss/references/authentication.md +39 -0
- package/skills/dataiku-dss/references/coding.md +8 -0
- package/skills/dataiku-dss/references/discovery.md +30 -0
- package/skills/dataiku-dss/references/flow-maps.md +12 -0
- package/skills/dataiku-dss/references/mutations.md +33 -0
- package/skills/dataiku-dss/references/troubleshooting.md +29 -0
|
@@ -1,18 +1,43 @@
|
|
|
1
1
|
import { readFileSync, } from "node:fs";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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 =
|
|
15
|
-
const remoteLines =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
534
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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:
|
|
35
|
-
description: "
|
|
36
|
-
examples: [
|
|
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,
|
|
51
|
-
|
|
52
|
-
|
|
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:
|
|
55
|
-
description: "Start or restart a webapp's backend.",
|
|
56
|
-
examples: [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
113
|
+
if (executionMode(f).dryRun) {
|
|
113
114
|
return { dryRun: true, action: "delete", resource: "wiki", article: a[0], current, };
|
|
114
115
|
}
|
|
115
116
|
}
|