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.
Files changed (80) hide show
  1. package/LICENSE +107 -17
  2. package/README.md +144 -17
  3. package/dist/build-metadata.json +1 -1
  4. package/dist/packages/types/src/index.d.ts +39 -4
  5. package/dist/packages/types/src/index.js +31 -2
  6. package/dist/src/cli/commands/api-service.js +62 -16
  7. package/dist/src/cli/commands/bundle.js +49 -15
  8. package/dist/src/cli/commands/code-env.js +210 -49
  9. package/dist/src/cli/commands/code.d.ts +19 -0
  10. package/dist/src/cli/commands/code.js +87 -15
  11. package/dist/src/cli/commands/dataset.js +8 -3
  12. package/dist/src/cli/commands/flow-zone.js +33 -5
  13. package/dist/src/cli/commands/notebook.js +72 -41
  14. package/dist/src/cli/commands/project-git.js +8 -5
  15. package/dist/src/cli/commands/project-library.d.ts +21 -0
  16. package/dist/src/cli/commands/project-library.js +121 -50
  17. package/dist/src/cli/commands/project.js +16 -2
  18. package/dist/src/cli/commands/recipe.js +37 -12
  19. package/dist/src/cli/commands/sql.d.ts +10 -0
  20. package/dist/src/cli/commands/sql.js +39 -17
  21. package/dist/src/cli/commands/webapp.js +67 -13
  22. package/dist/src/cli/contract.d.ts +15 -0
  23. package/dist/src/cli/contract.js +870 -107
  24. package/dist/src/cli/env.d.ts +9 -0
  25. package/dist/src/cli/env.js +20 -2
  26. package/dist/src/cli/flags.js +9 -0
  27. package/dist/src/cli/helpers/cleanup.js +104 -3
  28. package/dist/src/cli/helpers/flow-zone.d.ts +3 -0
  29. package/dist/src/cli/helpers/flow-zone.js +33 -3
  30. package/dist/src/cli/helpers/recipe.d.ts +20 -0
  31. package/dist/src/cli/helpers/recipe.js +158 -3
  32. package/dist/src/cli/output.d.ts +2 -0
  33. package/dist/src/cli/output.js +33 -6
  34. package/dist/src/cli/runtime.d.ts +35 -1
  35. package/dist/src/cli/runtime.js +125 -18
  36. package/dist/src/cli/types.d.ts +7 -0
  37. package/dist/src/cli.js +135 -28
  38. package/dist/src/client.d.ts +60 -0
  39. package/dist/src/client.js +237 -42
  40. package/dist/src/errors.d.ts +8 -2
  41. package/dist/src/errors.js +40 -0
  42. package/dist/src/index.d.ts +4 -3
  43. package/dist/src/index.js +2 -1
  44. package/dist/src/resources/api-services.d.ts +48 -6
  45. package/dist/src/resources/api-services.js +54 -8
  46. package/dist/src/resources/bundles.d.ts +51 -6
  47. package/dist/src/resources/bundles.js +39 -9
  48. package/dist/src/resources/code-envs.d.ts +25 -3
  49. package/dist/src/resources/code-envs.js +104 -2
  50. package/dist/src/resources/datasets.d.ts +20 -0
  51. package/dist/src/resources/datasets.js +232 -52
  52. package/dist/src/resources/jobs.d.ts +35 -10
  53. package/dist/src/resources/jobs.js +92 -17
  54. package/dist/src/resources/notebooks.d.ts +77 -7
  55. package/dist/src/resources/notebooks.js +143 -19
  56. package/dist/src/resources/project-git.d.ts +12 -0
  57. package/dist/src/resources/project-git.js +39 -8
  58. package/dist/src/resources/project-library.d.ts +122 -7
  59. package/dist/src/resources/project-library.js +494 -25
  60. package/dist/src/resources/projects.d.ts +5 -2
  61. package/dist/src/resources/projects.js +18 -2
  62. package/dist/src/resources/scenarios.d.ts +30 -0
  63. package/dist/src/resources/scenarios.js +73 -27
  64. package/dist/src/resources/sql.d.ts +6 -0
  65. package/dist/src/resources/sql.js +22 -2
  66. package/dist/src/resources/webapps.d.ts +35 -4
  67. package/dist/src/resources/webapps.js +54 -5
  68. package/dist/src/schemas.d.ts +2 -2
  69. package/dist/src/schemas.js +1 -1
  70. package/dist/src/skill.d.ts +9 -0
  71. package/dist/src/skill.js +37 -4
  72. package/dist/src/utils/flow-analysis.d.ts +53 -0
  73. package/dist/src/utils/flow-analysis.js +382 -0
  74. package/dist/src/utils/project-archive.d.ts +41 -2
  75. package/dist/src/utils/project-archive.js +159 -16
  76. package/package.json +5 -5
  77. package/packages/types/dist/index.d.ts +39 -4
  78. package/packages/types/dist/index.js +31 -2
  79. package/plugin.json +2 -2
  80. package/skills/dataiku-dss/SKILL.md +75 -48
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as nodePath from "node:path";
3
3
  import { Readable, Transform, } from "node:stream";
