dataiku-sdk 2.0.0 → 3.1.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/LICENSE +107 -17
- package/README.md +144 -17
- 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/cli/commands/api-service.js +62 -16
- package/dist/src/cli/commands/bundle.js +49 -15
- package/dist/src/cli/commands/code-env.js +210 -49
- package/dist/src/cli/commands/code.d.ts +19 -0
- package/dist/src/cli/commands/code.js +87 -15
- package/dist/src/cli/commands/dataset.js +8 -3
- package/dist/src/cli/commands/flow-zone.js +33 -5
- package/dist/src/cli/commands/notebook.js +72 -41
- 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 +121 -50
- package/dist/src/cli/commands/project.js +16 -2
- package/dist/src/cli/commands/recipe.js +37 -12
- package/dist/src/cli/commands/sql.d.ts +10 -0
- package/dist/src/cli/commands/sql.js +39 -17
- package/dist/src/cli/commands/webapp.js +67 -13
- package/dist/src/cli/contract.d.ts +15 -0
- package/dist/src/cli/contract.js +870 -107
- package/dist/src/cli/env.d.ts +9 -0
- package/dist/src/cli/env.js +20 -2
- package/dist/src/cli/flags.js +9 -0
- 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 +135 -28
- package/dist/src/client.d.ts +60 -0
- package/dist/src/client.js +237 -42
- package/dist/src/errors.d.ts +8 -2
- package/dist/src/errors.js +40 -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/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 -2
- package/dist/src/resources/projects.js +18 -2
- 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 +22 -2
- 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 +9 -0
- package/dist/src/skill.js +37 -4
- 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/package.json +5 -5
- package/packages/types/dist/index.d.ts +39 -4
- package/packages/types/dist/index.js +31 -2
- package/plugin.json +2 -2
- package/skills/dataiku-dss/SKILL.md +75 -48
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { encodeLibraryPath, EXPECT_SHA256_PATTERN, validateLibraryDestinationPath, validateLibraryName, validateLibraryPath, } from "../../resources/project-library.js";
|
|
3
|
+
import { numFlag, readStdinText, sha256Hex, } from "../coerce.js";
|
|
4
|
+
import { encodedProjectEndpoint, planResult, skipResult, } from "../output.js";
|
|
4
5
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
5
6
|
const PROJECT_LIBRARY_EXIT_CODES = { usage: 1, error: 2, transient: 3, };
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
throw new Error("Project library path is required");
|
|
10
|
-
return normalized;
|
|
11
|
-
}
|
|
12
|
-
function normalizeLibraryDestinationPath(path) {
|
|
13
|
-
if (path === "/")
|
|
14
|
-
return "/";
|
|
15
|
-
return `/${normalizeLibraryPath(path)}`;
|
|
16
|
-
}
|
|
17
|
-
function encodeLibraryPath(path) {
|
|
18
|
-
return normalizeLibraryPath(path)
|
|
19
|
-
.split("/")
|
|
20
|
-
.map((segment) => encodeURIComponent(segment))
|
|
21
|
-
.join("/");
|
|
7
|
+
const PUT_FLAG_ERROR_HINT = "Pass a 64-character SHA-256 hex digest (lowercase or uppercase).";
|
|
8
|
+
function sha256BytesHex(bytes) {
|
|
9
|
+
return new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
|
|
22
10
|
}
|
|
23
11
|
function projectLibraryContentsEndpoint(client, projectKey, path) {
|
|
24
12
|
return encodedProjectEndpoint(client, projectKey, `/libraries/contents/${encodeLibraryPath(path)}`);
|
|
@@ -29,22 +17,53 @@ function projectLibraryFolderEndpoint(client, projectKey, path) {
|
|
|
29
17
|
function projectLibraryActionEndpoint(client, projectKey, action) {
|
|
30
18
|
return encodedProjectEndpoint(client, projectKey, `/libraries/contents-actions/${action}`);
|
|
31
19
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Put-input metadata for plans and reports. Plans must never include content
|
|
22
|
+
* bytes: this helper reports only the source, byte count, and SHA-256 digest —
|
|
23
|
+
* and only when they are available without consuming stdin (dry-run keeps
|
|
24
|
+
* stdin unread, so the stdin source carries no size/hash).
|
|
25
|
+
*/
|
|
26
|
+
export function projectLibraryPutPayload(flags) {
|
|
27
|
+
const expectSha256 = expectSha256FromFlags(flags);
|
|
28
|
+
if (typeof flags["content"] === "string") {
|
|
29
|
+
const text = flags["content"];
|
|
30
|
+
return {
|
|
31
|
+
contentSource: "flag",
|
|
32
|
+
bytes: Buffer.byteLength(text, "utf8"),
|
|
33
|
+
sha256: sha256Hex(text),
|
|
34
|
+
...(expectSha256 ? { expectSha256, } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (typeof flags["file"] === "string") {
|
|
38
|
+
const bytes = fs.readFileSync(flags["file"]);
|
|
39
|
+
return {
|
|
40
|
+
contentSource: "file",
|
|
41
|
+
file: flags["file"],
|
|
42
|
+
bytes: bytes.length,
|
|
43
|
+
sha256: sha256BytesHex(bytes),
|
|
44
|
+
...(expectSha256 ? { expectSha256, } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { contentSource: "stdin", ...(expectSha256 ? { expectSha256, } : {}), };
|
|
38
48
|
}
|
|
39
49
|
function projectLibraryPlan(action, options) {
|
|
40
50
|
return planResult("project-library", action, {
|
|
41
51
|
...options,
|
|
52
|
+
idempotency: options.idempotency ?? "none",
|
|
42
53
|
asyncKind: "none",
|
|
43
54
|
exitCodesOnFailure: PROJECT_LIBRARY_EXIT_CODES,
|
|
44
|
-
idempotency: "none",
|
|
45
55
|
plannedAndDryRun: true,
|
|
46
56
|
});
|
|
47
57
|
}
|
|
58
|
+
function expectSha256FromFlags(flags) {
|
|
59
|
+
const value = flags["expect-sha256"];
|
|
60
|
+
if (value === undefined || value === false)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (typeof value !== "string" || !EXPECT_SHA256_PATTERN.test(value)) {
|
|
63
|
+
throw new UsageError("--expect-sha256 must be a 64-character SHA-256 hex digest.", "validation_failed", PUT_FLAG_ERROR_HINT);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
48
67
|
export const projectLibraryCommands = {
|
|
49
68
|
list: {
|
|
50
69
|
handler: (c, _a, f) => c.projectLibrary.listContents(f["project-key"]),
|
|
@@ -55,6 +74,7 @@ export const projectLibraryCommands = {
|
|
|
55
74
|
get: {
|
|
56
75
|
handler: (c, a, f) => {
|
|
57
76
|
requireArgs(a, 1, "dss project-library get <path> [--project-key KEY]");
|
|
77
|
+
validateLibraryPath(a[0]);
|
|
58
78
|
return c.projectLibrary.getFile(a[0], f["project-key"]);
|
|
59
79
|
},
|
|
60
80
|
usage: "dss project-library get <path> [--project-key KEY]",
|
|
@@ -69,7 +89,7 @@ export const projectLibraryCommands = {
|
|
|
69
89
|
throw new UsageError("--output PATH is required.", "missing_required_flag");
|
|
70
90
|
const bytes = await c.projectLibrary.getFileBytes(a[0], f["project-key"]);
|
|
71
91
|
const written = await Bun.write(out, bytes, { createPath: false, });
|
|
72
|
-
return { path: out, bytes: written, };
|
|
92
|
+
return { path: out, bytes: written, sha256: sha256BytesHex(bytes), };
|
|
73
93
|
},
|
|
74
94
|
usage: "dss project-library get-bytes <path> --output PATH [--project-key KEY]",
|
|
75
95
|
description: "Download a project library file's raw bytes to a local file.",
|
|
@@ -77,44 +97,62 @@ export const projectLibraryCommands = {
|
|
|
77
97
|
},
|
|
78
98
|
"create-file": {
|
|
79
99
|
handler: async (c, a, f) => {
|
|
80
|
-
requireArgs(a, 1, "dss project-library create-file <path> [--dry-run] [--project-key KEY]");
|
|
100
|
+
requireArgs(a, 1, "dss project-library create-file <path> [--if-not-exists] [--dry-run] [--project-key KEY]");
|
|
81
101
|
const pk = f["project-key"];
|
|
102
|
+
validateLibraryPath(a[0]);
|
|
82
103
|
if (f["dry-run"] === true) {
|
|
83
104
|
return projectLibraryPlan("create-file", {
|
|
84
105
|
method: "POST",
|
|
85
106
|
endpoint: projectLibraryContentsEndpoint(c, pk, a[0]),
|
|
86
107
|
identifiers: { path: a[0], },
|
|
108
|
+
idempotency: "if-not-exists",
|
|
87
109
|
});
|
|
88
110
|
}
|
|
111
|
+
if (f["if-not-exists"] === true) {
|
|
112
|
+
const exists = await c.projectLibrary.hasLibraryItem(a[0], pk);
|
|
113
|
+
if (exists) {
|
|
114
|
+
return skipResult("project-library", a[0], "exists", { kind: "file", });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
89
117
|
await c.projectLibrary.addFile(a[0], pk);
|
|
90
118
|
return { created: a[0], };
|
|
91
119
|
},
|
|
92
|
-
usage: "dss project-library create-file <path> [--dry-run] [--project-key KEY]",
|
|
93
|
-
description: "Create an empty file in the project library.",
|
|
94
|
-
examples: ["dss project-library create-file python/mylib/new.py",],
|
|
120
|
+
usage: "dss project-library create-file <path> [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
121
|
+
description: "Create an empty file in the project library; refuses to overwrite an existing item.",
|
|
122
|
+
examples: ["dss project-library create-file python/mylib/new.py --if-not-exists",],
|
|
95
123
|
},
|
|
96
124
|
"create-folder": {
|
|
97
125
|
handler: async (c, a, f) => {
|
|
98
|
-
requireArgs(a, 1, "dss project-library create-folder <path> [--dry-run] [--project-key KEY]");
|
|
126
|
+
requireArgs(a, 1, "dss project-library create-folder <path> [--if-not-exists] [--dry-run] [--project-key KEY]");
|
|
99
127
|
const pk = f["project-key"];
|
|
128
|
+
validateLibraryPath(a[0]);
|
|
100
129
|
if (f["dry-run"] === true) {
|
|
101
130
|
return projectLibraryPlan("create-folder", {
|
|
102
131
|
method: "POST",
|
|
103
132
|
endpoint: projectLibraryFolderEndpoint(c, pk, a[0]),
|
|
104
133
|
identifiers: { path: a[0], },
|
|
134
|
+
idempotency: "if-not-exists",
|
|
105
135
|
});
|
|
106
136
|
}
|
|
137
|
+
if (f["if-not-exists"] === true) {
|
|
138
|
+
const exists = await c.projectLibrary.hasLibraryItem(a[0], pk);
|
|
139
|
+
if (exists) {
|
|
140
|
+
return skipResult("project-library", a[0], "exists", { kind: "folder", });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
107
143
|
await c.projectLibrary.addFolder(a[0], pk);
|
|
108
144
|
return { created: a[0], };
|
|
109
145
|
},
|
|
110
|
-
usage: "dss project-library create-folder <path> [--dry-run] [--project-key KEY]",
|
|
111
|
-
description: "Create a folder in the project library.",
|
|
112
|
-
examples: ["dss project-library create-folder python/mylib",],
|
|
146
|
+
usage: "dss project-library create-folder <path> [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
147
|
+
description: "Create a folder in the project library; refuses to overwrite an existing item.",
|
|
148
|
+
examples: ["dss project-library create-folder python/mylib --if-not-exists",],
|
|
113
149
|
},
|
|
114
150
|
put: {
|
|
115
151
|
handler: async (c, a, f) => {
|
|
116
|
-
requireArgs(a, 1, "dss project-library put <path> (--content TEXT|--file PATH|--stdin) [--dry-run] [--project-key KEY]");
|
|
152
|
+
requireArgs(a, 1, "dss project-library put <path> (--content TEXT|--file PATH|--stdin) [--expect-sha256 SHA256] [--dry-run] [--project-key KEY]");
|
|
117
153
|
const pk = f["project-key"];
|
|
154
|
+
const expectSha256 = expectSha256FromFlags(f);
|
|
155
|
+
validateLibraryPath(a[0]);
|
|
118
156
|
if (f["dry-run"] === true) {
|
|
119
157
|
return projectLibraryPlan("put", {
|
|
120
158
|
method: "POST",
|
|
@@ -126,19 +164,48 @@ export const projectLibraryCommands = {
|
|
|
126
164
|
const content = typeof f["content"] === "string"
|
|
127
165
|
? f["content"]
|
|
128
166
|
: typeof f["file"] === "string"
|
|
129
|
-
? fs.readFileSync(f["file"]
|
|
167
|
+
? fs.readFileSync(f["file"])
|
|
168
|
+
: await readStdinText();
|
|
169
|
+
const result = await c.projectLibrary.addOrUpdateFile(a[0], content, pk, expectSha256 !== undefined ? { expectSha256, } : undefined);
|
|
170
|
+
return {
|
|
171
|
+
updated: result.path,
|
|
172
|
+
bytes: result.bytes,
|
|
173
|
+
sha256: result.sha256,
|
|
174
|
+
...(result.beforeSha256 !== undefined ? { beforeSha256: result.beforeSha256, } : {}),
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
usage: "dss project-library put <path> (--content TEXT|--file PATH|--stdin) [--expect-sha256 SHA256] [--dry-run] [--project-key KEY]",
|
|
178
|
+
description: "Create or overwrite a project library file with text or binary content; reports the written byte count and sha256.",
|
|
179
|
+
examples: [
|
|
180
|
+
"dss project-library put python/mylib/utils.py --file ./utils.py",
|
|
181
|
+
"dss project-library put python/mylib/utils.py --file ./utils.py --expect-sha256 <previous-sha256>",
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
diff: {
|
|
185
|
+
handler: async (c, a, f) => {
|
|
186
|
+
requireArgs(a, 1, "dss project-library diff <path> (--content TEXT|--file PATH|--stdin) [--max-lines N] [--project-key KEY]");
|
|
187
|
+
const pk = f["project-key"];
|
|
188
|
+
validateLibraryPath(a[0]);
|
|
189
|
+
const maxLines = numFlag(f, ["max-lines",]);
|
|
190
|
+
const local = typeof f["content"] === "string"
|
|
191
|
+
? f["content"]
|
|
192
|
+
: typeof f["file"] === "string"
|
|
193
|
+
? fs.readFileSync(f["file"])
|
|
130
194
|
: await readStdinText();
|
|
131
|
-
|
|
132
|
-
return { updated: a[0], };
|
|
195
|
+
return c.projectLibrary.diffFile(a[0], local, pk, maxLines !== undefined ? { maxLines, } : {});
|
|
133
196
|
},
|
|
134
|
-
usage: "dss project-library
|
|
135
|
-
description: "
|
|
136
|
-
examples: [
|
|
197
|
+
usage: "dss project-library diff <path> (--content TEXT|--file PATH|--stdin) [--max-lines N] [--project-key KEY]",
|
|
198
|
+
description: "Diff a project library file against local content; capped unified text diff, binary files detected instead of dumped.",
|
|
199
|
+
examples: [
|
|
200
|
+
"dss project-library diff python/mylib/utils.py --file ./utils.py",
|
|
201
|
+
"dss project-library diff python/mylib/utils.py --content 'print(1)' --max-lines 50",
|
|
202
|
+
],
|
|
137
203
|
},
|
|
138
204
|
delete: {
|
|
139
205
|
handler: async (c, a, f) => {
|
|
140
206
|
requireArgs(a, 1, "dss project-library delete <path> [--dry-run] [--project-key KEY]");
|
|
141
207
|
const pk = f["project-key"];
|
|
208
|
+
validateLibraryPath(a[0]);
|
|
142
209
|
if (f["dry-run"] === true) {
|
|
143
210
|
return projectLibraryPlan("delete", {
|
|
144
211
|
method: "DELETE",
|
|
@@ -157,16 +224,18 @@ export const projectLibraryCommands = {
|
|
|
157
224
|
handler: async (c, a, f) => {
|
|
158
225
|
requireArgs(a, 2, "dss project-library rename <path> <new-name> [--dry-run] [--project-key KEY]");
|
|
159
226
|
const pk = f["project-key"];
|
|
227
|
+
const validPath = validateLibraryPath(a[0]);
|
|
228
|
+
const validName = validateLibraryName(a[1]);
|
|
160
229
|
if (f["dry-run"] === true) {
|
|
161
230
|
return projectLibraryPlan("rename", {
|
|
162
231
|
method: "POST",
|
|
163
232
|
endpoint: projectLibraryActionEndpoint(c, pk, "rename/"),
|
|
164
|
-
identifiers: { path:
|
|
165
|
-
payload: { oldPath: `/${
|
|
233
|
+
identifiers: { path: validPath, newName: validName, },
|
|
234
|
+
payload: { oldPath: `/${validPath}`, newName: validName, },
|
|
166
235
|
});
|
|
167
236
|
}
|
|
168
|
-
await c.projectLibrary.rename(
|
|
169
|
-
return { renamed:
|
|
237
|
+
await c.projectLibrary.rename(validPath, validName, pk);
|
|
238
|
+
return { renamed: validPath, to: validName, };
|
|
170
239
|
},
|
|
171
240
|
usage: "dss project-library rename <path> <new-name> [--dry-run] [--project-key KEY]",
|
|
172
241
|
description: "Rename a project library file or folder within its parent.",
|
|
@@ -176,19 +245,21 @@ export const projectLibraryCommands = {
|
|
|
176
245
|
handler: async (c, a, f) => {
|
|
177
246
|
requireArgs(a, 2, "dss project-library move <path> <destination-folder> [--dry-run] [--project-key KEY]");
|
|
178
247
|
const pk = f["project-key"];
|
|
248
|
+
const validPath = validateLibraryPath(a[0]);
|
|
249
|
+
const destination = validateLibraryDestinationPath(a[1]);
|
|
179
250
|
if (f["dry-run"] === true) {
|
|
180
251
|
return projectLibraryPlan("move", {
|
|
181
252
|
method: "POST",
|
|
182
253
|
endpoint: projectLibraryActionEndpoint(c, pk, "move"),
|
|
183
|
-
identifiers: { path:
|
|
254
|
+
identifiers: { path: validPath, destinationFolder: destination, },
|
|
184
255
|
payload: {
|
|
185
|
-
oldPath: `/${
|
|
186
|
-
newPath:
|
|
256
|
+
oldPath: `/${validPath}`,
|
|
257
|
+
newPath: destination,
|
|
187
258
|
},
|
|
188
259
|
});
|
|
189
260
|
}
|
|
190
|
-
await c.projectLibrary.move(
|
|
191
|
-
return { moved:
|
|
261
|
+
await c.projectLibrary.move(validPath, destination, pk);
|
|
262
|
+
return { moved: validPath, to: destination, };
|
|
192
263
|
},
|
|
193
264
|
usage: "dss project-library move <path> <destination-folder> [--dry-run] [--project-key KEY]",
|
|
194
265
|
description: "Move a project library file or folder into another folder.",
|
|
@@ -6,6 +6,13 @@ import { jsonInput, num, requiredJsonInput, requiredStringFlag, } from "../coerc
|
|
|
6
6
|
import { CommandResultFailure, } from "../output.js";
|
|
7
7
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
8
8
|
const INSPECT_ARCHIVE_USAGE = "dss project inspect-archive <file>";
|
|
9
|
+
function flowRenderFormat(value) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (value === "ascii" || value === "mermaid")
|
|
13
|
+
return value;
|
|
14
|
+
throw new UsageError("--render must be ascii or mermaid.", "invalid_enum");
|
|
15
|
+
}
|
|
9
16
|
/** Local, DSS-free archive inspection shared by the local handler and the command handler. */
|
|
10
17
|
function inspectArchiveCommand(args) {
|
|
11
18
|
requireArgs(args, 1, INSPECT_ARCHIVE_USAGE);
|
|
@@ -42,12 +49,19 @@ export const projectCommands = {
|
|
|
42
49
|
maxNodes: num(f["max-nodes"], "--max-nodes"),
|
|
43
50
|
maxEdges: num(f["max-edges"], "--max-edges"),
|
|
44
51
|
includeRaw: f["include-raw"] === true,
|
|
52
|
+
render: flowRenderFormat(f["render"]),
|
|
45
53
|
}),
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
validate: (_a, f) => {
|
|
55
|
+
flowRenderFormat(f["render"]);
|
|
56
|
+
num(f["max-nodes"], "--max-nodes");
|
|
57
|
+
num(f["max-edges"], "--max-edges");
|
|
58
|
+
},
|
|
59
|
+
usage: "dss project map [--max-nodes N] [--max-edges N] [--include-raw] [--render ascii|mermaid] [--project-key KEY]",
|
|
60
|
+
description: "Get an analyzed flow map with zones, layers, components, and diagnostics.",
|
|
48
61
|
examples: [
|
|
49
62
|
"dss project map",
|
|
50
63
|
"dss project map --max-nodes 50 --max-edges 100",
|
|
64
|
+
"dss project map --render mermaid",
|
|
51
65
|
"dss project map --include-raw",
|
|
52
66
|
],
|
|
53
67
|
},
|
|
@@ -1,18 +1,42 @@
|
|
|
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
6
|
import { moveCreatedItemsToZone, resolveFlowZoneIdFromFlags, } from "../helpers/flow-zone.js";
|
|
7
|
-
import { readRecipeBackup, recipeBackupDocument, recipeBackupPath, recipeCodeEnv, recipeGraph, recipeInputItemRef, recipeRoleInputItems, recipeRunShouldWait, } from "../helpers/recipe.js";
|
|
7
|
+
import { ensureRecipeBackupDir, readRecipeBackup, recipeBackupDocument, recipeBackupPath, recipeCodeEnv, recipeGraph, recipeInputItemRef, recipeRoleInputItems, recipeRunShouldWait, writeRecipeBackup, } from "../helpers/recipe.js";
|
|
8
8
|
import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
|
|
9
9
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
10
|
+
/** Read a local recipe file before any DSS request, mapping read failures to usage validation. */
|
|
11
|
+
function readRecipeFile(filePath, flagName) {
|
|
12
|
+
try {
|
|
13
|
+
return readFileSync(filePath, "utf-8");
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
17
|
+
throw new UsageError(`Could not read ${flagName} ${filePath}: ${reason}.`, "not_found", "Verify the file path and that it is readable.");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Read a set-payload backup file before any DSS request, mapping read failures to usage validation. */
|
|
21
|
+
function readRecipeBackupFile(backupPath) {
|
|
22
|
+
try {
|
|
23
|
+
return readRecipeBackup(backupPath);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
27
|
+
throw new UsageError(`Could not read --backup ${backupPath}: ${reason}.`, "not_found", "Verify the backup file path and that it is readable.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
10
30
|
function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
|
|
11
|
-
|
|
31
|
+
// Line-ending differences are not content differences: compare and split on
|
|
32
|
+
// CRLF-normalized text, mirroring assert-unchanged's normalized payload hash.
|
|
33
|
+
const normalizedLocal = normalizeLineEndings(localContent);
|
|
34
|
+
const normalizedRemote = normalizeLineEndings(remoteContent);
|
|
35
|
+
if (normalizedLocal === normalizedRemote) {
|
|
12
36
|
return "No differences.";
|
|
13
37
|
}
|
|
14
|
-
const localLines =
|
|
15
|
-
const remoteLines =
|
|
38
|
+
const localLines = normalizedLocal.split("\n");
|
|
39
|
+
const remoteLines = normalizedRemote.split("\n");
|
|
16
40
|
const lines = [`--- remote:${remoteName}`, `+++ local:${localPath}`, "",];
|
|
17
41
|
const maxLen = Math.max(localLines.length, remoteLines.length);
|
|
18
42
|
for (let i = 0; i < maxLen; i++) {
|
|
@@ -104,7 +128,7 @@ export const recipeCommands = {
|
|
|
104
128
|
return c.recipes.run(a[0], options);
|
|
105
129
|
},
|
|
106
130
|
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.",
|
|
131
|
+
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
132
|
examples: [
|
|
109
133
|
"dss recipe run compute_orders --wait",
|
|
110
134
|
"dss recipe run compute_exports --include-logs --log-filter stdout --summary --timeout 600000",
|
|
@@ -344,6 +368,8 @@ export const recipeCommands = {
|
|
|
344
368
|
if (!filePath) {
|
|
345
369
|
throw new UsageError("--file is required. Usage: dss recipe diff <name> --file PATH");
|
|
346
370
|
}
|
|
371
|
+
// Read and validate the local file before any DSS request.
|
|
372
|
+
const localContent = readRecipeFile(filePath, "--file");
|
|
347
373
|
const result = await c.recipes.get(a[0], {
|
|
348
374
|
includePayload: true,
|
|
349
375
|
projectKey: f["project-key"],
|
|
@@ -351,7 +377,6 @@ export const recipeCommands = {
|
|
|
351
377
|
if (!result.payload) {
|
|
352
378
|
throw new Error(`Recipe "${a[0]}" has no code payload to diff.`);
|
|
353
379
|
}
|
|
354
|
-
const localContent = readFileSync(filePath, "utf-8");
|
|
355
380
|
return formatLineDiff(a[0], filePath, result.payload, localContent);
|
|
356
381
|
},
|
|
357
382
|
usage: "dss recipe diff <name> --file PATH [--project-key KEY]",
|
|
@@ -506,7 +531,7 @@ export const recipeCommands = {
|
|
|
506
531
|
const filePath = f["file"];
|
|
507
532
|
if (!filePath)
|
|
508
533
|
throw new UsageError("--file is required.");
|
|
509
|
-
const content =
|
|
534
|
+
const content = readRecipeFile(filePath, "--file");
|
|
510
535
|
const pk = f["project-key"];
|
|
511
536
|
const shouldBackup = f["no-backup"] !== true;
|
|
512
537
|
const backupDir = shouldBackup
|
|
@@ -530,8 +555,8 @@ export const recipeCommands = {
|
|
|
530
555
|
};
|
|
531
556
|
}
|
|
532
557
|
if (backupDir && backupPath) {
|
|
533
|
-
|
|
534
|
-
|
|
558
|
+
ensureRecipeBackupDir(backupDir);
|
|
559
|
+
writeRecipeBackup(backupPath, `${JSON.stringify(recipeBackupDocument(a[0], pk, current), null, 2)}\n`);
|
|
535
560
|
}
|
|
536
561
|
await c.recipes.replace(a[0], { ...current, payload: content, }, pk);
|
|
537
562
|
return {
|
|
@@ -555,7 +580,7 @@ export const recipeCommands = {
|
|
|
555
580
|
const usage = "dss recipe restore <name> --backup FILE [--payload-only] [--dry-run] [--project-key KEY]";
|
|
556
581
|
requireArgs(a, 1, usage);
|
|
557
582
|
const backupPath = requiredStringFlag(f, "backup", usage);
|
|
558
|
-
const backup =
|
|
583
|
+
const backup = readRecipeBackupFile(backupPath);
|
|
559
584
|
const payload = typeof backup.payload === "string" ? backup.payload : "";
|
|
560
585
|
const pk = f["project-key"];
|
|
561
586
|
const current = await c.recipes.get(a[0], { includePayload: true, projectKey: pk, });
|
|
@@ -598,7 +623,7 @@ export const recipeCommands = {
|
|
|
598
623
|
const usage = "dss recipe assert-unchanged <name> --since BACKUP [--project-key KEY]";
|
|
599
624
|
requireArgs(a, 1, usage);
|
|
600
625
|
const backupPath = requiredStringFlag(f, "since", usage);
|
|
601
|
-
const backup =
|
|
626
|
+
const backup = readRecipeBackupFile(backupPath);
|
|
602
627
|
const current = await c.recipes.get(a[0], {
|
|
603
628
|
includePayload: true,
|
|
604
629
|
projectKey: f["project-key"],
|
|
@@ -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>;
|
|
@@ -2,6 +2,7 @@ import { readFileSync, } from "node:fs";
|
|
|
2
2
|
import { mkdir, writeFile, } from "node:fs/promises";
|
|
3
3
|
import { dirname, resolve, } from "node:path";
|
|
4
4
|
import { readStdinText, stripUtf8Bom, } from "../coerce.js";
|
|
5
|
+
import { enqueueCliWarning, } from "../output.js";
|
|
5
6
|
import { UsageError, } from "../usage.js";
|
|
6
7
|
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
8
|
const DEFAULT_SQL_PREVIEW_ROWS = 5;
|
|
@@ -34,7 +35,7 @@ function parseSqlStartRetries(value) {
|
|
|
34
35
|
}
|
|
35
36
|
return parsed;
|
|
36
37
|
}
|
|
37
|
-
function resolveSqlInput(args, flags) {
|
|
38
|
+
export function resolveSqlInput(args, flags) {
|
|
38
39
|
const sources = [];
|
|
39
40
|
if (typeof flags["sql"] === "string") {
|
|
40
41
|
sources.push({
|
|
@@ -69,34 +70,55 @@ function resolveSqlInput(args, flags) {
|
|
|
69
70
|
}
|
|
70
71
|
return query;
|
|
71
72
|
}
|
|
73
|
+
export function resolveSqlQueryInvocation(args, flags, projectKeyOverride) {
|
|
74
|
+
const connection = flags["connection"];
|
|
75
|
+
const datasetFullName = flags["dataset"];
|
|
76
|
+
if ((connection ? 1 : 0) + (datasetFullName ? 1 : 0) !== 1) {
|
|
77
|
+
throw new UsageError(`Pass exactly one of --connection or --dataset. Usage: ${SQL_QUERY_USAGE}`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
query: resolveSqlInput(args, flags),
|
|
81
|
+
connection,
|
|
82
|
+
datasetFullName,
|
|
83
|
+
database: flags["database"],
|
|
84
|
+
projectKey: projectKeyOverride ?? flags["project-key"],
|
|
85
|
+
type: "sql",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
72
88
|
export const sqlCommands = {
|
|
73
89
|
query: {
|
|
74
90
|
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
91
|
const outputFile = f["output"]
|
|
81
92
|
?? f["output-file"];
|
|
82
93
|
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
94
|
const previewCount = previewProvided
|
|
87
95
|
? parseSqlPreviewCount(f["preview"])
|
|
88
96
|
: DEFAULT_SQL_PREVIEW_ROWS;
|
|
89
|
-
const query = resolveSqlInput(a, f);
|
|
90
97
|
const result = await c.sql.query({
|
|
91
|
-
|
|
92
|
-
connection,
|
|
93
|
-
datasetFullName,
|
|
94
|
-
database: f["database"],
|
|
95
|
-
projectKey: f["project-key"],
|
|
98
|
+
...resolveSqlQueryInvocation(a, f),
|
|
96
99
|
retryMaxAttempts: parseSqlStartRetries(f["start-retries"]),
|
|
97
100
|
});
|
|
98
|
-
if (!outputFile)
|
|
101
|
+
if (!outputFile && !previewProvided)
|
|
99
102
|
return result;
|
|
103
|
+
if (!outputFile) {
|
|
104
|
+
const truncated = result.rows.length > previewCount;
|
|
105
|
+
if (truncated) {
|
|
106
|
+
enqueueCliWarning({
|
|
107
|
+
code: "sql_preview_truncated",
|
|
108
|
+
rowCount: result.rows.length,
|
|
109
|
+
previewRows: previewCount,
|
|
110
|
+
hint: "Use --output PATH to write the full result without placing it on stdout.",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
queryId: result.queryId,
|
|
115
|
+
schema: result.schema,
|
|
116
|
+
columns: result.columns ?? result.schema,
|
|
117
|
+
rowCount: result.rows.length,
|
|
118
|
+
preview: result.rows.slice(0, previewCount),
|
|
119
|
+
truncated,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
100
122
|
const outputPath = resolve(outputFile);
|
|
101
123
|
await mkdir(dirname(outputPath), { recursive: true, });
|
|
102
124
|
await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
|
|
@@ -111,7 +133,7 @@ export const sqlCommands = {
|
|
|
111
133
|
};
|
|
112
134
|
},
|
|
113
135
|
usage: SQL_QUERY_USAGE,
|
|
114
|
-
description: "Run
|
|
136
|
+
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
137
|
examples: [
|
|
116
138
|
"dss sql query 'SELECT * FROM orders LIMIT 10' --connection my_pg",
|
|
117
139
|
"dss sql query --sql-file query.sql --connection my_pg",
|
|
@@ -1,5 +1,18 @@
|
|
|
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";
|
|
2
5
|
import { requireArgs, } from "../usage.js";
|
|
6
|
+
const UPDATE_SETTINGS_USAGE = "dss webapp update-settings <webappId> (--data JSON|--data-file PATH|--stdin) [--expect-hash SHA256] [--dry-run] [--project-key KEY]";
|
|
7
|
+
const RESTART_USAGE = "dss webapp restart-backend <webappId> [--wait] [--timeout MS] [--poll-interval MS] [--project-key KEY]";
|
|
8
|
+
function validateExpectHash(value) {
|
|
9
|
+
if (value === undefined || value === false)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (typeof value !== "string" || !/^[0-9a-fA-F]{64}$/.test(value)) {
|
|
12
|
+
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.");
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
3
16
|
export const webappCommands = {
|
|
4
17
|
list: {
|
|
5
18
|
handler: (c, _a, f) => c.webapps.list(f["project-key"]),
|
|
@@ -26,14 +39,45 @@ export const webappCommands = {
|
|
|
26
39
|
examples: ["dss webapp create --data-file webapp.json",],
|
|
27
40
|
},
|
|
28
41
|
"update-settings": {
|
|
29
|
-
handler: (c, a, f) => {
|
|
30
|
-
requireArgs(a, 1,
|
|
42
|
+
handler: async (c, a, f) => {
|
|
43
|
+
requireArgs(a, 1, UPDATE_SETTINGS_USAGE);
|
|
31
44
|
const body = requiredJsonInput(f, "--data, --data-file, or --stdin is required (webapp settings).");
|
|
32
|
-
|
|
45
|
+
const projectKey = f["project-key"];
|
|
46
|
+
const expectHash = validateExpectHash(f["expect-hash"]);
|
|
47
|
+
if (f["dry-run"] === true) {
|
|
48
|
+
const current = await c.webapps.getSettings(a[0], projectKey);
|
|
49
|
+
const currentHash = stableHash(current);
|
|
50
|
+
if (expectHash !== undefined && currentHash !== expectHash.toLowerCase()) {
|
|
51
|
+
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.", {
|
|
52
|
+
id: a[0],
|
|
53
|
+
expectedHash: expectHash.toLowerCase(),
|
|
54
|
+
actualHash: currentHash,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const next = deepMerge(current, body);
|
|
58
|
+
return {
|
|
59
|
+
dryRun: true,
|
|
60
|
+
action: "update-settings",
|
|
61
|
+
resource: "webapp",
|
|
62
|
+
id: a[0],
|
|
63
|
+
current,
|
|
64
|
+
next,
|
|
65
|
+
currentHash,
|
|
66
|
+
nextHash: stableHash(next),
|
|
67
|
+
...(expectHash !== undefined
|
|
68
|
+
? { expectHash: expectHash.toLowerCase(), provenanceVerified: true, }
|
|
69
|
+
: {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const updated = await c.webapps.updateSettings(a[0], body, projectKey, { expectHash, });
|
|
73
|
+
return { updated: a[0], ...updated, };
|
|
33
74
|
},
|
|
34
|
-
usage:
|
|
35
|
-
description: "
|
|
36
|
-
examples: [
|
|
75
|
+
usage: UPDATE_SETTINGS_USAGE,
|
|
76
|
+
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.",
|
|
77
|
+
examples: [
|
|
78
|
+
"dss webapp update-settings WEBAPP_ID --data-file webapp.json",
|
|
79
|
+
'dss webapp update-settings WEBAPP_ID --dry-run --data=\'{"params":{"designOptions":{"namingRule":"x"}}}\'',
|
|
80
|
+
],
|
|
37
81
|
},
|
|
38
82
|
"stop-backend": {
|
|
39
83
|
handler: async (c, a, f) => {
|
|
@@ -47,13 +91,23 @@ export const webappCommands = {
|
|
|
47
91
|
},
|
|
48
92
|
"restart-backend": {
|
|
49
93
|
handler: async (c, a, f) => {
|
|
50
|
-
requireArgs(a, 1,
|
|
51
|
-
|
|
52
|
-
|
|
94
|
+
requireArgs(a, 1, RESTART_USAGE);
|
|
95
|
+
const projectKey = f["project-key"];
|
|
96
|
+
if (f["wait"] === true) {
|
|
97
|
+
return c.webapps.restartBackendAndWait(a[0], projectKey, {
|
|
98
|
+
pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
|
|
99
|
+
timeoutMs: num(f["timeout"], "--timeout"),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const future = await c.webapps.startOrRestartBackend(a[0], projectKey);
|
|
103
|
+
return { restarted: true, future, };
|
|
53
104
|
},
|
|
54
|
-
usage:
|
|
55
|
-
description: "Start or restart a webapp's backend.",
|
|
56
|
-
examples: [
|
|
105
|
+
usage: RESTART_USAGE,
|
|
106
|
+
description: "Start or restart a webapp's backend. The documented endpoint returns a restart future; --wait settles it and returns the FutureWaitResult.",
|
|
107
|
+
examples: [
|
|
108
|
+
"dss webapp restart-backend WEBAPP_ID",
|
|
109
|
+
"dss webapp restart-backend WEBAPP_ID --wait --timeout=60000",
|
|
110
|
+
],
|
|
57
111
|
},
|
|
58
112
|
"backend-state": {
|
|
59
113
|
handler: (c, a, f) => {
|