dataiku-sdk 0.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 (48) hide show
  1. package/bin/dss.js +2 -0
  2. package/dist/packages/types/src/index.d.ts +458 -0
  3. package/dist/packages/types/src/index.js +384 -0
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +689 -0
  6. package/dist/src/client.d.ts +89 -0
  7. package/dist/src/client.js +301 -0
  8. package/dist/src/errors.d.ts +29 -0
  9. package/dist/src/errors.js +141 -0
  10. package/dist/src/index.d.ts +20 -0
  11. package/dist/src/index.js +24 -0
  12. package/dist/src/resources/base.d.ts +7 -0
  13. package/dist/src/resources/base.js +12 -0
  14. package/dist/src/resources/code-envs.d.ts +8 -0
  15. package/dist/src/resources/code-envs.js +36 -0
  16. package/dist/src/resources/connections.d.ts +20 -0
  17. package/dist/src/resources/connections.js +77 -0
  18. package/dist/src/resources/datasets.d.ts +57 -0
  19. package/dist/src/resources/datasets.js +423 -0
  20. package/dist/src/resources/folders.d.ts +15 -0
  21. package/dist/src/resources/folders.js +58 -0
  22. package/dist/src/resources/jobs.d.ts +72 -0
  23. package/dist/src/resources/jobs.js +184 -0
  24. package/dist/src/resources/notebooks.d.ts +34 -0
  25. package/dist/src/resources/notebooks.js +75 -0
  26. package/dist/src/resources/projects.d.ts +38 -0
  27. package/dist/src/resources/projects.js +185 -0
  28. package/dist/src/resources/recipes.d.ts +35 -0
  29. package/dist/src/resources/recipes.js +281 -0
  30. package/dist/src/resources/scenarios.d.ts +26 -0
  31. package/dist/src/resources/scenarios.js +57 -0
  32. package/dist/src/resources/sql.d.ts +40 -0
  33. package/dist/src/resources/sql.js +40 -0
  34. package/dist/src/resources/variables.d.ts +10 -0
  35. package/dist/src/resources/variables.js +22 -0
  36. package/dist/src/schemas.d.ts +7 -0
  37. package/dist/src/schemas.js +6 -0
  38. package/dist/src/utils/deep-merge.d.ts +1 -0
  39. package/dist/src/utils/deep-merge.js +15 -0
  40. package/dist/src/utils/flow-map.d.ts +37 -0
  41. package/dist/src/utils/flow-map.js +296 -0
  42. package/dist/src/utils/pagination.d.ts +8 -0
  43. package/dist/src/utils/pagination.js +23 -0
  44. package/dist/src/utils/sanitize.d.ts +2 -0
  45. package/dist/src/utils/sanitize.js +16 -0
  46. package/package.json +47 -0
  47. package/packages/types/dist/index.d.ts +458 -0
  48. package/packages/types/dist/index.js +384 -0