4
4
  import { pipeline, } from "node:stream/promises";
5
+ import { StringDecoder, } from "node:string_decoder";
5
6
  import { createGzip, } from "node:zlib";
6
7
  import { ClientValidationError, DataikuError, } from "../errors.js";
7
8
  import { DatasetDetailsSchema, DatasetSchemaSchema, DatasetSummaryArraySchema, } from "../schemas.js";
@@ -12,7 +13,52 @@ import { resolveAdminManagedStorageConnection, } from "./connections.js";
12
13
  // ---------------------------------------------------------------------------
13
14
  // Helpers: TSV → CSV streaming conversion
14
15
  // ---------------------------------------------------------------------------
15
- function csvEscape(field) {
16
+ /**
17
+ * Decoded-byte budgets for the streaming TSV parser. Each budget fails closed
18
+ * — the response is cancelled and any partial download removed — so a dataset
19
+ * holding one oversized field, an unterminated quoted row, or a huge response
20
+ * can never accumulate unbounded memory or CPU before row limits apply.
21
+ */
22
+ const TSV_MAX_FIELD_BYTES = 16 * 1024 * 1024; // 16 MiB per decoded field
23
+ const TSV_MAX_ROW_BYTES = 64 * 1024 * 1024; // 64 MiB per decoded row
24
+ const TSV_MAX_RESPONSE_BYTES = 16 * 1024 * 1024 * 1024; // 16 GiB per decoded response
25
+ function formatLimitBytes(bytes) {
26
+ if (bytes >= 1024 * 1024 * 1024)
27
+ return `${bytes / (1024 * 1024 * 1024)} GiB`;
28
+ if (bytes >= 1024 * 1024)
29
+ return `${bytes / (1024 * 1024)} MiB`;
30
+ return `${bytes} B`;
31
+ }
32
+ function tsvLimitError(what, limit) {
33
+ return new DataikuError(0, "Dataset TSV Limit", `Dataset TSV ${what} exceeded ${formatLimitBytes(limit)}; streaming was aborted to guard against unbounded memory use.`);
34
+ }
35
+ /** Characters that evaluate a spreadsheet cell as a formula. */
36
+ const FORMULA_SIGILS = "=+-@";
37
+ /**
38
+ * True when the first effective character of `field` — the first character
39
+ * that is not leading whitespace/control — is a spreadsheet formula sigil.
40
+ * Spreadsheet applications evaluate such cells even when CSV-quoted, so an
41
+ * untrusted dataset producer could plant formulas that execute on open.
42
+ */
43
+ function hasFormulaPrefix(field) {
44
+ for (let i = 0; i < field.length; i++) {
45
+ const code = field.charCodeAt(i);
46
+ if (code <= 0x20 || (code >= 0x7f && code <= 0x9f))
47
+ continue;
48
+ return FORMULA_SIGILS.includes(field[i]);
49
+ }
50
+ return false;
51
+ }
52
+ /**
53
+ * Escape one TSV field for CSV output. In spreadsheet-safe mode (the default
54
+ * for dataset downloads) a field whose first effective character is a formula
55
+ * sigil gets a leading apostrophe so spreadsheet importers show text instead
56
+ * of executing it; `rawData: true` (CLI `--raw-data`) preserves exact bytes.
57
+ */
58
+ function csvEscape(field, spreadsheetSafe) {
59
+ if (spreadsheetSafe && hasFormulaPrefix(field)) {
60
+ field = `'${field}`;
61
+ }
16
62
  if (field.includes(",")
17
63
  || field.includes('"')
18
64
  || field.includes("\n")
@@ -22,21 +68,56 @@ function csvEscape(field) {
22
68
  }
23
69
  return field;
24
70
  }
25
- function createTsvStreamState() {
71
+ function createTsvStreamState(limits = {
72
+ maxFieldBytes: TSV_MAX_FIELD_BYTES,
73
+ maxRowBytes: TSV_MAX_ROW_BYTES,
74
+ maxResponseBytes: TSV_MAX_RESPONSE_BYTES,
75
+ }) {
26
76
  return {
27
77
  currentField: "",
28
78
  currentRow: [],
29
79
  inQuotes: false,
30
80
  pendingQuoteInQuotes: false,
81
+ fieldBytes: 0,
82
+ rowBytes: 0,
83
+ responseBytes: 0,
84
+ limits,
31
85
  };
32
86
  }
87
+ /** Append decoded field data, bounding field/row bytes and failing closed. */
88
+ function appendFieldText(state, text) {
89
+ if (text.length === 0)
90
+ return;
91
+ const bytes = Buffer.byteLength(text, "utf-8");
92
+ state.fieldBytes += bytes;
93
+ state.rowBytes += bytes;
94
+ if (state.fieldBytes > state.limits.maxFieldBytes) {
95
+ throw tsvLimitError("field", state.limits.maxFieldBytes);
96
+ }
97
+ if (state.rowBytes > state.limits.maxRowBytes) {
98
+ throw tsvLimitError("row", state.limits.maxRowBytes);
99
+ }
100
+ state.currentField += text;
101
+ }
102
+ /** Close a field at a tab/newline delimiter and reset the field byte count. */
103
+ function endField(state) {
104
+ state.currentRow.push(state.currentField);
105
+ state.currentField = "";
106
+ state.fieldBytes = 0;
107
+ state.rowBytes += 1; // count the delimiter byte
108
+ }
33
109
  function consumeTsvChunk(text, state, onRow) {
110
+ // Response budget: bound total decoded bytes regardless of row structure.
111
+ state.responseBytes += Buffer.byteLength(text, "utf-8");
112
+ if (state.responseBytes > state.limits.maxResponseBytes) {
113
+ throw tsvLimitError("response", state.limits.maxResponseBytes);
114
+ }
34
115
  let i = 0;
35
116
  if (state.pendingQuoteInQuotes) {
36
117
  state.pendingQuoteInQuotes = false;
37
118
  const first = text[0];
38
119
  if (first === '"') {
39
- state.currentField += '"';
120
+ appendFieldText(state, '"');
40
121
  i = 1;
41
122
  }
42
123
  else if (first === "\t" || first === "\n" || first === "\r") {
@@ -44,72 +125,102 @@ function consumeTsvChunk(text, state, onRow) {
44
125
  }
45
126
  else if (first !== undefined) {
46
127
  // Ambiguous terminal quote from previous chunk; keep it as data.
47
- state.currentField += '"';
128
+ appendFieldText(state, '"');
48
129
  }
49
130
  }
50
- for (; i < text.length; i++) {
51
- const ch = text[i];
131
+ while (i < text.length) {
52
132
  if (state.inQuotes) {
53
- if (ch === '"') {
54
- const next = text[i + 1];
55
- if (next === '"') {
56
- state.currentField += '"';
57
- i++;
58
- continue;
59
- }
60
- if (next === undefined) {
61
- state.pendingQuoteInQuotes = true;
62
- continue;
63
- }
64
- if (next === "\t" || next === "\n" || next === "\r") {
65
- state.inQuotes = false;
66
- continue;
67
- }
68
- // Quote in the middle of quoted field text — keep it literal.
69
- state.currentField += '"';
133
+ const quoteIdx = text.indexOf('"', i);
134
+ if (quoteIdx === -1) {
135
+ appendFieldText(state, text.slice(i));
136
+ break;
137
+ }
138
+ if (quoteIdx > i)
139
+ appendFieldText(state, text.slice(i, quoteIdx));
140
+ const next = text[quoteIdx + 1];
141
+ if (next === '"') {
142
+ appendFieldText(state, '"');
143
+ i = quoteIdx + 2;
144
+ continue;
145
+ }
146
+ if (next === undefined) {
147
+ state.pendingQuoteInQuotes = true;
148
+ break;
149
+ }
150
+ if (next === "\t" || next === "\n" || next === "\r") {
151
+ state.inQuotes = false;
152
+ i = quoteIdx + 1;
70
153
  continue;
71
154
  }
72
- state.currentField += ch;
155
+ // Quote in the middle of quoted field text — keep it literal.
156
+ appendFieldText(state, '"');
157
+ i = quoteIdx + 1;
73
158
  continue;
74
159
  }
75
- if (ch === '"' && state.currentField.length === 0) {
76
- state.inQuotes = true;
160
+ // Scan ahead in whole runs to the next structural character, so field
161
+ // data is appended in slices instead of one character at a time.
162
+ let special = -1;
163
+ for (let j = i; j < text.length; j++) {
164
+ const ch = text[j];
165
+ if (ch === '"' || ch === "\t" || ch === "\n" || ch === "\r") {
166
+ special = j;
167
+ break;
168
+ }
169
+ }
170
+ const runEnd = special === -1 ? text.length : special;
171
+ if (runEnd > i) {
172
+ appendFieldText(state, text.slice(i, runEnd));
173
+ i = runEnd;
174
+ }
175
+ if (special === -1)
176
+ break;
177
+ const ch = text[special];
178
+ if (ch === '"') {
179
+ if (state.currentField.length === 0) {
180
+ state.inQuotes = true;
181
+ }
182
+ else {
183
+ // Quote in the middle of unquoted field text — keep it literal.
184
+ appendFieldText(state, '"');
185
+ }
186
+ i = special + 1;
77
187
  continue;
78
188
  }
79
189
  if (ch === "\t") {
80
- state.currentRow.push(state.currentField);
81
- state.currentField = "";
190
+ endField(state);
191
+ i = special + 1;
82
192
  continue;
83
193
  }
84
194
  if (ch === "\n") {
85
- state.currentRow.push(state.currentField);
86
- state.currentField = "";
195
+ endField(state);
196
+ i = special + 1;
87
197
  const row = state.currentRow;
88
198
  state.currentRow = [];
199
+ state.rowBytes = 0;
89
200
  onRow(row);
90
201
  continue;
91
202
  }
92
- if (ch === "\r") {
93
- continue;
94
- }
95
- state.currentField += ch;
203
+ // "\r" — carriage return: skipped, as in the original parser.
204
+ i = special + 1;
96
205
  }
97
206
  }
98
207
  function flushTsvStream(state, onRow) {
99
208
  if (state.pendingQuoteInQuotes) {
100
- state.currentField += '"';
209
+ appendFieldText(state, '"');
101
210
  state.pendingQuoteInQuotes = false;
102
211
  }
103
212
  if (state.currentField.length === 0 && state.currentRow.length === 0)
104
213
  return;
105
214
  state.currentRow.push(state.currentField);
106
215
  state.currentField = "";
216
+ state.fieldBytes = 0;
107
217
  const row = state.currentRow;
108
218
  state.currentRow = [];
219
+ state.rowBytes = 0;
109
220
  onRow(row);
110
221
  }
111
- function rowToCsv(row) {
112
- return row.map((field) => csvEscape(field)).join(",");
222
+ function rowToCsv(row, spreadsheetSafe) {
223
+ return row.map((field) => csvEscape(field, spreadsheetSafe)).join(",");
113
224
  }
114
225
  function isBlankRow(row) {
115
226
  return row.length === 1 && row[0].length === 0;
@@ -154,6 +265,7 @@ async function readChunkWithTimeout(reader, remainingMs, timeoutMs, action) {
154
265
  }
155
266
  async function collectPreviewRows(body, maxDataRows, timeoutMs, onHeader) {
156
267
  const state = createTsvStreamState();
268
+ const decoder = new StringDecoder("utf-8");
157
269
  let columns;
158
270
  const rows = [];
159
271
  let done = false;
@@ -193,12 +305,22 @@ async function collectPreviewRows(body, maxDataRows, timeoutMs, onHeader) {
193
305
  const result = await readChunkWithTimeout(reader, remainingMs, timeoutMs, "preview");
194
306
  if (result.done)
195
307
  break;
196
- consumeTsvChunk(Buffer.from(result.value).toString("utf-8"), state, handleRow);
308
+ consumeTsvChunk(decoder.write(result.value), state, handleRow);
197
309
  }
198
- if (!done)
310
+ if (!done) {
311
+ const tail = decoder.end();
312
+ if (tail !== "")
313
+ consumeTsvChunk(tail, state, handleRow);
199
314
  flushTsvStream(state, handleRow);
315
+ }
200
316
  return { columns: columns ?? [], rows, truncated, };
201
317
  }
318
+ catch (error) {
319
+ // Fail closed on TSV byte-limit overflow: cancel the response instead
320
+ // of retaining partial decoded data.
321
+ void reader.cancel().catch(() => { });
322
+ throw error;
323
+ }
202
324
  finally {
203
325
  reader.releaseLock();
204
326
  }
@@ -211,6 +333,7 @@ async function collectPreviewRows(body, maxDataRows, timeoutMs, onHeader) {
211
333
  */
212
334
  async function collectTsvRowCount(body, stopAfter, timeoutMs) {
213
335
  const state = createTsvStreamState();
336
+ const decoder = new StringDecoder("utf-8");
214
337
  let headerSeen = false;
215
338
  let count = 0;
216
339
  let bounded = false;
@@ -251,18 +374,29 @@ async function collectTsvRowCount(body, stopAfter, timeoutMs) {
251
374
  const result = await readChunkWithTimeout(reader, remainingMs, timeoutMs, "row count assertion");
252
375
  if (result.done)
253
376
  break;
254
- consumeTsvChunk(Buffer.from(result.value).toString("utf-8"), state, handleRow);
377
+ consumeTsvChunk(decoder.write(result.value), state, handleRow);
255
378
  }
256
- if (!bounded)
379
+ if (!bounded) {
380
+ const tail = decoder.end();
381
+ if (tail !== "")
382
+ consumeTsvChunk(tail, state, handleRow);
257
383
  flushTsvStream(state, handleRow);
384
+ }
258
385
  return { count, bounded, };
259
386
  }
387
+ catch (error) {
388
+ // Fail closed on TSV byte-limit overflow: cancel the response instead
389
+ // of continuing to consume unbounded decoded data.
390
+ void reader.cancel().catch(() => { });
391
+ throw error;
392
+ }
260
393
  finally {
261
394
  reader.releaseLock();
262
395
  }
263
396
  }
264
- function tsvToCsvTransform(maxDataRows, stats, onHeader) {
397
+ function tsvToCsvTransform(maxDataRows, stats, onHeader, spreadsheetSafe = true) {
265
398
  const state = createTsvStreamState();
399
+ const decoder = new StringDecoder("utf-8");
266
400
  const maxRows = Math.max(1, maxDataRows);
267
401
  let headerSeen = false;
268
402
  let done = false;
@@ -276,7 +410,7 @@ function tsvToCsvTransform(maxDataRows, stats, onHeader) {
276
410
  headerSeen = true;
277
411
  columnCount = row.length;
278
412
  onHeader?.(row);
279
- push(`${rowToCsv(row)}\n`);
413
+ push(`${rowToCsv(row, spreadsheetSafe)}\n`);
280
414
  return;
281
415
  }
282
416
  if (isBlankRow(row) && columnCount !== 1)
@@ -286,7 +420,7 @@ function tsvToCsvTransform(maxDataRows, stats, onHeader) {
286
420
  done = true;
287
421
  return;
288
422
  }
289
- push(`${rowToCsv(row)}\n`);
423
+ push(`${rowToCsv(row, spreadsheetSafe)}\n`);
290
424
  stats.rows += 1;
291
425
  };
292
426
  return new Transform({
@@ -295,15 +429,35 @@ function tsvToCsvTransform(maxDataRows, stats, onHeader) {
295
429
  callback();
296
430
  return;
297
431
  }
298
- consumeTsvChunk(chunk.toString("utf-8"), state, (row) => handleRow(row, (line) => this.push(line)));
432
+ try {
433
+ consumeTsvChunk(decoder.write(chunk), state, (row) => handleRow(row, (line) => this.push(line)));
434
+ }
435
+ catch (error) {
436
+ // Byte-limit overflow: destroy the transform so the pipeline
437
+ // cancels the response and the partial file is removed.
438
+ callback(error instanceof Error ? error : new Error(String(error)));
439
+ return;
440
+ }
299
441
  if (done)
300
442
  this.push(null);
301
443
  callback();
302
444
  },
303
445
  flush(callback) {
304
- if (!done) {
446
+ if (done) {
447
+ callback();
448
+ return;
449
+ }
450
+ try {
451
+ const tail = decoder.end();
452
+ if (tail !== "") {
453
+ consumeTsvChunk(tail, state, (row) => handleRow(row, (line) => this.push(line)));
454
+ }
305
455
  flushTsvStream(state, (row) => handleRow(row, (line) => this.push(line)));
306
456
  }
457
+ catch (error) {
458
+ callback(error instanceof Error ? error : new Error(String(error)));
459
+ return;
460
+ }
307
461
  callback();
308
462
  },
309
463
  });
@@ -495,6 +649,10 @@ export class DatasetsResource extends BaseResource {
495
649
  *
496
650
  * If `validateColumns` is provided, the first TSV row (header) is checked
497
651
  * against the column names. Mismatches emit a warning via onValidationWarning.
652
+ *
653
+ * The stream is byte-bounded (16 MiB per field, 64 MiB per row, 16 GiB per
654
+ * response): an oversized field or unterminated quoted row aborts the
655
+ * preview, cancels the response, and fails with a DataikuError.
498
656
  */
499
657
  async preview(datasetName, opts) {
500
658
  const maxRows = Math.max(1, Math.min(opts?.maxRows ?? 50, 500));
@@ -529,6 +687,10 @@ export class DatasetsResource extends BaseResource {
529
687
  * fully scanned. Returns the observed count together with `exact` (false when
530
688
  * the probe cap was reached, i.e. the count is a lower bound) and whether the
531
689
  * count satisfies the assertion.
690
+ *
691
+ * Streaming is byte-bounded (16 MiB per field, 64 MiB per row, 16 GiB per
692
+ * response); oversized fields abort the count, cancel the response, and
693
+ * fail with a DataikuError.
532
694
  */
533
695
  async assertRowCount(datasetName, expected, projectKey) {
534
696
  if (!Number.isSafeInteger(expected) || expected < 0) {
@@ -555,6 +717,16 @@ export class DatasetsResource extends BaseResource {
555
717
  * (default 100k). Returns the written path, the number of data rows written,
556
718
  * whether the dataset had more rows than the cap (truncated), and the limit
557
719
  * used — so callers can detect truncation instead of silently getting a sample.
720
+ *
721
+ * Exported cells are spreadsheet-safe by default: headers/values whose
722
+ * first effective character is a spreadsheet formula sigil (`=`, `+`, `-`,
723
+ * `@`, including leading whitespace/control variants) are neutralized with
724
+ * a leading apostrophe so the file never executes formulas on open. Pass
725
+ * `rawData: true` to preserve exact bytes.
726
+ *
727
+ * Streaming is byte-bounded (16 MiB per field, 64 MiB per row, 16 GiB per
728
+ * response); on overflow the response is cancelled, the error is thrown,
729
+ * and any partially written download file is removed.
558
730
  */
559
731
  async download(datasetName, opts) {
560
732
  const limit = Math.max(1, opts?.limit ?? 100_000);
@@ -575,15 +747,23 @@ export class DatasetsResource extends BaseResource {
575
747
  const shouldGzip = filePath.endsWith(".gz");
576
748
  const stats = { rows: 0, truncated: false, };
577
749
  const nodeStream = Readable.fromWeb(res.body);
578
- const csvTransform = tsvToCsvTransform(limit, stats, onHeader);
750
+ const csvTransform = tsvToCsvTransform(limit, stats, onHeader, opts?.rawData !== true);
579
751
  fs.mkdirSync(nodePath.dirname(filePath), { recursive: true, });
580
752
  const fileOut = fs.createWriteStream(filePath);
581
- if (shouldGzip) {
582
- const gzip = createGzip();
583
- await pipeline(nodeStream, csvTransform, gzip, fileOut);
753
+ try {
754
+ if (shouldGzip) {
755
+ const gzip = createGzip();
756
+ await pipeline(nodeStream, csvTransform, gzip, fileOut);
757
+ }
758
+ else {
759
+ await pipeline(nodeStream, csvTransform, fileOut);
760
+ }
584
761
  }
585
- else {
586
- await pipeline(nodeStream, csvTransform, fileOut);
762
+ catch (error) {
763
+ // A failed export (byte-limit overflow, network error) must never
764
+ // leave a partial file that looks like a complete download.
765
+ fs.rmSync(filePath, { force: true, });
766
+ throw error;
587
767
  }
588
768
  return { path: filePath, rows: stats.rows, truncated: stats.truncated, limit, };
589
769
  }
@@ -14,6 +14,15 @@ export interface JobLogSummary {
14
14
  lines: string[];
15
15
  progress?: JobLogProgress;
16
16
  }
17
+ /** Why a requested job log could not be retrieved. */
18
+ export type JobLogUnavailableReason = "not_found" | "error";
19
+ /** Result of a completed job wait, including log-unavailable metadata. */
20
+ export type JobWaitOutcome = JobWaitResult & {
21
+ logSummary?: JobLogSummary;
22
+ logUnavailable?: JobLogUnavailableReason;
23
+ /** True when the job itself no longer exists on the server. */
24
+ removed?: boolean;
25
+ };
17
26
  export interface JobBuildTarget {
18
27
  id: string;
19
28
  type?: JobBuildTargetType;
@@ -57,17 +66,25 @@ export declare class JobsResource extends BaseResource {
57
66
  /**
58
67
  * Retrieve job log text.
59
68
  * Returns the last `maxLogLines` lines (default 500) from the tail.
60
- * Use `0` or `-1` to return the full log without truncation.
69
+ * Use `0` or `-1` to return the log without line truncation.
70
+ *
71
+ * The download is byte-bounded and deadline-covered: at most `maxLogBytes`
72
+ * (default 10 MiB) of the *most recent* log output is retained, so a
73
+ * collaborator-influenced oversized log cannot exhaust client memory while
74
+ * the line limit previously only applied after a full unbounded download.
75
+ * Logs up to the cap are returned in full.
61
76
  */
62
77
  log(jobId: string, opts?: {
63
78
  activity?: string;
64
79
  logId?: string;
65
80
  logFilter?: JobLogFilter;
66
81
  maxLogLines?: number;
82
+ maxLogBytes?: number;
67
83
  projectKey?: string;
68
84
  }): Promise<string>;
69
85
  logFromUrl(logUrl: string, opts?: {
70
86
  maxLogLines?: number;
87
+ maxLogBytes?: number;
71
88
  }): Promise<string>;
72
89
  /**
73
90
  * Start a build job for one or more dataset or managed-folder outputs.
@@ -87,22 +104,26 @@ export declare class JobsResource extends BaseResource {
87
104
  * Build one or more dataset or managed-folder outputs and wait for a terminal state.
88
105
  * Combines {@link buildOutputs} then {@link wait}.
89
106
  */
90
- buildAndWaitOutputs(targets: JobBuildTarget[], opts?: JobBuildAndWaitOptions): Promise<JobWaitResult & {
91
- logSummary?: JobLogSummary;
92
- }>;
107
+ buildAndWaitOutputs(targets: JobBuildTarget[], opts?: JobBuildAndWaitOptions): Promise<JobWaitOutcome>;
93
108
  /**
94
109
  * Build a dataset or managed folder and wait for the job to reach a terminal state.
95
110
  * Combines {@link build} then {@link wait}.
96
111
  */
97
- buildAndWait(targetId: string, opts?: JobBuildAndWaitOptions): Promise<JobWaitResult & {
98
- logSummary?: JobLogSummary;
99
- }>;
112
+ buildAndWait(targetId: string, opts?: JobBuildAndWaitOptions): Promise<JobWaitOutcome>;
100
113
  /**
101
114
  * Poll a job until it reaches a terminal state or times out.
102
115
  *
103
116
  * Adaptive polling doubles the interval every 3 polls when
104
117
  * `pollIntervalMs` is not explicitly set.
105
118
  *
119
+ * A terminal state observed from the status endpoint is authoritative:
120
+ * when log retrieval fails afterwards (e.g. `not_found` because logs were
121
+ * purged or the job was removed), `wait` still returns the terminal
122
+ * outcome instead of throwing, with `logUnavailable` holding the machine-
123
+ * readable reason and `removed` set once the job itself is probed and
124
+ * confirmed gone. Explicit `includeLogs`/`summary` requests stay honest:
125
+ * `log`/`logSummary` are only present when logs were actually retrieved.
126
+ *
106
127
  * On timeout, returns `{ success: false, ... }` rather than throwing.
107
128
  */
108
129
  wait(jobId: string, opts?: {
@@ -115,9 +136,13 @@ export declare class JobsResource extends BaseResource {
115
136
  summary?: boolean;
116
137
  timeoutMs?: number;
117
138
  projectKey?: string;
118
- }): Promise<JobWaitResult & {
119
- logSummary?: JobLogSummary;
120
- }>;
139
+ }): Promise<JobWaitOutcome>;
140
+ /**
141
+ * Probe whether a job still exists. Returns `true` when the server reports
142
+ * it as not found, `false` when it is still visible, and `undefined` when
143
+ * the probe outcome is ambiguous.
144
+ */
145
+ private probeJobRemoved;
121
146
  /** Request a job abort. */
122
147
  abort(jobId: string, projectKey?: string): Promise<void>;
123
148
  }