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,66 +1,516 @@
1
+ import { createHash, } from "node:crypto";
2
+ import { ClientValidationError, DataikuError, } from "../errors.js";
1
3
  import { BaseResource, } from "./base.js";
2
- function normalizeLibraryPath(path) {
4
+ /**
5
+ * The only concurrency control the public project-library API permits. DSS
6
+ * exposes no conditional write (no ETag, no If-Match, no version token), so
7
+ * `expectSha256` can only detect a hash that is already stale when it is
8
+ * checked; it can never turn the subsequent POST into a conditional write.
9
+ */
10
+ export const PROJECT_LIBRARY_CONCURRENCY_CONTROL = "client-side-non-atomic-stale-read-check";
11
+ /** Lowercase or uppercase SHA-256 hex digest. */
12
+ export const EXPECT_SHA256_PATTERN = /^[0-9a-fA-F]{64}$/;
13
+ function sha256Hex(value) {
14
+ return createHash("sha256").update(value).digest("hex");
15
+ }
16
+ const CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f]/;
17
+ const BACKSLASH_RE = /\\/;
18
+ const TRAILING_AMBIGUOUS_RE = /[\s.]$/;
19
+ const WHITESPACE_ONLY_RE = /^\s+$/;
20
+ function pathValidationError(path, message) {
21
+ return new ClientValidationError(message, "validation_failed", "Use a project library path relative to the library root: folders separated by '/', each segment a simple file or folder name.", { path, });
22
+ }
23
+ /**
24
+ * Validate and canonicalize a project library path.
25
+ *
26
+ * Rejects empty paths, control characters, backslash separators, empty
27
+ * segments (leading/trailing/doubled '/'), '.'/'..' segments (including
28
+ * mid-path traversals), whitespace-only or edge-whitespace segments, and
29
+ * trailing dot/space segments that differ across filesystem normalizations.
30
+ *
31
+ * Returns the canonical path with any leading '/' prefix stripped (the DSS
32
+ * contents endpoints take paths relative to the library root).
33
+ */
34
+ export function validateLibraryPath(path) {
35
+ if (typeof path !== "string" || path.trim() === "") {
36
+ throw pathValidationError(path, "Project library path must be a non-empty string.");
37
+ }
38
+ if (CONTROL_CHARACTER_RE.test(path)) {
39
+ throw pathValidationError(path, "Project library path must not contain control characters.");
40
+ }
3
41
  const normalized = path.replace(/^\/+/, "");
4
- if (!normalized)
5
- throw new Error("Project library path is required");
42
+ if (normalized === "") {
43
+ throw pathValidationError(path, "Project library path must name a file or folder below the library root.");
44
+ }
45
+ const segments = normalized.split("/");
46
+ for (const segment of segments) {
47
+ if (segment === "") {
48
+ throw pathValidationError(path, "Project library path must not contain empty segments (leading, trailing, or doubled '/').");
49
+ }
50
+ if (segment === "." || segment === "..") {
51
+ throw pathValidationError(path, "Project library path must not contain '.' or '..' segments.");
52
+ }
53
+ if (BACKSLASH_RE.test(segment)) {
54
+ throw pathValidationError(path, "Project library path must not contain backslashes.");
55
+ }
56
+ if (segment !== segment.trim() || WHITESPACE_ONLY_RE.test(segment)) {
57
+ throw pathValidationError(path, "Project library path segments must not be whitespace-only, or start or end with whitespace.");
58
+ }
59
+ if (TRAILING_AMBIGUOUS_RE.test(segment)) {
60
+ throw pathValidationError(path, "Project library path must not contain segments with a trailing space or dot (ambiguous across filesystem normalizations).");
61
+ }
62
+ }
6
63
  return normalized;
7
64
  }
