dataiku-sdk 0.2.3 → 0.3.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.
@@ -14,9 +14,9 @@ export type SafeParseResult<T> = {
14
14
  errors: string[];
15
15
  };
16
16
  /**
17
- * Validate `data` against a TypeBox schema without throwing.
18
- * Always returns the data (cast as T) — on mismatch, includes human-readable
19
- * error strings so callers can warn instead of crash.
17
+ * Validate `data` against a TypeBox schema without throwing, even when invalid
18
+ * values are not JSON-serializable. Always returns the original data (cast as T)
19
+ * and includes human-readable error strings so callers can warn instead of crash.
20
20
  */
21
21
  export declare function safeParseSchema<S extends TSchema>(schema: S, data: unknown): SafeParseResult<Static<S>>;
22
22
  export declare const ProjectSummarySchema: import("@sinclair/typebox").TObject<{
@@ -12,15 +12,65 @@ export function parseSchema(schema, data) {
12
12
  return data;
13
13
  }
14
14
  /**
15
- * Validate `data` against a TypeBox schema without throwing.
16
- * Always returns the data (cast as T) — on mismatch, includes human-readable
17
- * error strings so callers can warn instead of crash.
15
+ * Format invalid values for validation errors without throwing on BigInt, circular,
16
+ * function, symbol, or other non-JSON-serializable input.
17
+ */
18
+ function formatInvalidValue(value) {
19
+ if (value === undefined)
20
+ return "undefined";
21
+ if (typeof value === "bigint")
22
+ return `${value.toString()}n`;
23
+ if (typeof value === "symbol") {
24
+ return value.description === undefined ? "Symbol()" : `Symbol(${value.description})`;
25
+ }
26
+ if (typeof value === "function") {
27
+ return value.name ? `[Function ${value.name}]` : "[Function anonymous]";
28
+ }
29
+ const seen = new WeakSet();
30
+ try {
31
+ const json = JSON.stringify(value, (_key, nestedValue) => {
32
+ if (typeof nestedValue === "bigint")
33
+ return `${nestedValue.toString()}n`;
34
+ if (typeof nestedValue === "symbol") {
35
+ return nestedValue.description === undefined
36
+ ? "Symbol()"
37
+ : `Symbol(${nestedValue.description})`;
38
+ }
39
+ if (typeof nestedValue === "function") {
40
+ return nestedValue.name
41
+ ? `[Function ${nestedValue.name}]`
42
+ : "[Function anonymous]";
43
+ }
44
+ if (nestedValue !== null && typeof nestedValue === "object") {
45
+ if (seen.has(nestedValue))
46
+ return "[Circular]";
47
+ seen.add(nestedValue);
48
+ }
49
+ return nestedValue;
50
+ });
51
+ if (json !== undefined)
52
+ return json;
53
+ }
54
+ catch {
55
+ // Ignore serialization failures and fall back to a safer summary below.
56
+ }
57
+ try {
58
+ return Object.prototype.toString.call(value);
59
+ }
60
+ catch {
61
+ return "[Unformattable value]";
62
+ }
63
+ }
64
+ /**
65
+ * Validate `data` against a TypeBox schema without throwing, even when invalid
66
+ * values are not JSON-serializable. Always returns the original data (cast as T)
67
+ * and includes human-readable error strings so callers can warn instead of crash.
18
68
  */
19
69
  export function safeParseSchema(schema, data) {
20
70
  if (Value.Check(schema, data)) {
21
71
  return { success: true, data: data, };
22
72
  }
23
- const errors = [...Value.Errors(schema, data),].map((e) => `${e.path}: ${e.message} (got ${JSON.stringify(e.value)})`);
73
+ const errors = [...Value.Errors(schema, data),].map((e) => `${e.path}: ${e.message} (got ${formatInvalidValue(e.value)})`);
24
74
  return { success: false, data: data, errors, };
25
75
  }
26
76
  // ---------------------------------------------------------------------------
@@ -1,4 +1,11 @@
1
- export declare function validateCredentials(url: string, apiKey: string): Promise<{
1
+ import { DataikuError } from "./errors.js";
2
+ export interface CredentialValidationResult {
2
3
  valid: boolean;
3
4
  error?: string;
4
- }>;
5
+ dataikuError?: DataikuError;
6
+ }
7
+ export interface CredentialValidationOptions {
8
+ tlsRejectUnauthorized?: boolean;
9
+ caCertPath?: string;
10
+ }
11
+ export declare function validateCredentials(url: string, apiKey: string, options?: CredentialValidationOptions): Promise<CredentialValidationResult>;
package/dist/src/auth.js CHANGED
@@ -1,19 +1,21 @@
1
1
  import { DataikuClient, } from "./client.js";
2
2
  import { DataikuError, } from "./errors.js";
3
- export async function validateCredentials(url, apiKey) {
3
+ export async function validateCredentials(url, apiKey, options = {}) {
4
4
  try {
5
5
  const client = new DataikuClient({
6
6
  url,
7
7
  apiKey,
8
8
  requestTimeoutMs: 10_000,
9
9
  retryMaxAttempts: 1,
10
+ tlsRejectUnauthorized: options.tlsRejectUnauthorized,
11
+ caCertPath: options.caCertPath,
10
12
  });
11
13
  await client.projects.list();
12
14
  return { valid: true, };
13
15
  }
14
16
  catch (err) {
15
17
  if (err instanceof DataikuError) {
16
- return { valid: false, error: err.message, };
18
+ return { valid: false, error: err.message, dataikuError: err, };
17
19
  }
18
20
  return { valid: false, error: err instanceof Error ? err.message : String(err), };
19
21
  }
package/dist/src/cli.js CHANGED
@@ -42,9 +42,13 @@ function json(v) {
42
42
  return undefined;
43
43
  return JSON.parse(v);
44
44
  }
45
+ const SQL_QUERY_USAGE = "dss sql query [SQL | --sql QUERY | --sql-file PATH | --sql - | --stdin] (--connection CONN | --dataset FULL_NAME) [--database DB] [--project-key KEY]";
46
+ function readStdinText() {
47
+ return readFileSync(0, "utf-8");
48
+ }
45
49
  function jsonInput(flags) {
46
50
  if (flags["stdin"] === true) {
47
- return JSON.parse(readFileSync(0, "utf-8"));
51
+ return JSON.parse(readStdinText());
48
52
  }
49
53
  if (typeof flags["data-file"] === "string") {
50
54
  return JSON.parse(readFileSync(flags["data-file"], "utf-8"));
@@ -54,6 +58,62 @@ function jsonInput(flags) {
54
58
  }
55
59
  return undefined;
56
60
  }
61
+ function parseTlsRejectUnauthorizedEnv(value) {
62
+ if (value === undefined)
63
+ return undefined;
64
+ const normalized = value.trim().toLowerCase();
65
+ if (normalized === "0" || normalized === "false" || normalized === "no")
66
+ return false;
67
+ if (normalized === "1" || normalized === "true" || normalized === "yes")
68
+ return true;
69
+ return undefined;
70
+ }
71
+ function resolveTlsSettings(flags, saved) {
72
+ let tlsRejectUnauthorized = flags["insecure"] === true ? false : undefined;
73
+ let caCertPath = flags["ca-cert"];
74
+ tlsRejectUnauthorized ??= parseTlsRejectUnauthorizedEnv(process.env.NODE_TLS_REJECT_UNAUTHORIZED);
75
+ caCertPath ??= process.env.NODE_EXTRA_CA_CERTS;
76
+ if (tlsRejectUnauthorized === undefined) {
77
+ tlsRejectUnauthorized = saved?.tlsRejectUnauthorized;
78
+ }
79
+ caCertPath ??= saved?.caCertPath;
80
+ return { tlsRejectUnauthorized, caCertPath, };
81
+ }
82
+ function resolveSqlInput(args, flags) {
83
+ const sources = [];
84
+ if (typeof flags["sql"] === "string") {
85
+ sources.push({
86
+ label: flags["sql"] === "-" ? "--sql -" : "--sql",
87
+ read: () => flags["sql"] === "-" ? readStdinText() : String(flags["sql"]),
88
+ });
89
+ }
90
+ if (typeof flags["sql-file"] === "string") {
91
+ sources.push({
92
+ label: "--sql-file",
93
+ read: () => readFileSync(flags["sql-file"], "utf-8"),
94
+ });
95
+ }
96
+ if (flags["stdin"] === true) {
97
+ sources.push({ label: "--stdin", read: readStdinText, });
98
+ }
99
+ if (args.length > 1) {
100
+ throw new UsageError(`Expected at most one positional SQL argument. Quote the SQL or use --sql-file/--stdin.\nUsage: ${SQL_QUERY_USAGE}`);
101
+ }
102
+ if (args[0] !== undefined) {
103
+ sources.push({ label: "positional SQL", read: () => args[0], });
104
+ }
105
+ if (sources.length === 0) {
106
+ throw new UsageError(`SQL input is required. Usage: ${SQL_QUERY_USAGE}`);
107
+ }
108
+ if (sources.length > 1) {
109
+ throw new UsageError(`Choose exactly one SQL input source: --sql, --sql-file, --stdin, or one positional SQL argument. Usage: ${SQL_QUERY_USAGE}`);
110
+ }
111
+ const query = sources[0].read();
112
+ if (query.trim().length === 0) {
113
+ throw new UsageError(`SQL input from ${sources[0].label} must not be empty. Usage: ${SQL_QUERY_USAGE}`);
114
+ }
115
+ return query;
116
+ }
57
117
  async function resolveFolderId(client, nameOrId, flags) {
58
118
  return client.folders.resolveId(nameOrId, flags["project-key"]);
59
119
  }
@@ -150,7 +210,19 @@ function writeCommandResult(result, format) {
150
210
  // ---------------------------------------------------------------------------
151
211
  // Arg parsing
152
212
  // ---------------------------------------------------------------------------
153
- const BOOLEAN_FLAGS = new Set(["help", "verbose", "version", "stdin", "global", "list-agents",]);
213
+ const BOOLEAN_FLAGS = new Set([
214
+ "help",
215
+ "verbose",
216
+ "version",
217
+ "stdin",
218
+ "insecure",
219
+ "global",
220
+ "list-agents",
221
+ "include-raw",
222
+ "include-payload",
223
+ "include-logs",
224
+ "replace",
225
+ ]);
154
226
  const SHORT_FLAGS = {
155
227
  h: "help",
156
228
  v: "verbose",
@@ -161,7 +233,18 @@ const SHORT_FLAGS = {
161
233
  /** Long-flag aliases: these are normalized to the canonical name in parseArgs. */
162
234
  const FLAG_ALIASES = {
163
235
  project: "project-key",
236
+ "skip-tls-verify": "insecure",
237
+ "extra-ca-certs": "ca-cert",
164
238
  };
239
+ function isNegativeNumberToken(value) {
240
+ return value.startsWith("-") && Number.isFinite(Number(value));
241
+ }
242
+ function requireFlagValue(flagLabel, next) {
243
+ if (next === undefined || (next.startsWith("-") && !isNegativeNumberToken(next))) {
244
+ throw new UsageError(`Flag ${flagLabel} requires a value.`);
245
+ }
246
+ return next;
247
+ }
165
248
  function parseArgs(argv) {
166
249
  const positional = [];
167
250
  const flags = {};
@@ -179,19 +262,15 @@ function parseArgs(argv) {
179
262
  flags[FLAG_ALIASES[raw] ?? raw] = arg.slice(eqIdx + 1);
180
263
  }
181
264
  else {
182
- const flagName = FLAG_ALIASES[arg.slice(2)] ?? arg.slice(2);
265
+ const rawFlagName = arg.slice(2);
266
+ const flagName = FLAG_ALIASES[rawFlagName] ?? rawFlagName;
183
267
  if (BOOLEAN_FLAGS.has(flagName)) {
184
268
  flags[flagName] = true;
185
269
  }
186
270
  else {
187
- const next = argv[i + 1];
188
- if (next !== undefined && !next.startsWith("-")) {
189
- flags[flagName] = next;
190
- i++;
191
- }
192
- else {
193
- flags[flagName] = true;
194
- }
271
+ const next = requireFlagValue(`--${rawFlagName}`, argv[i + 1]);
272
+ flags[flagName] = next;
273
+ i++;
195
274
  }
196
275
  }
197
276
  }
@@ -202,14 +281,9 @@ function parseArgs(argv) {
202
281
  flags[long] = true;
203
282
  }
204
283
  else {
205
- const next = argv[i + 1];
206
- if (next !== undefined && !next.startsWith("-")) {
207
- flags[long] = next;
208
- i++;
209
- }
210
- else {
211
- flags[long] = true;
212
- }
284
+ const next = requireFlagValue(`-${arg[1]}`, argv[i + 1]);
285
+ flags[long] = next;
286
+ i++;
213
287
  }
214
288
  }
215
289
  else {
@@ -275,9 +349,10 @@ const commands = {
275
349
  return c.datasets.preview(a[0], {
276
350
  maxRows: num(f["max-rows"]),
277
351
  projectKey: f["project-key"],
352
+ timeoutMs: num(f["timeout"]),
278
353
  });
279
354
  },
280
- usage: "dss dataset preview <name> [--max-rows N] [--project-key KEY]",
355
+ usage: "dss dataset preview <name> [--max-rows N] [--project-key KEY] [--timeout MS]",
281
356
  },
282
357
  metadata: {
283
358
  handler: (c, a, f) => {
@@ -650,18 +725,22 @@ const commands = {
650
725
  },
651
726
  sql: {
652
727
  query: {
653
- handler: (c, _a, f) => {
654
- const query = f["sql"];
655
- if (!query)
656
- throw new UsageError("--sql is required. Usage: dss sql query --sql 'SELECT ...'");
728
+ handler: (c, a, f) => {
729
+ const query = resolveSqlInput(a, f);
730
+ const connection = f["connection"];
731
+ const datasetFullName = f["dataset"];
732
+ if ((connection ? 1 : 0) + (datasetFullName ? 1 : 0) !== 1) {
733
+ throw new UsageError(`Pass exactly one of --connection or --dataset. Usage: ${SQL_QUERY_USAGE}`);
734
+ }
657
735
  return c.sql.query({
658
736
  query,
659
- connection: f["connection"],
660
- datasetFullName: f["dataset"],
737
+ connection,
738
+ datasetFullName,
661
739
  database: f["database"],
740
+ projectKey: f["project-key"],
662
741
  });
663
742
  },
664
- usage: "dss sql query --sql 'SELECT ...' [--connection CONN] [--dataset FULL_NAME] [--database DB]",
743
+ usage: SQL_QUERY_USAGE,
665
744
  },
666
745
  },
667
746
  notebook: {
@@ -782,6 +861,8 @@ function printTopLevelHelp() {
782
861
  " --api-key KEY API key (env: DATAIKU_API_KEY)",
783
862
  " --project-key KEY Default project key (env: DATAIKU_PROJECT_KEY)",
784
863
  " --timeout MS Request timeout in ms (default: 30000)",
864
+ " --insecure Disable TLS certificate verification",
865
+ " --ca-cert PATH Extra PEM CA bundle (env: NODE_EXTRA_CA_CERTS)",
785
866
  "",
786
867
  "Resources:",
787
868
  ...RESOURCE_NAMES.map((r) => ` ${r}`),
@@ -866,6 +947,7 @@ function loadEnvFile() {
866
947
  const AUTH_ACTIONS = {
867
948
  login: {
868
949
  handler: async (flags) => {
950
+ const tlsSettings = resolveTlsSettings(flags);
869
951
  let { url, apiKey, projectKey, } = resolveCredentials(flags);
870
952
  if (!url || !apiKey) {
871
953
  if (!process.stdin.isTTY) {
@@ -883,41 +965,46 @@ const AUTH_ACTIONS = {
883
965
  if (!apiKey)
884
966
  throw new UsageError("API key is required.");
885
967
  process.stderr.write("Validating credentials... ");
886
- const result = await validateCredentials(url, apiKey);
968
+ const result = await validateCredentials(url, apiKey, tlsSettings);
887
969
  if (!result.valid) {
888
970
  process.stderr.write(`✗ Failed\n`);
971
+ if (result.dataikuError)
972
+ throw result.dataikuError;
889
973
  throw new DataikuError(0, "Authentication Failed", result.error ?? "Credential validation failed");
890
974
  }
891
- process.stderr.write("\u2713 Connected\n");
892
- saveCredentials({ url, apiKey, projectKey, });
975
+ process.stderr.write(" Connected\n");
976
+ saveCredentials({ url, apiKey, projectKey, ...tlsSettings, });
893
977
  process.stderr.write(`Credentials saved to ${getCredentialsPath()}\n`);
894
978
  },
895
- usage: "dss auth login [--url URL] [--api-key KEY] [--project-key KEY]",
979
+ usage: "dss auth login [--url URL] [--api-key KEY] [--project-key KEY] [--insecure] [--ca-cert PATH]",
896
980
  },
897
981
  status: {
898
- handler: async (_flags) => {
982
+ handler: async (flags) => {
899
983
  const creds = loadCredentials();
900
984
  if (!creds) {
901
985
  process.stderr.write("No saved credentials. Run: dss auth login\n");
902
986
  return;
903
987
  }
988
+ const tlsSettings = resolveTlsSettings(flags, creds);
904
989
  const lines = [
905
990
  `URL: ${creds.url}`,
906
991
  `API key: ${maskApiKey(creds.apiKey)}`,
907
992
  `Project key: ${creds.projectKey ?? "(not set)"}`,
993
+ `TLS verify: ${tlsSettings.tlsRejectUnauthorized === false ? "disabled" : "strict"}`,
994
+ `CA cert: ${tlsSettings.caCertPath ?? "(default trust store)"}`,
908
995
  ];
909
996
  for (const line of lines)
910
997
  process.stderr.write(`${line}\n`);
911
- const result = await validateCredentials(creds.url, creds.apiKey);
998
+ const result = await validateCredentials(creds.url, creds.apiKey, tlsSettings);
912
999
  if (result.valid) {
913
- process.stderr.write("Connection: \u2713 Valid\n");
1000
+ process.stderr.write("Connection: Valid\n");
914
1001
  }
915
1002
  else {
916
- process.stderr.write(`Connection: \u2717 Failed (${result.error ?? "unknown error"})\n`);
1003
+ process.stderr.write(`Connection: Failed (${result.error ?? "unknown error"})\n`);
917
1004
  }
918
1005
  process.stderr.write(`Config: ${getCredentialsPath()}\n`);
919
1006
  },
920
- usage: "dss auth status",
1007
+ usage: "dss auth status [--insecure] [--ca-cert PATH]",
921
1008
  },
922
1009
  logout: {
923
1010
  handler: async (_flags) => {
@@ -964,18 +1051,21 @@ function resolveCredentials(flags) {
964
1051
  let url = flags["url"];
965
1052
  let apiKey = flags["api-key"];
966
1053
  let projectKey = flags["project-key"];
1054
+ const saved = loadCredentials();
967
1055
  url ??= process.env.DATAIKU_URL;
968
1056
  apiKey ??= process.env.DATAIKU_API_KEY;
969
1057
  projectKey ??= process.env.DATAIKU_PROJECT_KEY;
970
- if (!url || !apiKey) {
971
- const saved = loadCredentials();
972
- if (saved) {
973
- url ||= saved.url;
974
- apiKey ||= saved.apiKey;
975
- projectKey ??= saved.projectKey;
976
- }
1058
+ if (saved) {
1059
+ url ||= saved.url;
1060
+ apiKey ||= saved.apiKey;
1061
+ projectKey ??= saved.projectKey;
977
1062
  }
978
- return { url: url ?? "", apiKey: apiKey ?? "", projectKey, };
1063
+ return {
1064
+ url: url ?? "",
1065
+ apiKey: apiKey ?? "",
1066
+ projectKey,
1067
+ ...resolveTlsSettings(flags, saved ?? undefined),
1068
+ };
979
1069
  }
980
1070
  // ---------------------------------------------------------------------------
981
1071
  // Main
@@ -1108,7 +1198,7 @@ async function main() {
1108
1198
  process.exit(0);
1109
1199
  }
1110
1200
  // Resolve credentials: flags > env > saved > .env
1111
- const { url, apiKey, projectKey, } = resolveCredentials(flags);
1201
+ const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
1112
1202
  if (!url) {
1113
1203
  throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL, pass --url, or run: dss auth login");
1114
1204
  }
@@ -1122,6 +1212,8 @@ async function main() {
1122
1212
  projectKey,
1123
1213
  verbose: flags["verbose"] === true,
1124
1214
  requestTimeoutMs,
1215
+ tlsRejectUnauthorized,
1216
+ caCertPath,
1125
1217
  });
1126
1218
  const args = positional.slice(2);
1127
1219
  const format = parseOutputFormat(flags["format"]);
@@ -23,6 +23,10 @@ export interface DataikuClientConfig {
23
23
  retryMaxAttempts?: number;
24
24
  /** Emit HTTP request/response logs to stderr for CLI debugging. */
25
25
  verbose?: boolean;
26
+ /** Override TLS certificate verification for HTTPS requests. */
27
+ tlsRejectUnauthorized?: boolean;
28
+ /** Extra PEM CA bundle to trust in addition to Bun's default trust store. */
29
+ caCertPath?: string;
26
30
  /**
27
31
  * Called when an API response fails schema validation but data is still usable.
28
32
  * Default: writes to stderr. Set to a throwing function for strict mode.
@@ -38,6 +42,7 @@ export declare class DataikuClient {
38
42
  private readonly requestTimeoutMs;
39
43
  private readonly retryMaxAttempts;
40
44
  private readonly verbose;
45
+ private readonly tlsOptions;
41
46
  private readonly onValidationWarning;
42
47
  private _projects?;
43
48
  private _datasets?;
@@ -62,6 +67,7 @@ export declare class DataikuClient {
62
67
  get sql(): SqlResource;
63
68
  get notebooks(): NotebooksResource;
64
69
  constructor(config: DataikuClientConfig);
70
+ getRequestTimeoutMs(): number;
65
71
  resolveProjectKey(paramValue?: string): string;
66
72
  get<T = unknown>(path: string): Promise<T>;
67
73
  getText(path: string): Promise<string>;
@@ -81,9 +87,10 @@ export declare class DataikuClient {
81
87
  */
82
88
  parse<S extends TSchema>(schema: S, data: unknown): Static<S>;
83
89
  /**
84
- * Validate raw data against a TypeBox schema without throwing.
85
- * Always returns the data. On mismatch, fires onValidationWarning callback
86
- * with the method name and error details.
90
+ * Validate raw data against a TypeBox schema without throwing, even when
91
+ * mismatched values are not JSON-serializable. Always returns the original
92
+ * data, and on mismatch emits onValidationWarning with the method name and
93
+ * error details. If the callback throws, that error still propagates.
87
94
  */
88
95
  safeParse<S extends TSchema>(schema: S, data: unknown, method: string): Static<S>;
89
96
  /** Emit a validation warning via the configured callback. */
@@ -1,3 +1,5 @@
1
+ import { readFileSync, } from "node:fs";
2
+ import { getCACertificates, } from "node:tls";
1
3
  import { Value, } from "@sinclair/typebox/value";
2
4
  import { safeParseSchema, } from "./schemas.js";
3
5
  import { classifyDataikuError, DataikuError, } from "./errors.js";
@@ -50,6 +52,27 @@ function buildRetryMetadata(method, enabled, maxAttempts, attempts, delaysMs, ti
50
52
  timedOut,
51
53
  };
52
54
  }
55
+ function buildFetchTlsOptions(config) {
56
+ const rejectUnauthorized = config.tlsRejectUnauthorized;
57
+ const caCertPath = config.caCertPath?.trim();
58
+ if (rejectUnauthorized === undefined && !caCertPath)
59
+ return undefined;
60
+ const tls = {};
61
+ if (rejectUnauthorized !== undefined)
62
+ tls.rejectUnauthorized = rejectUnauthorized;
63
+ if (caCertPath) {
64
+ try {
65
+ tls.ca = [...getCACertificates("default"), readFileSync(caCertPath, "utf-8"),];
66
+ }
67
+ catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ throw new Error(`Unable to read CA certificate bundle at ${caCertPath}: ${message}`, {
70
+ cause: error,
71
+ });
72
+ }
73
+ }
74
+ return tls;
75
+ }
53
76
  /* ------------------------------------------------------------------ */
54
77
  /* Client */
55
78
  /* ------------------------------------------------------------------ */
@@ -60,6 +83,7 @@ export class DataikuClient {
60
83
  requestTimeoutMs;
61
84
  retryMaxAttempts;
62
85
  verbose;
86
+ tlsOptions;
63
87
  onValidationWarning;
64
88
  /* Resource namespaces — lazily initialized to break circular imports */
65
89
  _projects;
@@ -120,8 +144,12 @@ export class DataikuClient {
120
144
  const rawMax = config.retryMaxAttempts ?? DEFAULT_RETRY_MAX_ATTEMPTS;
121
145
  this.retryMaxAttempts = Math.min(Math.max(1, rawMax), MAX_RETRY_ATTEMPTS_CAP);
122
146
  this.verbose = config.verbose === true;
147
+ this.tlsOptions = buildFetchTlsOptions(config);
123
148
  this.onValidationWarning = config.onValidationWarning ?? defaultValidationWarning;
124
149
  }
150
+ getRequestTimeoutMs() {
151
+ return this.requestTimeoutMs;
152
+ }
125
153
  /* ---- public: project key resolution ---- */
126
154
  resolveProjectKey(paramValue) {
127
155
  const pk = paramValue?.trim();
@@ -223,9 +251,10 @@ export class DataikuClient {
223
251
  return data;
224
252
  }
225
253
  /**
226
- * Validate raw data against a TypeBox schema without throwing.
227
- * Always returns the data. On mismatch, fires onValidationWarning callback
228
- * with the method name and error details.
254
+ * Validate raw data against a TypeBox schema without throwing, even when
255
+ * mismatched values are not JSON-serializable. Always returns the original
256
+ * data, and on mismatch emits onValidationWarning with the method name and
257
+ * error details. If the callback throws, that error still propagates.
229
258
  */
230
259
  safeParse(schema, data, method) {
231
260
  const result = safeParseSchema(schema, data);
@@ -270,7 +299,14 @@ export class DataikuClient {
270
299
  }, this.requestTimeoutMs);
271
300
  this.logVerbose(`${method} ${url}`);
272
301
  try {
273
- const res = await fetch(url, { ...init, method, signal: controller.signal, });
302
+ const requestInit = {
303
+ ...init,
304
+ method,
305
+ signal: controller.signal,
306
+ };
307
+ if (this.tlsOptions)
308
+ requestInit.tls = this.tlsOptions;
309
+ const res = await fetch(url, requestInit);
274
310
  this.logVerbose(`${method} ${url} → ${res.status} (${Date.now() - startedAt}ms)`);
275
311
  if (!res.ok) {
276
312
  const text = await res.text();
@@ -2,6 +2,8 @@ export interface DssCredentials {
2
2
  url: string;
3
3
  apiKey: string;
4
4
  projectKey?: string;
5
+ tlsRejectUnauthorized?: boolean;
6
+ caCertPath?: string;
5
7
  }
6
8
  export declare function getConfigDir(): string;
7
9
  export declare function getCredentialsPath(): string;
@@ -30,6 +30,10 @@ export function loadCredentials() {
30
30
  url: obj.url,
31
31
  apiKey: obj.apiKey,
32
32
  projectKey: typeof obj.projectKey === "string" ? obj.projectKey : undefined,
33
+ tlsRejectUnauthorized: typeof obj.tlsRejectUnauthorized === "boolean"
34
+ ? obj.tlsRejectUnauthorized
35
+ : undefined,
36
+ caCertPath: typeof obj.caCertPath === "string" ? obj.caCertPath : undefined,
33
37
  };
34
38
  }
35
39
  catch (err) {
@@ -44,6 +48,11 @@ export function saveCredentials(creds) {
44
48
  const data = { url: creds.url, apiKey: creds.apiKey, };
45
49
  if (creds.projectKey)
46
50
  data.projectKey = creds.projectKey;
51
+ if (creds.tlsRejectUnauthorized !== undefined) {
52
+ data.tlsRejectUnauthorized = creds.tlsRejectUnauthorized;
53
+ }
54
+ if (creds.caCertPath)
55
+ data.caCertPath = creds.caCertPath;
47
56
  writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
48
57
  chmodSync(path, 0o600);
49
58
  }
@@ -1,5 +1,5 @@
1
1
  export { DataikuClient, type DataikuClientConfig, } from "./client.js";
2
- export { validateCredentials, } from "./auth.js";
2
+ export { type CredentialValidationOptions, type CredentialValidationResult, validateCredentials, } from "./auth.js";
3
3
  export { deleteCredentials, type DssCredentials, getConfigDir, getCredentialsPath, loadCredentials, maskApiKey, saveCredentials, } from "./config.js";
4
4
  export { DataikuError, type DataikuErrorCategory, type DataikuErrorTaxonomy, type DataikuRetryMetadata, } from "./errors.js";
5
5
  export { CodeEnvsResource, } from "./resources/code-envs.js";
@@ -28,6 +28,7 @@ export declare class DatasetsResource extends BaseResource {
28
28
  validateColumns?: {
29
29
  name: string;
30
30
  }[];
31
+ timeoutMs?: number;
31
32
  }): Promise<string>;
32
33
  /** Get dataset metadata (tags, custom fields, checklists). */
33
34
  metadata(datasetName: string, projectKey?: string): Promise<Record<string, unknown>>;
@@ -149,37 +149,65 @@ function emitCsvLineWithLimit(row, maxDataRows, emittedRows, onLine, onHeader) {
149
149
  }
150
150
  return false;
151
151
  }
152
- async function collectPreviewCsv(body, maxDataRows, onHeader) {
152
+ function buildPreviewTimeoutError(timeoutMs) {
153
+ return new DataikuError(0, "Request Timeout", `Dataset preview timed out after ${timeoutMs}ms while waiting for rows.`);
154
+ }
155
+ async function readChunkWithTimeout(reader, remainingMs, timeoutMs) {
156
+ return new Promise((resolveChunk, rejectChunk) => {
157
+ const timer = setTimeout(() => {
158
+ void reader.cancel(buildPreviewTimeoutError(timeoutMs)).catch(() => { });
159
+ rejectChunk(buildPreviewTimeoutError(timeoutMs));
160
+ }, remainingMs);
161
+ reader.read().then((result) => {
162
+ clearTimeout(timer);
163
+ resolveChunk(result);
164
+ }, (error) => {
165
+ clearTimeout(timer);
166
+ rejectChunk(error);
167
+ });
168
+ });
169
+ }
170
+ async function collectPreviewCsv(body, maxDataRows, timeoutMs, onHeader) {
153
171
  const state = createTsvStreamState();
154
172
  const emittedRows = { value: 0, };
155
173
  const lines = [];
156
174
  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;
175
+ const startedAt = Date.now();
176
+ const reader = body.getReader();
177
+ try {
178
+ while (true) {
179
+ if (done) {
180
+ void reader.cancel().catch(() => { });
181
+ break;
182
+ }
183
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
184
+ if (remainingMs <= 0)
185
+ throw buildPreviewTimeoutError(timeoutMs);
186
+ const result = await readChunkWithTimeout(reader, remainingMs, timeoutMs);
187
+ if (result.done)
188
+ break;
189
+ consumeTsvChunk(Buffer.from(result.value).toString("utf-8"), state, (row) => {
190
+ if (done)
191
+ return;
192
+ done = emitCsvLineWithLimit(row, maxDataRows, emittedRows, (line) => {
193
+ lines.push(line);
194
+ }, onHeader);
195
+ });
196
+ }
197
+ if (!done) {
198
+ flushTsvStream(state, (row) => {
199
+ if (done)
200
+ return;
201
+ done = emitCsvLineWithLimit(row, maxDataRows, emittedRows, (line) => {
202
+ lines.push(line);
203
+ }, onHeader);
204
+ });
171
205
  }
206
+ return lines.join("\n");
172
207
  }
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
- });
208
+ finally {
209
+ reader.releaseLock();
181
210
  }
182
- return lines.join("\n");
183
211
  }
184
212
  function tsvToCsvTransform(maxDataRows, onHeader) {
185
213
  const state = createTsvStreamState();
@@ -309,6 +337,7 @@ export class DatasetsResource extends BaseResource {
309
337
  */
310
338
  async preview(datasetName, opts) {
311
339
  const maxRows = Math.max(1, Math.min(opts?.maxRows ?? 50, 500));
340
+ const timeoutMs = Math.max(1, opts?.timeoutMs ?? this.client.getRequestTimeoutMs());
312
341
  const dsEnc = encodeURIComponent(datasetName);
313
342
  const res = await this.client.stream(`/public/api/projects/${this.enc(opts?.projectKey)}/datasets/${dsEnc}/data/?format=tsv-excel-header&limit=${maxRows}`);
314
343
  const onHeader = opts?.validateColumns
@@ -319,7 +348,9 @@ export class DatasetsResource extends BaseResource {
319
348
  }
320
349
  }
321
350
  : undefined;
322
- return collectPreviewCsv(res.body, maxRows, onHeader);
351
+ if (!res.body)
352
+ return "";
353
+ return collectPreviewCsv(res.body, maxRows, timeoutMs, onHeader);
323
354
  }
324
355
  /** Get dataset metadata (tags, custom fields, checklists). */
325
356
  async metadata(datasetName, projectKey) {
@@ -1,20 +1,23 @@
1
1
  import type { SqlQueryResponse, SqlQueryResult } from "../schemas.js";
2
2
  import { BaseResource } from "./base.js";
3
+ type SqlQueryOptions = {
4
+ query: string;
5
+ connection?: string;
6
+ datasetFullName?: string;
7
+ database?: string;
8
+ preQueries?: string[];
9
+ postQueries?: string[];
10
+ type?: string;
11
+ projectKey?: string;
12
+ };
3
13
  export declare class SqlResource extends BaseResource {
14
+ private resolveOptionalProjectKey;
4
15
  /**
5
16
  * Start a SQL query and return the queryId + schema.
6
17
  * Specify either `connection` (run against a DB connection)
7
18
  * or `datasetFullName` (run against a dataset's connection).
8
19
  */
9
- startQuery(opts: {
10
- query: string;
11
- connection?: string;
12
- datasetFullName?: string;
13
- database?: string;
14
- preQueries?: string[];
15
- postQueries?: string[];
16
- type?: string;
17
- }): Promise<SqlQueryResult>;
20
+ startQuery(opts: SqlQueryOptions): Promise<SqlQueryResult>;
18
21
  /**
19
22
  * Stream results of a started query as parsed JSON (array of arrays).
20
23
  */
@@ -24,17 +27,12 @@ export declare class SqlResource extends BaseResource {
24
27
  * Throws on failure.
25
28
  */
26
29
  finishStreaming(queryId: string): Promise<void>;
30
+ private executeQuery;
31
+ private resolveDatasetQueryFallback;
27
32
  /**
28
33
  * Execute a SQL query end-to-end: start, stream all rows, verify, return combined result.
29
34
  * This is the primary method most callers want.
30
35
  */
31
- query(opts: {
32
- query: string;
33
- connection?: string;
34
- datasetFullName?: string;
35
- database?: string;
36
- preQueries?: string[];
37
- postQueries?: string[];
38
- type?: string;
39
- }): Promise<SqlQueryResponse>;
36
+ query(opts: SqlQueryOptions): Promise<SqlQueryResponse>;
40
37
  }
38
+ export {};
@@ -13,7 +13,37 @@ function buildUnsupportedSqlDatasetConnectionMessage(datasetFullName) {
13
13
  : "This query uses a connection that DSS does not support for direct SQL queries.";
14
14
  return `${subject} Use --connection with a SQL-compatible connection instead.`;
15
15
  }
16
+ function asRecord(value) {
17
+ if (!value || typeof value !== "object" || Array.isArray(value))
18
+ return undefined;
19
+ return value;
20
+ }
21
+ function asString(value) {
22
+ if (typeof value !== "string")
23
+ return undefined;
24
+ const trimmed = value.trim();
25
+ return trimmed.length > 0 ? trimmed : undefined;
26
+ }
27
+ function splitDatasetIdentifier(datasetFullName, fallbackProjectKey) {
28
+ const trimmed = datasetFullName.trim();
29
+ const dotIndex = trimmed.indexOf(".");
30
+ if (dotIndex <= 0) {
31
+ return { datasetName: trimmed, projectKey: fallbackProjectKey, };
32
+ }
33
+ return {
34
+ projectKey: trimmed.slice(0, dotIndex),
35
+ datasetName: trimmed.slice(dotIndex + 1),
36
+ };
37
+ }
16
38
  export class SqlResource extends BaseResource {
39
+ resolveOptionalProjectKey(projectKey) {
40
+ try {
41
+ return this.resolveProjectKey(projectKey);
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
17
47
  /**
18
48
  * Start a SQL query and return the queryId + schema.
19
49
  * Specify either `connection` (run against a DB connection)
@@ -22,6 +52,7 @@ export class SqlResource extends BaseResource {
22
52
  async startQuery(opts) {
23
53
  return this.client.post("/public/api/sql/queries/", {
24
54
  ...opts,
55
+ projectKey: opts.projectKey ?? this.resolveOptionalProjectKey(opts.projectKey),
25
56
  type: opts.type ?? "sql",
26
57
  });
27
58
  }
@@ -44,6 +75,39 @@ export class SqlResource extends BaseResource {
44
75
  throw new Error(`SQL query ${queryId} failed: ${text}`);
45
76
  }
46
77
  }
78
+ async executeQuery(opts) {
79
+ const { queryId, schema, } = await this.startQuery(opts);
80
+ const rows = await this.streamResults(queryId);
81
+ await this.finishStreaming(queryId);
82
+ return { queryId, schema, rows, };
83
+ }
84
+ async resolveDatasetQueryFallback(opts) {
85
+ const datasetFullName = opts.datasetFullName;
86
+ if (!datasetFullName)
87
+ return null;
88
+ try {
89
+ const identifier = splitDatasetIdentifier(datasetFullName, opts.projectKey);
90
+ const projectKey = identifier.projectKey
91
+ ? identifier.projectKey
92
+ : this.resolveProjectKey(opts.projectKey);
93
+ const dsEnc = encodeURIComponent(identifier.datasetName);
94
+ const raw = await this.client.get(`/public/api/projects/${encodeURIComponent(projectKey)}/datasets/${dsEnc}`);
95
+ const params = asRecord(raw.params);
96
+ const connection = asString(params?.connection);
97
+ if (!connection)
98
+ return null;
99
+ return {
100
+ ...opts,
101
+ connection,
102
+ datasetFullName: undefined,
103
+ database: opts.database ?? asString(params?.schema) ?? asString(params?.catalog),
104
+ projectKey,
105
+ };
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
47
111
  /**
48
112
  * Execute a SQL query end-to-end: start, stream all rows, verify, return combined result.
49
113
  * This is the primary method most callers want.
@@ -51,17 +115,18 @@ export class SqlResource extends BaseResource {
51
115
  async query(opts) {
52
116
  const queryOpts = { ...opts, type: opts.type ?? "sql", };
53
117
  try {
54
- const { queryId, schema, } = await this.startQuery(queryOpts);
55
- const rows = await this.streamResults(queryId);
56
- await this.finishStreaming(queryId);
57
- return { queryId, schema, rows, };
118
+ return await this.executeQuery(queryOpts);
58
119
  }
59
120
  catch (error) {
60
121
  if (!isUnsupportedSqlDatasetConnectionError(error))
61
122
  throw error;
62
- throw new Error(buildUnsupportedSqlDatasetConnectionMessage(queryOpts.datasetFullName), {
63
- cause: error,
64
- });
123
+ const retryOpts = await this.resolveDatasetQueryFallback(queryOpts);
124
+ if (!retryOpts) {
125
+ throw new Error(buildUnsupportedSqlDatasetConnectionMessage(queryOpts.datasetFullName), {
126
+ cause: error,
127
+ });
128
+ }
129
+ return this.executeQuery(retryOpts);
65
130
  }
66
131
  }
67
132
  }
package/dist/src/skill.js CHANGED
@@ -36,7 +36,7 @@ dss auth login --url https://dss.example.com --api-key YOUR_KEY
36
36
  dss auth status # verify connection
37
37
  \`\`\`
38
38
 
39
- Credentials are saved to \`~/.dss/credentials.json\`. Alternatively set environment variables:
39
+ Credentials are saved to \`~/.config/dataiku/credentials.json\`. Alternatively set environment variables:
40
40
 
41
41
  \`\`\`bash
42
42
  export DATAIKU_URL=https://dss.example.com
@@ -98,7 +98,9 @@ Use \`dss <resource> --help\` to see all actions and flags for any resource.
98
98
  -v, --verbose log HTTP requests to stderr
99
99
  --project-key KEY override default project for any command
100
100
  --timeout MS request timeout (default: 30000)
101
- --stdin read JSON input from stdin
101
+ --insecure disable TLS certificate verification
102
+ --ca-cert PATH trust an extra PEM CA bundle
103
+ --stdin read command input from stdin (JSON or SQL, depending on command)
102
104
  \`\`\`
103
105
 
104
106
  ## Gotchas
@@ -206,7 +208,7 @@ export function detectAgents() {
206
208
  // ---------------------------------------------------------------------------
207
209
  // Workspace root detection
208
210
  // ---------------------------------------------------------------------------
209
- const WORKSPACE_MARKERS = [".git", ".cursor", ".claude", ".codex", ".vscode",];
211
+ const WORKSPACE_MARKERS = [".git", ".cursor", ".claude", ".codex", ".pi", ".omp", ".vscode",];
210
212
  /**
211
213
  * Walk upward from startDir looking for common workspace markers.
212
214
  * Returns the first directory containing a marker, or startDir if none found.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataiku-sdk",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Dataiku DSS SDK and CLI for programmatic access to DSS REST APIs",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -14,9 +14,9 @@ export type SafeParseResult<T> = {
14
14
  errors: string[];
15
15
  };
16
16
  /**
17
- * Validate `data` against a TypeBox schema without throwing.
18
- * Always returns the data (cast as T) — on mismatch, includes human-readable
19
- * error strings so callers can warn instead of crash.
17
+ * Validate `data` against a TypeBox schema without throwing, even when invalid
18
+ * values are not JSON-serializable. Always returns the original data (cast as T)
19
+ * and includes human-readable error strings so callers can warn instead of crash.
20
20
  */
21
21
  export declare function safeParseSchema<S extends TSchema>(schema: S, data: unknown): SafeParseResult<Static<S>>;
22
22
  export declare const ProjectSummarySchema: import("@sinclair/typebox").TObject<{
@@ -12,15 +12,65 @@ export function parseSchema(schema, data) {
12
12
  return data;
13
13
  }
14
14
  /**
15
- * Validate `data` against a TypeBox schema without throwing.
16
- * Always returns the data (cast as T) — on mismatch, includes human-readable
17
- * error strings so callers can warn instead of crash.
15
+ * Format invalid values for validation errors without throwing on BigInt, circular,
16
+ * function, symbol, or other non-JSON-serializable input.
17
+ */
18
+ function formatInvalidValue(value) {
19
+ if (value === undefined)
20
+ return "undefined";
21
+ if (typeof value === "bigint")
22
+ return `${value.toString()}n`;
23
+ if (typeof value === "symbol") {
24
+ return value.description === undefined ? "Symbol()" : `Symbol(${value.description})`;
25
+ }
26
+ if (typeof value === "function") {
27
+ return value.name ? `[Function ${value.name}]` : "[Function anonymous]";
28
+ }
29
+ const seen = new WeakSet();
30
+ try {
31
+ const json = JSON.stringify(value, (_key, nestedValue) => {
32
+ if (typeof nestedValue === "bigint")
33
+ return `${nestedValue.toString()}n`;
34
+ if (typeof nestedValue === "symbol") {
35
+ return nestedValue.description === undefined
36
+ ? "Symbol()"
37
+ : `Symbol(${nestedValue.description})`;
38
+ }
39
+ if (typeof nestedValue === "function") {
40
+ return nestedValue.name
41
+ ? `[Function ${nestedValue.name}]`
42
+ : "[Function anonymous]";
43
+ }
44
+ if (nestedValue !== null && typeof nestedValue === "object") {
45
+ if (seen.has(nestedValue))
46
+ return "[Circular]";
47
+ seen.add(nestedValue);
48
+ }
49
+ return nestedValue;
50
+ });
51
+ if (json !== undefined)
52
+ return json;
53
+ }
54
+ catch {
55
+ // Ignore serialization failures and fall back to a safer summary below.
56
+ }
57
+ try {
58
+ return Object.prototype.toString.call(value);
59
+ }
60
+ catch {
61
+ return "[Unformattable value]";
62
+ }
63
+ }
64
+ /**
65
+ * Validate `data` against a TypeBox schema without throwing, even when invalid
66
+ * values are not JSON-serializable. Always returns the original data (cast as T)
67
+ * and includes human-readable error strings so callers can warn instead of crash.
18
68
  */
19
69
  export function safeParseSchema(schema, data) {
20
70
  if (Value.Check(schema, data)) {
21
71
  return { success: true, data: data, };
22
72
  }
23
- const errors = [...Value.Errors(schema, data),].map((e) => `${e.path}: ${e.message} (got ${JSON.stringify(e.value)})`);
73
+ const errors = [...Value.Errors(schema, data),].map((e) => `${e.path}: ${e.message} (got ${formatInvalidValue(e.value)})`);
24
74
  return { success: false, data: data, errors, };
25
75
  }
26
76
  // ---------------------------------------------------------------------------