@@ -0,0 +1,20 @@
1
+ import type { ConnectionSummary } from "../schemas.js";
2
+ import { BaseResource } from "./base.js";
3
+ export declare class ConnectionsResource extends BaseResource {
4
+ /**
5
+ * Returns sorted list of all connection names visible to the current user.
6
+ */
7
+ list(): Promise<string[]>;
8
+ /**
9
+ * Infers available connections.
10
+ *
11
+ * - fast (default): fetches the connection name list and maps to ConnectionSummary.
12
+ * Falls back to rich mode on any failure.
13
+ * - rich: inspects project datasets to derive connection metadata
14
+ * (types, managed flag, db schemas).
15
+ */
16
+ infer(opts?: {
17
+ mode?: "fast" | "rich";
18
+ projectKey?: string;
19
+ }): Promise<ConnectionSummary[]>;
20
+ }
@@ -0,0 +1,77 @@
1
+ import { BaseResource, } from "./base.js";
2
+ // ---------------------------------------------------------------------------
3
+ // Helpers
4
+ // ---------------------------------------------------------------------------
5
+ function normalizeConnectionNames(value) {
6
+ if (!Array.isArray(value))
7
+ return [];
8
+ return value
9
+ .filter((v) => typeof v === "string" && v.length > 0)
10
+ .sort();
11
+ }
12
+ async function inferRichConnectionsFromDatasets(client, projectEnc) {
13
+ const datasets = await client.get(`/public/api/projects/${projectEnc}/datasets/`);
14
+ const map = new Map();
15
+ for (const ds of datasets) {
16
+ const d = ds;
17
+ const params = d["params"];
18
+ const connection = params?.["connection"];
19
+ if (typeof connection !== "string" || connection.length === 0)
20
+ continue;
21
+ if (!map.has(connection)) {
22
+ map.set(connection, { types: new Set(), managed: false, dbSchemas: new Set(), });
23
+ }
24
+ const entry = map.get(connection);
25
+ const dsType = d["type"];
26
+ if (typeof dsType === "string" && dsType.length > 0)
27
+ entry.types.add(dsType);
28
+ if (d["managed"] === true)
29
+ entry.managed = true;
30
+ const schema = params?.["schema"];
31
+ if (typeof schema === "string" && schema.length > 0)
32
+ entry.dbSchemas.add(schema);
33
+ }
34
+ return [...map.entries(),]
35
+ .sort(([a,], [b,]) => a.localeCompare(b))
36
+ .map(([name, { types, managed, dbSchemas, },]) => ({
37
+ name,
38
+ types: [...types,].sort(),
39
+ managed,
40
+ dbSchemas: [...dbSchemas,].sort(),
41
+ }));
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // Resource
45
+ // ---------------------------------------------------------------------------
46
+ export class ConnectionsResource extends BaseResource {
47
+ /**
48
+ * Returns sorted list of all connection names visible to the current user.
49
+ */
50
+ async list() {
51
+ const raw = await this.client.get(`/public/api/connections/get-names/`);
52
+ return normalizeConnectionNames(raw);
53
+ }
54
+ /**
55
+ * Infers available connections.
56
+ *
57
+ * - fast (default): fetches the connection name list and maps to ConnectionSummary.
58
+ * Falls back to rich mode on any failure.
59
+ * - rich: inspects project datasets to derive connection metadata
60
+ * (types, managed flag, db schemas).
61
+ */
62
+ async infer(opts) {
63
+ const mode = opts?.mode ?? "fast";
64
+ const projectEnc = this.enc(opts?.projectKey);
65
+ if (mode === "rich") {
66
+ return inferRichConnectionsFromDatasets(this.client, projectEnc);
67
+ }
68
+ // fast — attempt name list, fall back to rich on any error
69
+ try {
70
+ const names = await this.list();
71
+ return names.map((name) => ({ name, }));
72
+ }
73
+ catch {
74
+ return inferRichConnectionsFromDatasets(this.client, projectEnc);
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,57 @@
1
+ import { BaseResource } from "./base.js";
2
+ import type { DatasetCreateOptions, DatasetDetails, DatasetSchema, DatasetSummary } from "../schemas.js";
3
+ /**
4
+ * Compare streamed TSV header columns against a known dataset schema.
5
+ * Returns an array of warning strings (empty if all columns match).
6
+ */
7
+ export declare function validateStreamColumns(headerRow: string[], expectedColumns: {
8
+ name: string;
9
+ }[]): string[];
10
+ export declare class DatasetsResource extends BaseResource {
11
+ /** List all datasets in a project. */
12
+ list(projectKey?: string): Promise<DatasetSummary[]>;
13
+ /** Get full dataset details. */
14
+ get(datasetName: string, projectKey?: string): Promise<DatasetDetails>;
15
+ /** Get dataset schema (column names and types). */
16
+ schema(datasetName: string, projectKey?: string): Promise<DatasetSchema>;
17
+ /**
18
+ * Preview dataset data as CSV text.
19
+ * Streams TSV from the API, converts to CSV, and returns up to `maxRows`
20
+ * data rows (plus header).
21
+ *
22
+ * If `validateColumns` is provided, the first TSV row (header) is checked
23
+ * against the column names. Mismatches emit a warning via onValidationWarning.
24
+ */
25
+ preview(datasetName: string, opts?: {
26
+ maxRows?: number;
27
+ projectKey?: string;
28
+ validateColumns?: {
29
+ name: string;
30
+ }[];
31
+ }): Promise<string>;
32
+ /** Get dataset metadata (tags, custom fields, checklists). */
33
+ metadata(datasetName: string, projectKey?: string): Promise<Record<string, unknown>>;
34
+ /**
35
+ * Download dataset data as a gzipped CSV file.
36
+ * Returns the absolute path of the written file.
37
+ */
38
+ download(datasetName: string, opts?: {
39
+ outputPath?: string;
40
+ projectKey?: string;
41
+ validateColumns?: {
42
+ name: string;
43
+ }[];
44
+ }): Promise<string>;
45
+ /**
46
+ * Create a new dataset.
47
+ *
48
+ * If `dsType` is not provided, a default is inferred from whether `table`
49
+ * is specified. On failure, the method retries once using a type inferred
50
+ * from existing datasets on the same connection.
51
+ */
52
+ create(opts: DatasetCreateOptions): Promise<Record<string, unknown>>;
53
+ /** Update a dataset by deep-merging a patch into the current definition. */
54
+ update(datasetName: string, data: Record<string, unknown>, projectKey?: string): Promise<void>;
55
+ /** Delete a dataset. */
56
+ delete(datasetName: string, projectKey?: string): Promise<void>;
57
+ }
@@ -0,0 +1,423 @@
1
+ import { createWriteStream, } from "node:fs";
2
+ import { resolve, } from "node:path";
3
+ import { Readable, Transform, } from "node:stream";
4
+ import { pipeline, } from "node:stream/promises";
5
+ import { createGzip, } from "node:zlib";
6
+ import { DataikuError, } from "../errors.js";
7
+ import { DatasetDetailsSchema, DatasetSchemaSchema, DatasetSummaryArraySchema, } from "../schemas.js";
8
+ import { deepMerge, } from "../utils/deep-merge.js";
9
+ import { sanitizeFileName, } from "../utils/sanitize.js";
10
+ import { BaseResource, } from "./base.js";
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers: TSV → CSV streaming conversion
13
+ // ---------------------------------------------------------------------------
14
+ function csvEscape(field) {
15
+ if (field.includes(",")
16
+ || field.includes('"')
17
+ || field.includes("\n")
18
+ || field.includes("\r")
19
+ || field.includes("\t")) {
20
+ return `"${field.replace(/"/g, '""')}"`;
21
+ }
22
+ return field;
23
+ }
24
+ function createTsvStreamState() {
25
+ return {
26
+ currentField: "",
27
+ currentRow: [],
28
+ inQuotes: false,
29
+ pendingQuoteInQuotes: false,
30
+ };
31
+ }
32
+ function consumeTsvChunk(text, state, onRow) {
33
+ let i = 0;
34
+ if (state.pendingQuoteInQuotes) {
35
+ state.pendingQuoteInQuotes = false;
36
+ const first = text[0];
37
+ if (first === '"') {
38
+ state.currentField += '"';
39
+ i = 1;
40
+ }
41
+ else if (first === "\t" || first === "\n" || first === "\r") {
42
+ state.inQuotes = false;
43
+ }
44
+ else if (first !== undefined) {
45
+ // Ambiguous terminal quote from previous chunk; keep it as data.
46
+ state.currentField += '"';
47
+ }
48
+ }
49
+ for (; i < text.length; i++) {
50
+ const ch = text[i];
51
+ if (state.inQuotes) {
52
+ if (ch === '"') {
53
+ const next = text[i + 1];
54
+ if (next === '"') {
55
+ state.currentField += '"';
56
+ i++;
57
+ continue;
58
+ }
59
+ if (next === undefined) {
60
+ state.pendingQuoteInQuotes = true;
61
+ continue;
62
+ }
63
+ if (next === "\t" || next === "\n" || next === "\r") {
64
+ state.inQuotes = false;
65
+ continue;
66
+ }
67
+ // Quote in the middle of quoted field text — keep it literal.
68
+ state.currentField += '"';
69
+ continue;
70
+ }
71
+ state.currentField += ch;
72
+ continue;
73
+ }
74
+ if (ch === '"' && state.currentField.length === 0) {
75
+ state.inQuotes = true;
76
+ continue;
77
+ }
78
+ if (ch === "\t") {
79
+ state.currentRow.push(state.currentField);
80
+ state.currentField = "";
81
+ continue;
82
+ }
83
+ if (ch === "\n") {
84
+ state.currentRow.push(state.currentField);
85
+ state.currentField = "";
86
+ const row = state.currentRow;
87
+ state.currentRow = [];
88
+ onRow(row);
89
+ continue;
90
+ }
91
+ if (ch === "\r") {
92
+ continue;
93
+ }
94
+ state.currentField += ch;
95
+ }
96
+ }
97
+ function flushTsvStream(state, onRow) {
98
+ if (state.pendingQuoteInQuotes) {
99
+ state.currentField += '"';
100
+ state.pendingQuoteInQuotes = false;
101
+ }
102
+ if (state.currentField.length === 0 && state.currentRow.length === 0)
103
+ return;
104
+ state.currentRow.push(state.currentField);
105
+ state.currentField = "";
106
+ const row = state.currentRow;
107
+ state.currentRow = [];
108
+ onRow(row);
109
+ }
110
+ function rowToCsv(row) {
111
+ return row.map((field) => csvEscape(field)).join(",");
112
+ }
113
+ function isBlankRow(row) {
114
+ return row.length === 1 && row[0].length === 0;
115
+ }
116
+ /**
117
+ * Compare streamed TSV header columns against a known dataset schema.
118
+ * Returns an array of warning strings (empty if all columns match).
119
+ */
120
+ export function validateStreamColumns(headerRow, expectedColumns) {
121
+ const warnings = [];
122
+ const headerSet = new Set(headerRow);
123
+ const expectedSet = new Set(expectedColumns.map((c) => c.name));
124
+ for (const col of expectedColumns) {
125
+ if (!headerSet.has(col.name)) {
126
+ warnings.push(`Missing expected column: "${col.name}"`);
127
+ }
128
+ }
129
+ for (const col of headerRow) {
130
+ if (!expectedSet.has(col)) {
131
+ warnings.push(`Unexpected column in stream: "${col}"`);
132
+ }
133
+ }
134
+ return warnings;
135
+ }
136
+ function emitCsvLineWithLimit(row, maxDataRows, emittedRows, onLine, onHeader) {
137
+ if (isBlankRow(row))
138
+ return false;
139
+ const isHeader = emittedRows.value === 0;
140
+ if (isHeader && onHeader)
141
+ onHeader(row);
142
+ if (!isHeader && emittedRows.value - 1 >= maxDataRows) {
143
+ return true;
144
+ }
145
+ onLine(rowToCsv(row));
146
+ emittedRows.value += 1;
147
+ if (!isHeader && emittedRows.value - 1 >= maxDataRows) {
148
+ return true;
149
+ }
150
+ return false;
151
+ }
152
+ async function collectPreviewCsv(body, maxDataRows, onHeader) {
153
+ const state = createTsvStreamState();
154
+ const emittedRows = { value: 0, };
155
+ const lines = [];
156
+ let done = false;
157
+ const nodeStream = Readable.fromWeb(body);
158
+ for await (const chunk of nodeStream) {
159
+ if (done)
160
+ break;
161
+ consumeTsvChunk(Buffer.from(chunk).toString("utf-8"), state, (row) => {
162
+ if (done)
163
+ return;
164
+ done = emitCsvLineWithLimit(row, maxDataRows, emittedRows, (line) => {
165
+ lines.push(line);
166
+ }, onHeader);
167
+ });
168
+ if (done) {
169
+ nodeStream.destroy();
170
+ break;
171
+ }
172
+ }
173
+ if (!done) {
174
+ flushTsvStream(state, (row) => {
175
+ if (done)
176
+ return;
177
+ done = emitCsvLineWithLimit(row, maxDataRows, emittedRows, (line) => {
178
+ lines.push(line);
179
+ }, onHeader);
180
+ });
181
+ }
182
+ return lines.join("\n");
183
+ }
184
+ function tsvToCsvTransform(maxDataRows, onHeader) {
185
+ const state = createTsvStreamState();
186
+ const emittedRows = { value: 0, };
187
+ let done = false;
188
+ const maxRows = Math.max(1, maxDataRows);
189
+ return new Transform({
190
+ transform(chunk, _encoding, callback) {
191
+ if (done) {
192
+ callback();
193
+ return;
194
+ }
195
+ consumeTsvChunk(chunk.toString("utf-8"), state, (row) => {
196
+ if (done)
197
+ return;
198
+ done = emitCsvLineWithLimit(row, maxRows, emittedRows, (line) => {
199
+ this.push(`${line}\n`);
200
+ }, onHeader);
201
+ });
202
+ if (done) {
203
+ this.push(null);
204
+ }
205
+ callback();
206
+ },
207
+ flush(callback) {
208
+ if (done) {
209
+ callback();
210
+ return;
211
+ }
212
+ flushTsvStream(state, (row) => {
213
+ if (done)
214
+ return;
215
+ done = emitCsvLineWithLimit(row, maxRows, emittedRows, (line) => {
216
+ this.push(`${line}\n`);
217
+ }, onHeader);
218
+ });
219
+ callback();
220
+ },
221
+ });
222
+ }
223
+ // ---------------------------------------------------------------------------
224
+ // Helpers: dataset creation
225
+ // ---------------------------------------------------------------------------
226
+ const DEFAULT_DATABASE_DATASET_TYPE = "Snowflake";
227
+ const DEFAULT_FILESYSTEM_DATASET_TYPE = "Filesystem";
228
+ function shouldRetryWithConnectionInferredType(error) {
229
+ if (!(error instanceof DataikuError))
230
+ return false;
231
+ if (error.category !== "validation" && error.category !== "unknown")
232
+ return false;
233
+ const detail = `${error.statusText ?? ""}\n${error.body ?? ""}`.toLowerCase();
234
+ return (detail.includes("connection")
235
+ || detail.includes("dataset type")
236
+ || detail.includes("invalid type")
237
+ || detail.includes("illegal argument"));
238
+ }
239
+ function buildDatasetCreateBody(opts) {
240
+ if (opts.table) {
241
+ const params = {
242
+ connection: opts.connection,
243
+ mode: "table",
244
+ table: opts.table,
245
+ };
246
+ if (opts.dbSchema)
247
+ params.schema = opts.dbSchema;
248
+ if (opts.catalog)
249
+ params.catalog = opts.catalog;
250
+ return {
251
+ projectKey: opts.projectKey,
252
+ name: opts.datasetName,
253
+ type: opts.dsType,
254
+ params,
255
+ managed: opts.managed ?? false,
256
+ };
257
+ }
258
+ return {
259
+ projectKey: opts.projectKey,
260
+ name: opts.datasetName,
261
+ type: opts.dsType,
262
+ params: {
263
+ connection: opts.connection,
264
+ path: `${opts.projectKey}/${opts.datasetName}`,
265
+ },
266
+ formatType: opts.formatType ?? "csv",
267
+ formatParams: opts.formatParams ?? {
268
+ style: "excel",
269
+ charset: "utf8",
270
+ separator: "\t",
271
+ quoteChar: '"',
272
+ escapeChar: "\\",
273
+ dateSerializationFormat: "ISO",
274
+ arrayMapFormat: "json",
275
+ parseHeaderRow: true,
276
+ compress: "gz",
277
+ },
278
+ managed: opts.managed ?? true,
279
+ };
280
+ }
281
+ // ---------------------------------------------------------------------------
282
+ // Resource
283
+ // ---------------------------------------------------------------------------
284
+ export class DatasetsResource extends BaseResource {
285
+ /** List all datasets in a project. */
286
+ async list(projectKey) {
287
+ const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/datasets/`);
288
+ return this.client.safeParse(DatasetSummaryArraySchema, raw, "datasets.list");
289
+ }
290
+ /** Get full dataset details. */
291
+ async get(datasetName, projectKey) {
292
+ const dsEnc = encodeURIComponent(datasetName);
293
+ const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/datasets/${dsEnc}`);
294
+ return this.client.safeParse(DatasetDetailsSchema, raw, "datasets.get");
295
+ }
296
+ /** Get dataset schema (column names and types). */
297
+ async schema(datasetName, projectKey) {
298
+ const dsEnc = encodeURIComponent(datasetName);
299
+ const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/datasets/${dsEnc}/schema`);
300
+ return this.client.safeParse(DatasetSchemaSchema, raw, "datasets.schema");
301
+ }
302
+ /**
303
+ * Preview dataset data as CSV text.
304
+ * Streams TSV from the API, converts to CSV, and returns up to `maxRows`
305
+ * data rows (plus header).
306
+ *
307
+ * If `validateColumns` is provided, the first TSV row (header) is checked
308
+ * against the column names. Mismatches emit a warning via onValidationWarning.
309
+ */
310
+ async preview(datasetName, opts) {
311
+ const maxRows = Math.max(1, Math.min(opts?.maxRows ?? 50, 500));
312
+ const dsEnc = encodeURIComponent(datasetName);
313
+ const res = await this.client.stream(`/public/api/projects/${this.enc(opts?.projectKey)}/datasets/${dsEnc}/data/?format=tsv-excel-header&limit=${maxRows}`);
314
+ const onHeader = opts?.validateColumns
315
+ ? (headerRow) => {
316
+ const warnings = validateStreamColumns(headerRow, opts.validateColumns);
317
+ if (warnings.length > 0) {
318
+ this.client.warn(`datasets.preview(${datasetName})`, warnings);
319
+ }
320
+ }
321
+ : undefined;
322
+ return collectPreviewCsv(res.body, maxRows, onHeader);
323
+ }
324
+ /** Get dataset metadata (tags, custom fields, checklists). */
325
+ async metadata(datasetName, projectKey) {
326
+ const dsEnc = encodeURIComponent(datasetName);
327
+ return this.client.get(`/public/api/projects/${this.enc(projectKey)}/datasets/${dsEnc}/metadata`);
328
+ }
329
+ /**
330
+ * Download dataset data as a gzipped CSV file.
331
+ * Returns the absolute path of the written file.
332
+ */
333
+ async download(datasetName, opts) {
334
+ const downloadLimit = 100_000;
335
+ const dsEnc = encodeURIComponent(datasetName);
336
+ const res = await this.client.stream(`/public/api/projects/${this.enc(opts?.projectKey)}/datasets/${dsEnc}/data/?format=tsv-excel-header&limit=${downloadLimit}`);
337
+ const safeDatasetName = sanitizeFileName(datasetName, "dataset");
338
+ const filePath = opts?.outputPath?.endsWith(".gz") || opts?.outputPath?.endsWith(".csv")
339
+ ? resolve(opts.outputPath)
340
+ : resolve(opts?.outputPath ?? process.cwd(), `${safeDatasetName}.csv.gz`);
341
+ const onHeader = opts?.validateColumns
342
+ ? (headerRow) => {
343
+ const warnings = validateStreamColumns(headerRow, opts.validateColumns);
344
+ if (warnings.length > 0) {
345
+ this.client.warn(`datasets.download(${datasetName})`, warnings);
346
+ }
347
+ }
348
+ : undefined;
349
+ const nodeStream = Readable.fromWeb(res.body);
350
+ const csvTransform = tsvToCsvTransform(downloadLimit, onHeader);
351
+ const gzip = createGzip();
352
+ const fileOut = createWriteStream(filePath);
353
+ await pipeline(nodeStream, csvTransform, gzip, fileOut);
354
+ return filePath;
355
+ }
356
+ /**
357
+ * Create a new dataset.
358
+ *
359
+ * If `dsType` is not provided, a default is inferred from whether `table`
360
+ * is specified. On failure, the method retries once using a type inferred
361
+ * from existing datasets on the same connection.
362
+ */
363
+ async create(opts) {
364
+ const pk = this.resolveProjectKey(opts.projectKey);
365
+ const enc = encodeURIComponent(pk);
366
+ const explicitType = opts.dsType;
367
+ let dsType = explicitType
368
+ ?? (opts.table ? DEFAULT_DATABASE_DATASET_TYPE : DEFAULT_FILESYSTEM_DATASET_TYPE);
369
+ let body = buildDatasetCreateBody({
370
+ projectKey: pk,
371
+ datasetName: opts.datasetName,
372
+ connection: opts.connection,
373
+ dsType,
374
+ table: opts.table,
375
+ dbSchema: opts.dbSchema,
376
+ catalog: opts.catalog,
377
+ formatType: opts.formatType,
378
+ formatParams: opts.formatParams,
379
+ managed: opts.managed,
380
+ });
381
+ try {
382
+ return await this.client.post(`/public/api/projects/${enc}/datasets/`, body);
383
+ }
384
+ catch (error) {
385
+ if (explicitType || !shouldRetryWithConnectionInferredType(error)) {
386
+ throw error;
387
+ }
388
+ // Infer type from existing datasets on the same connection.
389
+ const existing = await this.client.get(`/public/api/projects/${enc}/datasets/`);
390
+ const inferredType = existing.find((d) => d.params?.connection === opts.connection && d.type)?.type;
391
+ if (!inferredType || inferredType === dsType) {
392
+ throw error;
393
+ }
394
+ dsType = inferredType;
395
+ body = buildDatasetCreateBody({
396
+ projectKey: pk,
397
+ datasetName: opts.datasetName,
398
+ connection: opts.connection,
399
+ dsType,
400
+ table: opts.table,
401
+ dbSchema: opts.dbSchema,
402
+ catalog: opts.catalog,
403
+ formatType: opts.formatType,
404
+ formatParams: opts.formatParams,
405
+ managed: opts.managed,
406
+ });
407
+ return this.client.post(`/public/api/projects/${enc}/datasets/`, body);
408
+ }
409
+ }
410
+ /** Update a dataset by deep-merging a patch into the current definition. */
411
+ async update(datasetName, data, projectKey) {
412
+ const dsEnc = encodeURIComponent(datasetName);
413
+ const pkEnc = this.enc(projectKey);
414
+ const current = await this.client.get(`/public/api/projects/${pkEnc}/datasets/${dsEnc}`);
415
+ const merged = deepMerge(current, data);
416
+ await this.client.put(`/public/api/projects/${pkEnc}/datasets/${dsEnc}`, merged);
417
+ }
418
+ /** Delete a dataset. */
419
+ async delete(datasetName, projectKey) {
420
+ const dsEnc = encodeURIComponent(datasetName);
421
+ await this.client.del(`/public/api/projects/${this.enc(projectKey)}/datasets/${dsEnc}`);
422
+ }
423
+ }
@@ -0,0 +1,15 @@
1
+ import type { FolderDetails, FolderItem, FolderSummary } from "../schemas.js";
2
+ import { BaseResource } from "./base.js";
3
+ export declare class FoldersResource extends BaseResource {
4
+ list(projectKey?: string): Promise<FolderSummary[]>;
5
+ get(folderId: string, projectKey?: string): Promise<FolderDetails>;
6
+ contents(folderId: string, opts?: {
7
+ projectKey?: string;
8
+ }): Promise<FolderItem[]>;
9
+ download(folderId: string, path: string, opts?: {
10
+ localPath?: string;
11
+ projectKey?: string;
12
+ }): Promise<string>;
13
+ upload(folderId: string, path: string, localPath: string, projectKey?: string): Promise<void>;
14
+ deleteFile(folderId: string, path: string, projectKey?: string): Promise<void>;
15
+ }
@@ -0,0 +1,58 @@
1
+ import { createWriteStream, } from "node:fs";
2
+ import { resolve, } from "node:path";
3
+ import { Readable, } from "node:stream";
4
+ import { pipeline, } from "node:stream/promises";
5
+ import { FolderDetailsSchema, FolderItemArraySchema, FolderSummaryArraySchema, } from "../schemas.js";
6
+ import { sanitizeFileName, } from "../utils/sanitize.js";
7
+ import { BaseResource, } from "./base.js";
8
+ function normalizeRemotePath(path) {
9
+ return path.replace(/\\/g, "/");
10
+ }
11
+ function inferDownloadFileName(remotePath) {
12
+ const segments = remotePath.split("/").filter(Boolean);
13
+ const last = segments[segments.length - 1] ?? "file";
14
+ return sanitizeFileName(last, "file");
15
+ }
16
+ // ---------------------------------------------------------------------------
17
+ // Resource
18
+ // ---------------------------------------------------------------------------
19
+ export class FoldersResource extends BaseResource {
20
+ async list(projectKey) {
21
+ const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/managedfolders/`);
22
+ return this.client.safeParse(FolderSummaryArraySchema, raw, "folders.list");
23
+ }
24
+ async get(folderId, projectKey) {
25
+ const fEnc = encodeURIComponent(folderId);
26
+ const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/managedfolders/${fEnc}`);
27
+ return this.client.safeParse(FolderDetailsSchema, raw, "folders.get");
28
+ }
29
+ async contents(folderId, opts) {
30
+ const fEnc = encodeURIComponent(folderId);
31
+ const response = await this.client.get(`/public/api/projects/${this.enc(opts?.projectKey)}/managedfolders/${fEnc}/contents/`);
32
+ const items = response.items ?? [];
33
+ return this.client.safeParse(FolderItemArraySchema, items, "folders.contents");
34
+ }
35
+ async download(folderId, path, opts) {
36
+ const fEnc = encodeURIComponent(folderId);
37
+ const normalizedPath = normalizeRemotePath(path);
38
+ const pEnc = encodeURIComponent(normalizedPath);
39
+ const res = await this.client.stream(`/public/api/projects/${this.enc(opts?.projectKey)}/managedfolders/${fEnc}/contents/${pEnc}`);
40
+ const dest = opts?.localPath ?? resolve(process.cwd(), inferDownloadFileName(normalizedPath));
41
+ const nodeStream = Readable.fromWeb(res.body);
42
+ const fileOut = createWriteStream(dest);
43
+ await pipeline(nodeStream, fileOut);
44
+ return dest;
45
+ }
46
+ upload(folderId, path, localPath, projectKey) {
47
+ const fEnc = encodeURIComponent(folderId);
48
+ const normalizedPath = normalizeRemotePath(path);
49
+ const pEnc = encodeURIComponent(normalizedPath);
50
+ return this.client.upload(`/public/api/projects/${this.enc(projectKey)}/managedfolders/${fEnc}/contents/${pEnc}`, localPath);
51
+ }
52
+ deleteFile(folderId, path, projectKey) {
53
+ const fEnc = encodeURIComponent(folderId);
54
+ const normalizedPath = normalizeRemotePath(path);
55
+ const pEnc = encodeURIComponent(normalizedPath);
56
+ return this.client.del(`/public/api/projects/${this.enc(projectKey)}/managedfolders/${fEnc}/contents/${pEnc}`);
57
+ }
58
+ }