8
- function normalizeLibraryDestinationPath(path) {
65
+ /**
66
+ * Validate a rename target: a single library segment (no '/'), with the same
67
+ * character rules as path segments. Returns the validated name unchanged.
68
+ */
69
+ export function validateLibraryName(name) {
70
+ if (typeof name !== "string" || name.trim() === "") {
71
+ throw new ClientValidationError("Project library rename target must be a non-empty string.", "validation_failed", "Pass the new name as a single file or folder name.", { newName: name, });
72
+ }
73
+ if (CONTROL_CHARACTER_RE.test(name)) {
74
+ throw new ClientValidationError("Project library rename target must not contain control characters.", "validation_failed", undefined, { newName: name, });
75
+ }
76
+ if (name.includes("/")) {
77
+ throw new ClientValidationError("Project library rename target must be a single segment without '/'.", "validation_failed", "Rename within the current parent folder; use the move operation to change folders.", { newName: name, });
78
+ }
79
+ if (name === "." || name === "..") {
80
+ throw new ClientValidationError("Project library rename target must not be '.' or '..'.", "validation_failed", undefined, { newName: name, });
81
+ }
82
+ if (BACKSLASH_RE.test(name)) {
83
+ throw new ClientValidationError("Project library rename target must not contain backslashes.", "validation_failed", undefined, { newName: name, });
84
+ }
85
+ if (name !== name.trim() || WHITESPACE_ONLY_RE.test(name) || TRAILING_AMBIGUOUS_RE.test(name)) {
86
+ throw new ClientValidationError("Project library rename target must not start or end with whitespace or a trailing dot.", "validation_failed", undefined, { newName: name, });
87
+ }
88
+ return name;
89
+ }
90
+ /**
91
+ * Validate a destination folder path for a library move. '/' (the library
92
+ * root) is valid; any other path must pass {@link validateLibraryPath}.
93
+ * Returns the canonical destination ('/' or '/<path>').
94
+ */
95
+ export function validateLibraryDestinationPath(path) {
9
96
  if (path === "/")
10
97
  return "/";
11
- return `/${normalizeLibraryPath(path)}`;
98
+ return `/${validateLibraryPath(path)}`;
12
99
  }
13
- function encodeLibraryPath(path) {
14
- const normalized = normalizeLibraryPath(path);
100
+ /** Validate and URL-encode each segment of a library path for endpoint use. */
101
+ export function encodeLibraryPath(path) {
102
+ const normalized = validateLibraryPath(path);
15
103
  return normalized
16
104
  .split("/")
17
105
  .map((segment) => encodeURIComponent(segment))
18
106
  .join("/");
19
107
  }
108
+ const DEFAULT_DIFF_MAX_LINES = 200;
109
+ const DIFF_CONTEXT_LINES = 3;
110
+ const LCS_WORK_LIMIT = 400_000;
111
+ function decodeUtf8(bytes) {
112
+ for (let i = 0; i < Math.min(bytes.length, 8192); i++) {
113
+ if (bytes[i] === 0)
114
+ return { text: "", binary: true, };
115
+ }
116
+ try {
117
+ return { text: new TextDecoder("utf-8", { fatal: true, }).decode(bytes), binary: false, };
118
+ }
119
+ catch {
120
+ return { text: "", binary: true, };
121
+ }
122
+ }
123
+ function splitLines(text) {
124
+ return text.split("\n");
125
+ }
126
+ /**
127
+ * Sequence diff via LCS dynamic programming, capped at LCS_WORK_LIMIT cell
128
+ * writes. Beyond the cap the middle block degrades to a single
129
+ * remove-all/add-all edit script, which is always a correct (if coarser)
130
+ * description of the change.
131
+ */
132
+ function editScript(before, after) {
133
+ const n = before.length;
134
+ const m = after.length;
135
+ if (n * m > LCS_WORK_LIMIT) {
136
+ return [
137
+ ...before.map((line) => ({ type: "delete", line, })),
138
+ ...after.map((line) => ({ type: "insert", line, })),
139
+ ];
140
+ }
141
+ const width = m + 1;
142
+ const table = new Uint32Array((n + 1) * width);
143
+ for (let i = n - 1; i >= 0; i--) {
144
+ for (let j = m - 1; j >= 0; j--) {
145
+ if (before[i] === after[j]) {
146
+ table[i * width + j] = table[(i + 1) * width + j + 1] + 1;
147
+ }
148
+ else {
149
+ table[i * width + j] = Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
150
+ }
151
+ }
152
+ }
153
+ const ops = [];
154
+ let i = 0;
155
+ let j = 0;
156
+ while (i < n && j < m) {
157
+ if (before[i] === after[j]) {
158
+ ops.push({ type: "equal", line: before[i], });
159
+ i++;
160
+ j++;
161
+ }
162
+ else if (table[(i + 1) * width + j] >= table[i * width + j + 1]) {
163
+ ops.push({ type: "delete", line: before[i], });
164
+ i++;
165
+ }
166
+ else {
167
+ ops.push({ type: "insert", line: after[j], });
168
+ j++;
169
+ }
170
+ }
171
+ while (i < n) {
172
+ ops.push({ type: "delete", line: before[i], });
173
+ i++;
174
+ }
175
+ while (j < m) {
176
+ ops.push({ type: "insert", line: after[j], });
177
+ j++;
178
+ }
179
+ return ops;
180
+ }
181
+ /**
182
+ * Group a full edit script into unified-diff hunks: every change block is
183
+ * surrounded by up to DIFF_CONTEXT_LINES equal lines; two change blocks whose
184
+ * separating equal run is shorter than 2*DIFF_CONTEXT_LINES merge into one
185
+ * hunk, matching conventional unified diff output.
186
+ */
187
+ function buildHunks(ops) {
188
+ const blocks = [];
189
+ for (let z = 0; z < ops.length; z++) {
190
+ const op = ops[z];
191
+ const kind = op.type === "equal" ? "equal" : "change";
192
+ const last = blocks[blocks.length - 1];
193
+ if (last && last.kind === kind)
194
+ last.ops.push(op);
195
+ else
196
+ blocks.push({ kind, start: z, ops: [op,], });
197
+ }
198
+ // Prefix line counters for hunk numbering: before[z]/after[z] count the
199
+ // before/after lines consumed by ops[0..z).
200
+ const beforePrefix = new Uint32Array(ops.length + 1);
201
+ const afterPrefix = new Uint32Array(ops.length + 1);
202
+ for (let z = 0; z < ops.length; z++) {
203
+ const op = ops[z];
204
+ beforePrefix[z + 1] = beforePrefix[z] + (op.type === "insert" ? 0 : 1);
205
+ afterPrefix[z + 1] = afterPrefix[z] + (op.type === "delete" ? 0 : 1);
206
+ }
207
+ const hunks = [];
208
+ let index = 0;
209
+ while (index < blocks.length) {
210
+ const block = blocks[index];
211
+ if (block.kind === "equal") {
212
+ index++;
213
+ continue;
214
+ }
215
+ // Merge with later change blocks when the separating equal run is short.
216
+ let endBlock = index;
217
+ while (endBlock + 2 < blocks.length
218
+ && blocks[endBlock + 1].kind === "equal"
219
+ && blocks[endBlock + 1].ops.length <= DIFF_CONTEXT_LINES * 2
220
+ && blocks[endBlock + 2].kind === "change") {
221
+ endBlock += 2;
222
+ }
223
+ const hasBeforeContext = index > 0 && blocks[index - 1].kind === "equal";
224
+ const hasAfterContext = endBlock + 1 < blocks.length && blocks[endBlock + 1].kind === "equal";
225
+ const beforeBlock = hasBeforeContext ? blocks[index - 1] : undefined;
226
+ const afterBlock = hasAfterContext ? blocks[endBlock + 1] : undefined;
227
+ const contextBefore = beforeBlock?.ops.slice(-DIFF_CONTEXT_LINES) ?? [];
228
+ const contextAfter = afterBlock?.ops.slice(0, DIFF_CONTEXT_LINES) ?? [];
229
+ const windowStart = (beforeBlock?.start ?? block.start)
230
+ + (beforeBlock ? contextBefore.length : 0);
231
+ const windowEnd = (afterBlock?.start ?? blocks[endBlock].start + blocks[endBlock].ops.length)
232
+ + (afterBlock ? contextAfter.length : 0);
233
+ const body = [];
234
+ let beforeCount = 0;
235
+ let afterCount = 0;
236
+ for (let z = windowStart; z < windowEnd; z++) {
237
+ const op = ops[z];
238
+ if (op.type === "equal") {
239
+ body.push(` ${op.line}`);
240
+ beforeCount++;
241
+ afterCount++;
242
+ }
243
+ else if (op.type === "delete") {
244
+ body.push(`-${op.line}`);
245
+ beforeCount++;
246
+ }
247
+ else {
248
+ body.push(`+${op.line}`);
249
+ afterCount++;
250
+ }
251
+ }
252
+ hunks.push({
253
+ beforeStart: beforePrefix[windowStart] + 1,
254
+ beforeCount,
255
+ afterStart: afterPrefix[windowStart] + 1,
256
+ afterCount,
257
+ body,
258
+ });
259
+ index = endBlock + 1;
260
+ }
261
+ return hunks;
262
+ }
263
+ function hunkHeader(hunk) {
264
+ const beforeRange = hunk.beforeCount === 1
265
+ ? `${hunk.beforeStart}`
266
+ : `${hunk.beforeStart},${hunk.beforeCount}`;
267
+ const afterRange = hunk.afterCount === 1
268
+ ? `${hunk.afterStart}`
269
+ : `${hunk.afterStart},${hunk.afterCount}`;
270
+ return `@@ -${beforeRange} +${afterRange} @@`;
271
+ }
272
+ function formatUnifiedDiff(beforeText, afterText, path, maxLines) {
273
+ const before = splitLines(beforeText);
274
+ const after = splitLines(afterText);
275
+ // Trim the common prefix and suffix so the edit script only covers the
276
+ // changed middle (keeps the LCS working set small for large files).
277
+ let prefix = 0;
278
+ while (prefix < before.length && prefix < after.length && before[prefix] === after[prefix]) {
279
+ prefix++;
280
+ }
281
+ let suffix = 0;
282
+ while (suffix < before.length - prefix
283
+ && suffix < after.length - prefix
284
+ && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) {
285
+ suffix++;
286
+ }
287
+ const ops = [
288
+ ...before.slice(0, prefix).map((line) => ({ type: "equal", line, })),
289
+ ...editScript(before.slice(prefix, before.length - suffix), after.slice(prefix, after.length - suffix)),
290
+ ...before.slice(before.length - suffix).map((line) => ({ type: "equal", line, })),
291
+ ];
292
+ const added = ops.filter((op) => op.type === "insert").length;
293
+ const removed = ops.filter((op) => op.type === "delete").length;
294
+ const hunks = buildHunks(ops);
295
+ const header = `--- a/${path}\n+++ b/${path}\n`;
296
+ const lines = [];
297
+ for (const hunk of hunks) {
298
+ lines.push(hunkHeader(hunk), ...hunk.body);
299
+ }
300
+ // maxLines counts every reported diff line: the two file headers, the kept
301
+ // hunk lines, and (when truncated) the trailing "…" marker. The trailing
302
+ // newline of the final line is not counted as a line.
303
+ const cap = Math.max(1, maxLines);
304
+ const diffTruncated = lines.length + 2 + 1 > cap;
305
+ const kept = diffTruncated ? lines.slice(0, Math.max(0, cap - 3)) : lines;
306
+ const diffText = kept.length > 0
307
+ ? `${header}${kept.join("\n")}\n${diffTruncated ? "…\n" : ""}`
308
+ : header;
309
+ return { diff: diffText, diffTruncated, added, removed, };
310
+ }
20
311
  export class ProjectLibraryResource extends BaseResource {
21
- /** List the full project code-library contents tree. */
312
+ /**
313
+ * List the full project code-library contents tree. Items carry the DSS
314
+ * observed fields (path, size, mimeType, hasData, lastModified) when the
315
+ * server reports them; folders carry children, files do not.
316
+ */
22
317
  async listContents(projectKey) {
23
318
  return this.client.get(`/public/api/projects/${this.enc(projectKey)}/libraries/contents`);
24
319
  }
25
320
  /** Read a project code-library file as text. */
26
321
  async getFile(path, projectKey) {
27
- const res = await this.client.get(this.contentsPath(path, projectKey));
322
+ const valid = validateLibraryPath(path);
323
+ const res = await this.client.get(this.contentsPath(valid, projectKey));
28
324
  return res.data;
29
325
  }
30
326
  /** Read a project code-library file as bytes. */
31
327
  async getFileBytes(path, projectKey) {
32
- const res = await this.client.get(`${this.contentsPath(path, projectKey)}?dataEncoding=base64`);
328
+ const valid = validateLibraryPath(path);
329
+ const res = await this.client.get(`${this.contentsPath(valid, projectKey)}?dataEncoding=base64`);
33
330
  return Buffer.from(res.data, "base64");
34
331
  }
35
- /** Create an empty project code-library file. */
332
+ /**
333
+ * True when a file or folder exists at the given library path. Existence is
334
+ * verified against the live contents tree read from DSS.
335
+ */
336
+ async hasLibraryItem(path, projectKey) {
337
+ const valid = validateLibraryPath(path);
338
+ const segments = valid.split("/");
339
+ let items = await this.listContents(projectKey);
340
+ for (const segment of segments) {
341
+ const next = items.find((item) => item.name === segment);
342
+ if (!next)
343
+ return false;
344
+ items = next.children ?? [];
345
+ }
346
+ return true;
347
+ }
348
+ /** Create an empty project code-library file; refuses to overwrite an existing item. */
36
349
  async addFile(path, projectKey) {
37
- await this.client.postText(this.contentsPath(path, projectKey));
350
+ const valid = validateLibraryPath(path);
351
+ await this.assertLibraryFileAbsent(valid, projectKey);
352
+ await this.client.postText(this.contentsPath(valid, projectKey));
38
353
  }
39
- /** Create a project code-library folder. */
354
+ /** Create a project code-library folder; refuses to overwrite an existing item. */
40
355
  async addFolder(path, projectKey) {
41
- await this.client.postText(`/public/api/projects/${this.enc(projectKey)}/libraries/folders/${encodeLibraryPath(path)}`);
356
+ const valid = validateLibraryPath(path);
357
+ if (await this.hasLibraryItem(valid, projectKey)) {
358
+ throw this.alreadyExistsError(valid);
359
+ }
360
+ await this.client.postText(`/public/api/projects/${this.enc(projectKey)}/libraries/folders/${encodeLibraryPath(valid)}`);
361
+ }
362
+ /**
363
+ * Create or replace a project code-library file with raw content. `content`
364
+ * may be a string (UTF-8 text) or raw bytes (binary-safe); the exact bytes
365
+ * sent are returned as a SHA-256 digest and byte count.
366
+ *
367
+ * When `options.expectSha256` is set, the current remote content is read
368
+ * first and the write is refused with an `assertion_failed` error when its
369
+ * SHA-256 differs. The check is non-atomic (see
370
+ * {@link PROJECT_LIBRARY_CONCURRENCY_CONTROL}).
371
+ */
372
+ async addOrUpdateFile(path, content, projectKey, options) {
373
+ const valid = validateLibraryPath(path);
374
+ if (typeof content !== "string" && !(content instanceof Uint8Array)) {
375
+ throw new ClientValidationError("Project library file content must be a string or raw bytes.", "validation_failed");
376
+ }
377
+ const expectSha256 = options?.expectSha256;
378
+ if (expectSha256 !== undefined
379
+ && (typeof expectSha256 !== "string" || !EXPECT_SHA256_PATTERN.test(expectSha256))) {
380
+ throw new ClientValidationError("expectSha256 must be a 64-character SHA-256 hex digest.", "validation_failed", "Use the sha256 value returned by a project library read or diff.", { path: valid, });
381
+ }
382
+ let beforeSha256;
383
+ if (expectSha256 !== undefined) {
384
+ try {
385
+ const beforeBytes = await this.getFileBytes(valid, projectKey);
386
+ beforeSha256 = sha256Hex(beforeBytes);
387
+ }
388
+ catch (error) {
389
+ if (error instanceof DataikuError && error.category === "not_found") {
390
+ beforeSha256 = undefined;
391
+ }
392
+ else {
393
+ throw error;
394
+ }
395
+ }
396
+ if (beforeSha256 !== expectSha256.toLowerCase()) {
397
+ throw new ClientValidationError(`Project library file "${valid}" does not match --expect-sha256.`, "assertion_failed", "Re-read the file's sha256 and retry with the current digest. The stale-read check is non-atomic: a concurrent writer can still land between this read and the write.", {
398
+ path: valid,
399
+ expectedSha256: expectSha256.toLowerCase(),
400
+ actualSha256: beforeSha256 ?? null,
401
+ });
402
+ }
403
+ }
404
+ const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : content;
405
+ const sha256 = sha256Hex(bytes);
406
+ await this.postRawBody(this.contentsPath(valid, projectKey), content);
407
+ return {
408
+ path: valid,
409
+ bytes: bytes.length,
410
+ sha256,
411
+ ...(beforeSha256 !== undefined ? { beforeSha256, } : {}),
412
+ };
42
413
  }
43
- /** Create or replace a project code-library file with raw text content. */
44
- async addOrUpdateFile(path, content, projectKey) {
45
- await this.postRawText(this.contentsPath(path, projectKey), content);
414
+ /**
415
+ * Diff a library file against local content. The remote side is read as
416
+ * bytes (so binary files are detected instead of dumped); the reported
417
+ * unified diff is capped at `options.maxLines` (default 200) and never
418
+ * contains binary payloads.
419
+ */
420
+ async diffFile(path, local, projectKey, options) {
421
+ const valid = validateLibraryPath(path);
422
+ const maxLines = Math.max(1, Math.floor(options?.maxLines ?? DEFAULT_DIFF_MAX_LINES));
423
+ const localBytes = typeof local === "string" ? Buffer.from(local, "utf8") : local;
424
+ const localSha256 = sha256Hex(localBytes);
425
+ const localDecoded = typeof local === "string"
426
+ ? { text: local, binary: false, }
427
+ : decodeUtf8(localBytes);
428
+ let remoteBytes;
429
+ let remoteAbsent = false;
430
+ try {
431
+ remoteBytes = await this.getFileBytes(valid, projectKey);
432
+ }
433
+ catch (error) {
434
+ if (error instanceof DataikuError && error.category === "not_found") {
435
+ remoteAbsent = true;
436
+ }
437
+ else {
438
+ throw error;
439
+ }
440
+ }
441
+ const base = {
442
+ path: valid,
443
+ unchanged: false,
444
+ added: 0,
445
+ removed: 0,
446
+ diff: "",
447
+ diffTruncated: false,
448
+ localSha256,
449
+ localBytes: localBytes.length,
450
+ maxLines,
451
+ };
452
+ if (remoteAbsent) {
453
+ const rendered = localDecoded.binary
454
+ ? { added: 0, removed: 0, diff: "", diffTruncated: false, }
455
+ : formatUnifiedDiff("", localDecoded.text, valid, maxLines);
456
+ return {
457
+ ...base,
458
+ unchanged: localBytes.length === 0,
459
+ added: rendered.added,
460
+ removed: 0,
461
+ diff: rendered.diff,
462
+ diffTruncated: rendered.diffTruncated,
463
+ remoteAbsent: true,
464
+ ...(localDecoded.binary ? { binary: true, } : {}),
465
+ };
466
+ }
467
+ const remoteSha256 = sha256Hex(remoteBytes);
468
+ const remoteDecoded = decodeUtf8(remoteBytes);
469
+ const binary = remoteDecoded.binary || localDecoded.binary;
470
+ const unchanged = remoteSha256 === localSha256;
471
+ if (binary || unchanged) {
472
+ return {
473
+ ...base,
474
+ unchanged,
475
+ ...(binary ? { binary: true, } : {}),
476
+ remoteSha256,
477
+ remoteBytes: remoteBytes.length,
478
+ };
479
+ }
480
+ const rendered = formatUnifiedDiff(remoteDecoded.text, localDecoded.text, valid, maxLines);
481
+ return {
482
+ ...base,
483
+ unchanged: false,
484
+ added: rendered.added,
485
+ removed: rendered.removed,
486
+ diff: rendered.diff,
487
+ diffTruncated: rendered.diffTruncated,
488
+ remoteSha256,
489
+ remoteBytes: remoteBytes.length,
490
+ };
46
491
  }
47
- /** Delete a project code-library file. */
492
+ /** Delete a project code-library file or folder. */
48
493
  async deleteFile(path, projectKey) {
49
- await this.client.del(this.contentsPath(path, projectKey));
494
+ const valid = validateLibraryPath(path);
495
+ await this.client.del(this.contentsPath(valid, projectKey));
50
496
  }
51
497
  /** Rename a project code-library file or folder within its current parent folder. */
52
498
  async rename(path, newName, projectKey) {
499
+ const validPath = validateLibraryPath(path);
500
+ const validName = validateLibraryName(newName);
53
501
  const body = {
54
- oldPath: `/${normalizeLibraryPath(path)}`,
55
- newName,
502
+ oldPath: `/${validPath}`,
503
+ newName: validName,
56
504
  };
57
505
  await this.client.postText(`/public/api/projects/${this.enc(projectKey)}/libraries/contents-actions/rename/`, body);
58
506
  }
59
507
  /** Move a project code-library file or folder into another library folder. */
60
508
  async move(path, destinationFolderPath, projectKey) {
509
+ const validPath = validateLibraryPath(path);
510
+ const destination = validateLibraryDestinationPath(destinationFolderPath);
61
511
  const body = {
62
- oldPath: `/${normalizeLibraryPath(path)}`,
63
- newPath: normalizeLibraryDestinationPath(destinationFolderPath),
512
+ oldPath: `/${validPath}`,
513
+ newPath: destination,
64
514
  };
65
515
  await this.client.postText(`/public/api/projects/${this.enc(projectKey)}/libraries/contents-actions/move`, body);
66
516
  }
@@ -68,15 +518,34 @@ export class ProjectLibraryResource extends BaseResource {
68
518
  const projectKeyPart = this.enc(projectKey);
69
519
  return `/public/api/projects/${projectKeyPart}/libraries/contents/${encodeLibraryPath(path)}`;
70
520
  }
71
- async postRawText(path, content) {
521
+ async postRawBody(path, body) {
72
522
  // DSS library writes use dataikuapi's raw_body; route through the client's
73
523
  // transport so auth, retries, TLS options, and DSS error handling stay aligned.
524
+ // BodyInit rejects plain Uint8Array views, so non-Buffer bytes are copied
525
+ // into a Buffer (an ArrayBufferView BodyInit accepts).
526
+ const payload = body instanceof Uint8Array && !(body instanceof Buffer)
527
+ ? Buffer.from(body.buffer, body.byteOffset, body.byteLength)
528
+ : body;
74
529
  const rawClient = this.client;
75
530
  const res = await rawClient.fetchWithRetry(`${rawClient.baseUrl}${path}`, {
76
531
  method: "POST",
77
532
  headers: rawClient.getAnyHeaders(),
78
- body: content,
533
+ body: payload,
79
534
  });
80
535
  await res.text();
81
536
  }
537
+ alreadyExistsError(path) {
538
+ return new ClientValidationError(`Project library item "${path}" already exists.`, "validation_failed", "Refuse to overwrite an existing item; delete it first, or use the create-or-replace update to replace an existing file.", { path, });
539
+ }
540
+ async assertLibraryFileAbsent(path, projectKey) {
541
+ try {
542
+ await this.client.get(this.contentsPath(path, projectKey));
543
+ }
544
+ catch (error) {
545
+ if (error instanceof DataikuError && error.category === "not_found")
546
+ return;
547
+ throw error;
548
+ }
549
+ throw this.alreadyExistsError(path);
550
+ }
82
551
  }
@@ -1,5 +1,5 @@
1
1
  import type { FlowMapOptions, ProjectDetails, ProjectMetadata, ProjectSummary } from "../schemas.js";
2
- import type { NormalizedFlowMap } from "../utils/flow-map.js";
2
+ import type { AnalyzedFlowMap, FlowMapRendering } from "../utils/flow-analysis.js";
3
3
  import { BaseResource } from "./base.js";
4
4
  interface FlowMapTruncationSummary {
5
5
  truncated: boolean;
@@ -11,8 +11,9 @@ interface FlowMapTruncationSummary {
11
11
  edgeCountAfter: number;
12
12
  }
13
13
  export interface FlowMapResult {
14
- map: NormalizedFlowMap;
14
+ map: AnalyzedFlowMap;
15
15
  truncation: FlowMapTruncationSummary;
16
+ rendering?: FlowMapRendering;
16
17
  raw?: unknown;
17
18
  }
18
19
  export type ProjectLifecycleSettings = Record<string, unknown>;
@@ -86,8 +87,6 @@ export interface ProjectImportResult extends ProjectImportProcessResult {
86
87
  projectIncarnationHash?: string;
87
88
  }
88
89
  export declare class ProjectsResource extends BaseResource {
89
- private httpInternals;
90
- private postStream;
91
90
  /** Create a new project. */
92
91
  createProject(projectKey: string, name: string, ownerLogin: string, settings?: ProjectLifecycleSettings | null): Promise<Record<string, unknown>>;
93
92
  /** Delete a project using the lifecycle endpoint. */
@@ -127,6 +126,8 @@ export declare class ProjectsResource extends BaseResource {
127
126
  metadata(projectKey?: string): Promise<ProjectMetadata>;
128
127
  /** Get the raw flow graph for a project. */
129
128
  flow(projectKey?: string): Promise<unknown>;
129
+ /** Compute a stable fingerprint of flow nodes and edges, excluding visual metadata. */
130
+ topologyFingerprint(projectKey?: string): Promise<string>;
130
131
  /**
131
132
  * Build a normalized, optionally truncated flow map for a project.
132
133
  *