dataiku-sdk 3.7.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-metadata.json +1 -1
- package/dist/packages/types/src/index.d.ts +40 -4
- package/dist/packages/types/src/index.js +22 -1
- package/dist/src/cli/coerce.d.ts +1 -0
- package/dist/src/cli/coerce.js +1 -1
- package/dist/src/cli/command-syntax.json +120 -0
- package/dist/src/cli/commands/dataset.js +42 -6
- package/dist/src/cli/commands/flow-zone.js +241 -46
- package/dist/src/cli/commands/job.js +18 -3
- package/dist/src/cli/commands/recipe.d.ts +4 -0
- package/dist/src/cli/commands/recipe.js +38 -1
- package/dist/src/cli/commands/wiki.js +56 -0
- package/dist/src/cli/contract.js +11 -3
- package/dist/src/cli/doctor.js +2 -0
- package/dist/src/cli/flags.d.ts +7 -0
- package/dist/src/cli/flags.js +28 -2
- package/dist/src/cli/helpers/dataset.d.ts +12 -0
- package/dist/src/cli/helpers/dataset.js +36 -0
- package/dist/src/cli/helpers/flow-zone.d.ts +21 -0
- package/dist/src/cli/helpers/flow-zone.js +144 -4
- package/dist/src/cli/output.js +6 -0
- package/dist/src/cli/plans.js +58 -2
- package/dist/src/cli/types.d.ts +2 -0
- package/dist/src/cli.js +12 -4
- package/dist/src/errors.d.ts +11 -1
- package/dist/src/errors.js +18 -3
- package/dist/src/resources/dataset-create.d.ts +19 -0
- package/dist/src/resources/dataset-create.js +69 -0
- package/dist/src/resources/datasets.d.ts +21 -0
- package/dist/src/resources/datasets.js +61 -1
- package/dist/src/resources/flow-zones.d.ts +29 -3
- package/dist/src/resources/flow-zones.js +53 -3
- package/dist/src/resources/jobs.d.ts +34 -0
- package/dist/src/resources/jobs.js +68 -1
- package/dist/src/resources/projects.d.ts +9 -0
- package/dist/src/resources/projects.js +10 -3
- package/dist/src/resources/recipe-create.d.ts +17 -3
- package/dist/src/resources/recipe-create.js +149 -33
- package/dist/src/resources/recipes.js +76 -23
- package/dist/src/resources/sql.js +52 -13
- package/dist/src/resources/wiki.d.ts +47 -1
- package/dist/src/resources/wiki.js +89 -0
- package/dist/src/utils/flow-analysis.js +4 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/dataiku-dss/references/flow-maps.md +2 -2
- package/skills/dataiku-dss/references/troubleshooting.md +1 -1
package/dist/src/cli/flags.d.ts
CHANGED
|
@@ -18,6 +18,13 @@ export declare const REPEATABLE_VALUE_FLAGS: Set<string>;
|
|
|
18
18
|
export declare const KNOWN_LONG_FLAGS: Set<string>;
|
|
19
19
|
export declare function normalizeLongFlag(rawFlagName: string, argv: string[]): string;
|
|
20
20
|
export declare function isNegativeNumberToken(value: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the token after a value flag is that flag's value rather than the
|
|
23
|
+
* next flag. `-` (stdin), negative numbers, and anything not flag-shaped
|
|
24
|
+
* qualify, so a SQL value beginning with a `-- comment` is a value; `--` alone
|
|
25
|
+
* and `--flag`/`--flag=x` are not.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isFlagValueToken(token: string): boolean;
|
|
21
28
|
export declare function requireFlagValue(flagLabel: string, next: string | undefined): string;
|
|
22
29
|
export declare function setParsedFlagValue(flags: Record<string, string | boolean>, flagName: string, value: string): void;
|
|
23
30
|
export interface ParsedArgs {
|
package/dist/src/cli/flags.js
CHANGED
|
@@ -81,6 +81,7 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
81
81
|
"allow-same-path",
|
|
82
82
|
"sync",
|
|
83
83
|
"validate-objects",
|
|
84
|
+
"recreate-on-move",
|
|
84
85
|
"errors-only",
|
|
85
86
|
"keep",
|
|
86
87
|
"full-log",
|
|
@@ -223,6 +224,8 @@ export const VALUE_FLAGS = new Set([
|
|
|
223
224
|
"render",
|
|
224
225
|
"prediction-type",
|
|
225
226
|
"project-key",
|
|
227
|
+
"query",
|
|
228
|
+
"query-file",
|
|
226
229
|
"recipe",
|
|
227
230
|
"reference",
|
|
228
231
|
"repository",
|
|
@@ -353,9 +356,32 @@ export function normalizeLongFlag(rawFlagName, argv) {
|
|
|
353
356
|
export function isNegativeNumberToken(value) {
|
|
354
357
|
return value.startsWith("-") && Number.isFinite(Number(value));
|
|
355
358
|
}
|
|
359
|
+
/** `--name` or `--name=value`: a token that parses as a long flag. */
|
|
360
|
+
const LONG_FLAG_SHAPE = /^--[A-Za-z][A-Za-z0-9-]*(?:=|$)/;
|
|
361
|
+
/**
|
|
362
|
+
* Whether the token after a value flag is that flag's value rather than the
|
|
363
|
+
* next flag. `-` (stdin), negative numbers, and anything not flag-shaped
|
|
364
|
+
* qualify, so a SQL value beginning with a `-- comment` is a value; `--` alone
|
|
365
|
+
* and `--flag`/`--flag=x` are not.
|
|
366
|
+
*/
|
|
367
|
+
export function isFlagValueToken(token) {
|
|
368
|
+
if (token === "-" || isNegativeNumberToken(token))
|
|
369
|
+
return true;
|
|
370
|
+
if (!token.startsWith("-"))
|
|
371
|
+
return true;
|
|
372
|
+
return token.startsWith("--") && token !== "--" && !LONG_FLAG_SHAPE.test(token);
|
|
373
|
+
}
|
|
356
374
|
export function requireFlagValue(flagLabel, next) {
|
|
357
|
-
if (next === undefined ||
|
|
358
|
-
|
|
375
|
+
if (next === undefined || !isFlagValueToken(next)) {
|
|
376
|
+
const inlineForm = flagLabel.startsWith("--") ? `${flagLabel}=VALUE` : undefined;
|
|
377
|
+
const hint = next === undefined || !next.startsWith("-")
|
|
378
|
+
? undefined
|
|
379
|
+
: flagLabel === "--sql"
|
|
380
|
+
? 'A SQL value that starts with `--` (a comment) can be passed as --sql="-- ...", from a file with --sql-file PATH, or on stdin with --stdin.'
|
|
381
|
+
: inlineForm
|
|
382
|
+
? `If the value itself starts with \`-\`, pass it as ${inlineForm}.`
|
|
383
|
+
: undefined;
|
|
384
|
+
throw new UsageError(`Flag ${flagLabel} requires a value.`, "missing_required_flag", hint);
|
|
359
385
|
}
|
|
360
386
|
return next;
|
|
361
387
|
}
|
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
import type { DatasetDetails } from "../../schemas.js";
|
|
2
|
+
export interface DatasetSqlSourceFlags {
|
|
3
|
+
table?: string;
|
|
4
|
+
dbSchema?: string;
|
|
5
|
+
catalog?: string;
|
|
6
|
+
query?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The SQL source of `dataset create` (`--table [--schema S] [--catalog C]` or
|
|
10
|
+
* `--query SQL`/`--query-file PATH`), validated against the dataset type.
|
|
11
|
+
* Empty when no SQL flag is given.
|
|
12
|
+
*/
|
|
13
|
+
export declare function datasetSqlSourceFromFlags(flags: Record<string, string | boolean>, dsType: string): DatasetSqlSourceFlags;
|
|
2
14
|
export declare function datasetSourceSummary(details: DatasetDetails): Record<string, unknown>;
|
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
import { assertSqlDatasetSource, } from "../../resources/dataset-create.js";
|
|
2
|
+
import { readInputText, stripUtf8Bom, } from "../coerce.js";
|
|
3
|
+
import { UsageError, } from "../usage.js";
|
|
4
|
+
/**
|
|
5
|
+
* The SQL source of `dataset create` (`--table [--schema S] [--catalog C]` or
|
|
6
|
+
* `--query SQL`/`--query-file PATH`), validated against the dataset type.
|
|
7
|
+
* Empty when no SQL flag is given.
|
|
8
|
+
*/
|
|
9
|
+
export function datasetSqlSourceFromFlags(flags, dsType) {
|
|
10
|
+
const text = (flag) => {
|
|
11
|
+
const value = flags[flag];
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (typeof value !== "string") {
|
|
15
|
+
throw new UsageError(`--${flag} requires a value.`, "missing_required_flag");
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
if (flags["query"] !== undefined && flags["query-file"] !== undefined) {
|
|
20
|
+
throw new UsageError("Conflicting input sources: --query, --query-file. Provide exactly one.", "conflicting_input_sources", undefined, { sources: ["query", "query-file",], });
|
|
21
|
+
}
|
|
22
|
+
const queryFile = text("query-file");
|
|
23
|
+
const inline = text("query");
|
|
24
|
+
const source = {
|
|
25
|
+
...(text("table") !== undefined ? { table: text("table"), } : {}),
|
|
26
|
+
...(text("schema") !== undefined ? { dbSchema: text("schema"), } : {}),
|
|
27
|
+
...(text("catalog") !== undefined ? { catalog: text("catalog"), } : {}),
|
|
28
|
+
...(queryFile !== undefined
|
|
29
|
+
? { query: stripUtf8Bom(readInputText(queryFile, "--query-file")), }
|
|
30
|
+
: inline !== undefined
|
|
31
|
+
? { query: inline, }
|
|
32
|
+
: {}),
|
|
33
|
+
};
|
|
34
|
+
assertSqlDatasetSource(dsType, source);
|
|
35
|
+
return source;
|
|
36
|
+
}
|
|
1
37
|
export function datasetSourceSummary(details) {
|
|
2
38
|
const params = details.params ?? {};
|
|
3
39
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DataikuClient } from "../../client.js";
|
|
2
2
|
import type { FlowZoneItemInput } from "../../resources/flow-zones.js";
|
|
3
3
|
import type { FlowZone, FlowZoneObjectType, FlowZonePosition } from "../../schemas.js";
|
|
4
|
+
import type { NormalizedFlowMap } from "../../utils/flow-map.js";
|
|
4
5
|
export declare function flowZoneId(value: string): string;
|
|
5
6
|
export declare function flowZoneName(value: string | boolean | undefined): string;
|
|
6
7
|
export declare function flowZoneColor(value: string | boolean | undefined): string | undefined;
|
|
@@ -18,6 +19,8 @@ export interface FlowZoneOrganizeZonePlan {
|
|
|
18
19
|
color?: string;
|
|
19
20
|
position?: FlowZonePosition;
|
|
20
21
|
items: FlowZoneItemInput[];
|
|
22
|
+
/** Recipe/output unit members added by {@link applyFlowZoneUnits}; also present in `items`. */
|
|
23
|
+
impliedItems?: FlowZoneItemInput[];
|
|
21
24
|
}
|
|
22
25
|
export interface FlowZoneOrganizePlan {
|
|
23
26
|
topologyFingerprint?: string;
|
|
@@ -60,7 +63,25 @@ export declare function flowZoneExplicitItems(zone: FlowZone): FlowZoneItemInput
|
|
|
60
63
|
export declare function flowZonePlanFromZones(zones: FlowZone[], topologyFingerprint: string): FlowZoneOrganizePlan;
|
|
61
64
|
export declare function flowZoneMoveDelta(existing: FlowZone | undefined, plannedItems: FlowZoneItemInput[]): FlowZoneItemInput[];
|
|
62
65
|
export declare function flowZonePruneItems(existing: FlowZone | undefined, plannedItemKeys: Set<string>): FlowZoneItemInput[];
|
|
66
|
+
/**
|
|
67
|
+
* The position an existing zone must move to, if the plan changes it. DSS
|
|
68
|
+
* honors a zone position only on creation, so a move means recreating the zone.
|
|
69
|
+
*/
|
|
70
|
+
export declare function flowZoneRepositionTarget(plan: FlowZoneOrganizeZonePlan, existing: FlowZone): FlowZonePosition | undefined;
|
|
63
71
|
export declare function flowZoneOrganizeStep(plan: FlowZoneOrganizeZonePlan, existing: FlowZone | undefined, sync: boolean, plannedItemKeys: Set<string>): Record<string, unknown>;
|
|
72
|
+
/**
|
|
73
|
+
* Enforce the DSS recipe/output zone invariant on a plan. Rejects plans that
|
|
74
|
+
* place members of one unit in different zones (they would never converge),
|
|
75
|
+
* and adds the unlisted members of each planned unit to that zone as
|
|
76
|
+
* `impliedItems`, so moves, `--sync` pruning, and reruns all agree with what
|
|
77
|
+
* DSS actually does.
|
|
78
|
+
*/
|
|
79
|
+
export declare function applyFlowZoneUnits(plan: FlowZoneOrganizePlan, graph: NormalizedFlowMap, zones: FlowZone[], projectKey: string | undefined): FlowZoneOrganizePlan;
|
|
80
|
+
/**
|
|
81
|
+
* Default-zone objects an organization plan has to place: recipes (their
|
|
82
|
+
* outputs follow) and objects no recipe produces, such as uploaded sources.
|
|
83
|
+
*/
|
|
84
|
+
export declare function flowZoneUnassignedItems(graph: NormalizedFlowMap, zones: FlowZone[]): FlowZoneItemInput[];
|
|
64
85
|
export declare function flowZoneValidationBucket(index: FlowZoneOrganizeValidationIndex, objectType: FlowZoneObjectType): Set<string>;
|
|
65
86
|
export declare function flowZoneValidationIndex(client: DataikuClient, projectKey: string | undefined): Promise<FlowZoneOrganizeValidationIndex>;
|
|
66
87
|
export declare function validateFlowZoneOrganizeObjects(client: DataikuClient, plan: FlowZoneOrganizePlan, projectKey: string | undefined): Promise<FlowZoneOrganizeValidationResult>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, } from "node:fs";
|
|
2
|
+
import { ClientValidationError, } from "../../errors.js";
|
|
2
3
|
import { asRecord, } from "../../utils/records.js";
|
|
3
4
|
import { compareStrings, } from "../../utils/stable-hash.js";
|
|
4
5
|
import { finiteNumberField, jsonInput, optionalStringField, parseJsonObject, requiredStringArray, splitCsvFlag, } from "../coerce.js";
|
|
@@ -319,6 +320,16 @@ export function flowZonePruneItems(existing, plannedItemKeys) {
|
|
|
319
320
|
return [];
|
|
320
321
|
return flowZoneExplicitItems(existing).filter((item) => !plannedItemKeys.has(flowZoneItemKey(item)));
|
|
321
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* The position an existing zone must move to, if the plan changes it. DSS
|
|
325
|
+
* honors a zone position only on creation, so a move means recreating the zone.
|
|
326
|
+
*/
|
|
327
|
+
export function flowZoneRepositionTarget(plan, existing) {
|
|
328
|
+
return plan.position !== undefined
|
|
329
|
+
&& !flowZoneSamePosition(flowZoneCurrentPosition(existing), plan.position)
|
|
330
|
+
? plan.position
|
|
331
|
+
: undefined;
|
|
332
|
+
}
|
|
322
333
|
export function flowZoneOrganizeStep(plan, existing, sync, plannedItemKeys) {
|
|
323
334
|
ensureFlowZonePlanTarget(plan, existing);
|
|
324
335
|
const update = {};
|
|
@@ -326,10 +337,8 @@ export function flowZoneOrganizeStep(plan, existing, sync, plannedItemKeys) {
|
|
|
326
337
|
update.name = plan.name;
|
|
327
338
|
if (existing && plan.color && plan.color !== existing.color)
|
|
328
339
|
update.color = plan.color;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
update.position = plan.position;
|
|
332
|
-
}
|
|
340
|
+
const recreatePosition = existing ? flowZoneRepositionTarget(plan, existing) : undefined;
|
|
341
|
+
const recreate = recreatePosition ? { position: recreatePosition, } : undefined;
|
|
333
342
|
const pruneItems = sync ? flowZonePruneItems(existing, plannedItemKeys) : [];
|
|
334
343
|
const moveItems = flowZoneMoveDelta(existing, plan.items);
|
|
335
344
|
return {
|
|
@@ -341,10 +350,141 @@ export function flowZoneOrganizeStep(plan, existing, sync, plannedItemKeys) {
|
|
|
341
350
|
},
|
|
342
351
|
...(existing ? { existing: flowZoneSummary(existing), } : { create: true, }),
|
|
343
352
|
...(Object.keys(update).length > 0 ? { update, } : {}),
|
|
353
|
+
...(recreate ? { recreate, } : {}),
|
|
344
354
|
moveItems,
|
|
355
|
+
...(plan.impliedItems && plan.impliedItems.length > 0
|
|
356
|
+
? { impliedItems: plan.impliedItems, }
|
|
357
|
+
: {}),
|
|
345
358
|
...(pruneItems.length > 0 ? { pruneItems, } : {}),
|
|
346
359
|
};
|
|
347
360
|
}
|
|
361
|
+
function flowNodeObjectType(node, knownTypes) {
|
|
362
|
+
switch (node.kind) {
|
|
363
|
+
case "recipe":
|
|
364
|
+
return "RECIPE";
|
|
365
|
+
case "dataset":
|
|
366
|
+
return "DATASET";
|
|
367
|
+
case "folder":
|
|
368
|
+
return "MANAGED_FOLDER";
|
|
369
|
+
default:
|
|
370
|
+
return knownTypes.get(node.id);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* DSS keeps a recipe and its outputs in one zone: moving either drags the
|
|
375
|
+
* others. Each unit is keyed by its recipe id and lists the recipe plus every
|
|
376
|
+
* object the recipe writes.
|
|
377
|
+
*/
|
|
378
|
+
function flowZoneUnits(graph) {
|
|
379
|
+
const kindById = new Map(graph.nodes.map((node) => [node.id, node.kind,]));
|
|
380
|
+
const units = new Map();
|
|
381
|
+
for (const node of graph.nodes) {
|
|
382
|
+
if (node.kind === "recipe")
|
|
383
|
+
units.set(node.id, [node.id,]);
|
|
384
|
+
}
|
|
385
|
+
for (const edge of graph.edges) {
|
|
386
|
+
if (kindById.get(edge.from) !== "recipe" || kindById.get(edge.to) === "recipe")
|
|
387
|
+
continue;
|
|
388
|
+
units.get(edge.from)?.push(edge.to);
|
|
389
|
+
}
|
|
390
|
+
return units;
|
|
391
|
+
}
|
|
392
|
+
function explicitTypesById(zones) {
|
|
393
|
+
const types = new Map();
|
|
394
|
+
for (const zone of zones) {
|
|
395
|
+
for (const item of flowZoneItems(zone)) {
|
|
396
|
+
if (!item.projectKey)
|
|
397
|
+
types.set(item.objectId, item.objectType);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return types;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Enforce the DSS recipe/output zone invariant on a plan. Rejects plans that
|
|
404
|
+
* place members of one unit in different zones (they would never converge),
|
|
405
|
+
* and adds the unlisted members of each planned unit to that zone as
|
|
406
|
+
* `impliedItems`, so moves, `--sync` pruning, and reruns all agree with what
|
|
407
|
+
* DSS actually does.
|
|
408
|
+
*/
|
|
409
|
+
export function applyFlowZoneUnits(plan, graph, zones, projectKey) {
|
|
410
|
+
const zoneIndexById = new Map();
|
|
411
|
+
plan.zones.forEach((zonePlan, index) => {
|
|
412
|
+
for (const item of zonePlan.items) {
|
|
413
|
+
// Shared objects from other projects never move with a local recipe.
|
|
414
|
+
if (!item.projectKey || item.projectKey === projectKey)
|
|
415
|
+
zoneIndexById.set(item.objectId, index);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node,]));
|
|
419
|
+
const knownTypes = explicitTypesById(zones);
|
|
420
|
+
const conflicts = [];
|
|
421
|
+
const implied = plan.zones.map(() => []);
|
|
422
|
+
for (const [recipe, members,] of flowZoneUnits(graph)) {
|
|
423
|
+
const planned = members.filter((member) => zoneIndexById.has(member));
|
|
424
|
+
if (planned.length === 0)
|
|
425
|
+
continue;
|
|
426
|
+
const indexes = new Set(planned.map((member) => zoneIndexById.get(member)));
|
|
427
|
+
if (indexes.size > 1) {
|
|
428
|
+
const byZone = {};
|
|
429
|
+
for (const member of planned) {
|
|
430
|
+
const label = flowZonePlanLabel(plan.zones[zoneIndexById.get(member)]);
|
|
431
|
+
(byZone[label] ??= []).push(member);
|
|
432
|
+
}
|
|
433
|
+
conflicts.push({ recipe, zones: byZone, });
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const [index,] = indexes;
|
|
437
|
+
for (const member of members) {
|
|
438
|
+
if (zoneIndexById.has(member))
|
|
439
|
+
continue;
|
|
440
|
+
const node = nodeById.get(member);
|
|
441
|
+
const objectType = node ? flowNodeObjectType(node, knownTypes) : undefined;
|
|
442
|
+
if (objectType)
|
|
443
|
+
implied[index].push({ objectType, objectId: member, });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (conflicts.length > 0) {
|
|
447
|
+
throw new ClientValidationError(`Flow zone plan splits ${conflicts.length} recipe(s) from their outputs across zones; DSS keeps a recipe and its outputs in one zone.`, "validation_failed", "Assign each recipe to one zone (its outputs follow automatically), or list the recipe and all its outputs in the same zone.", { conflicts, });
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
...plan,
|
|
451
|
+
zones: plan.zones.map((zonePlan, index) => implied[index].length === 0
|
|
452
|
+
? zonePlan
|
|
453
|
+
: {
|
|
454
|
+
...zonePlan,
|
|
455
|
+
items: [...zonePlan.items, ...implied[index],],
|
|
456
|
+
impliedItems: implied[index],
|
|
457
|
+
}),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Default-zone objects an organization plan has to place: recipes (their
|
|
462
|
+
* outputs follow) and objects no recipe produces, such as uploaded sources.
|
|
463
|
+
*/
|
|
464
|
+
export function flowZoneUnassignedItems(graph, zones) {
|
|
465
|
+
const assigned = new Set();
|
|
466
|
+
for (const zone of zones) {
|
|
467
|
+
if (zone.id === "default")
|
|
468
|
+
continue;
|
|
469
|
+
for (const item of flowZoneExplicitItems(zone))
|
|
470
|
+
assigned.add(item.objectId);
|
|
471
|
+
}
|
|
472
|
+
const produced = new Set();
|
|
473
|
+
for (const members of flowZoneUnits(graph).values()) {
|
|
474
|
+
for (const member of members.slice(1))
|
|
475
|
+
produced.add(member);
|
|
476
|
+
}
|
|
477
|
+
const knownTypes = explicitTypesById(zones);
|
|
478
|
+
const items = [];
|
|
479
|
+
for (const node of graph.nodes) {
|
|
480
|
+
if (assigned.has(node.id) || produced.has(node.id))
|
|
481
|
+
continue;
|
|
482
|
+
const objectType = flowNodeObjectType(node, knownTypes);
|
|
483
|
+
if (objectType)
|
|
484
|
+
items.push({ objectType, objectId: node.id, });
|
|
485
|
+
}
|
|
486
|
+
return items.sort((a, b) => compareStrings(flowZoneItemKey(a), flowZoneItemKey(b)));
|
|
487
|
+
}
|
|
348
488
|
export function flowZoneValidationBucket(index, objectType) {
|
|
349
489
|
switch (objectType) {
|
|
350
490
|
case "DATASET":
|
package/dist/src/cli/output.js
CHANGED
|
@@ -187,6 +187,12 @@ export class CommandResultFailure extends Error {
|
|
|
187
187
|
export function commandFailureMessage(result) {
|
|
188
188
|
if (isFailedWaitResult(result)) {
|
|
189
189
|
const record = result;
|
|
190
|
+
const failure = record.failure;
|
|
191
|
+
if (failure !== null && typeof failure === "object" && !Array.isArray(failure)) {
|
|
192
|
+
const message = failure.message;
|
|
193
|
+
if (typeof message === "string" && message.length > 0)
|
|
194
|
+
return message;
|
|
195
|
+
}
|
|
190
196
|
const state = typeof record.state === "string"
|
|
191
197
|
? record.state
|
|
192
198
|
: typeof record.outcome === "string"
|
package/dist/src/cli/plans.js
CHANGED
|
@@ -16,11 +16,12 @@ import { schemaPropagationOptionsFromFlags, } from "./commands/flow.js";
|
|
|
16
16
|
import { mlTaskFieldsFromFlags, mlTaskReguessOptionsFromFlags, } from "./commands/ml-task.js";
|
|
17
17
|
import { deployRunOptionsFromFlags, experimentsDatasetOptionsFromFlags, inferenceInfoFromFlags, } from "./commands/mlflow.js";
|
|
18
18
|
import { projectLibraryPutPayload, } from "./commands/project-library.js";
|
|
19
|
-
import { recipeCreateOptionsFromFlags, } from "./commands/recipe.js";
|
|
19
|
+
import { enqueueRecipeCreateWarnings, recipeCreateOptionsFromFlags, } from "./commands/recipe.js";
|
|
20
20
|
import { resolveSqlQueryInvocation, } from "./commands/sql.js";
|
|
21
21
|
import { buildRegistryEntry, splitPackageSpec, } from "./contract.js";
|
|
22
22
|
import { ambientProjectKey, } from "./env.js";
|
|
23
23
|
import { executionMode, } from "./flags.js";
|
|
24
|
+
import { datasetSqlSourceFromFlags, } from "./helpers/dataset.js";
|
|
24
25
|
import { flowZoneColor, flowZoneMoveItems, flowZoneName, } from "./helpers/flow-zone.js";
|
|
25
26
|
import { recipeBackupPath, recipeRunShouldWait, } from "./helpers/recipe.js";
|
|
26
27
|
import { diagnosticsComputationFromFlags, } from "./helpers/trained-model.js";
|
|
@@ -419,6 +420,50 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
|
|
|
419
420
|
endpoint: projectEndpoint(`/wiki/${encodeURIComponent(id)}`),
|
|
420
421
|
identifiers: { article: id, },
|
|
421
422
|
};
|
|
423
|
+
case "wiki.attach": {
|
|
424
|
+
const articleEndpoint = projectEndpoint(`/wiki/${encodeURIComponent(id)}`);
|
|
425
|
+
const upload = {
|
|
426
|
+
method: "POST",
|
|
427
|
+
endpoint: `${articleEndpoint}/upload`,
|
|
428
|
+
payload: uploadPayload(args[1]),
|
|
429
|
+
};
|
|
430
|
+
return {
|
|
431
|
+
...upload,
|
|
432
|
+
identifiers: { article: id, localPath: args[1], },
|
|
433
|
+
requests: [
|
|
434
|
+
{
|
|
435
|
+
sequence: 1,
|
|
436
|
+
method: "GET",
|
|
437
|
+
endpoint: articleEndpoint,
|
|
438
|
+
purpose: "read current attachments",
|
|
439
|
+
},
|
|
440
|
+
{ sequence: 2, ...upload, },
|
|
441
|
+
{
|
|
442
|
+
sequence: 3,
|
|
443
|
+
method: "GET",
|
|
444
|
+
endpoint: articleEndpoint,
|
|
445
|
+
assert: { newAttachments: 1, },
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
case "wiki.detach": {
|
|
451
|
+
const articleEndpoint = projectEndpoint(`/wiki/${encodeURIComponent(id)}`);
|
|
452
|
+
return {
|
|
453
|
+
method: "PUT",
|
|
454
|
+
endpoint: articleEndpoint,
|
|
455
|
+
identifiers: { article: id, smartId: args[1], },
|
|
456
|
+
requests: [
|
|
457
|
+
{ sequence: 1, method: "GET", endpoint: articleEndpoint, },
|
|
458
|
+
{
|
|
459
|
+
sequence: 2,
|
|
460
|
+
method: "PUT",
|
|
461
|
+
endpoint: articleEndpoint,
|
|
462
|
+
payload: "<article data without the attachment with the given smartId>",
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
};
|
|
466
|
+
}
|
|
422
467
|
case "dashboard.create": {
|
|
423
468
|
const data = jsonInput(flags);
|
|
424
469
|
const flagName = flags["name"];
|
|
@@ -730,8 +775,16 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
|
|
|
730
775
|
if (!connection && dsType.toLowerCase() !== "uploadedfiles") {
|
|
731
776
|
throw new UsageError("--connection is required unless --type is UploadedFiles.");
|
|
732
777
|
}
|
|
778
|
+
const sqlSource = datasetSqlSourceFromFlags(flags, dsType);
|
|
779
|
+
const hasSqlSource = sqlSource.table !== undefined || sqlSource.query !== undefined;
|
|
733
780
|
const endpoint = projectEndpoint("/datasets/"); // throws without a project key
|
|
734
781
|
return {
|
|
782
|
+
...(hasSqlSource
|
|
783
|
+
? {
|
|
784
|
+
exact: false,
|
|
785
|
+
reason: "The POST is exact; apply then detects the schema (POST /actions/testAndDetectSettings/externalSQL) and stores it (PUT /schema).",
|
|
786
|
+
}
|
|
787
|
+
: {}),
|
|
735
788
|
method: "POST",
|
|
736
789
|
endpoint,
|
|
737
790
|
identifiers: { name, },
|
|
@@ -740,6 +793,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
|
|
|
740
793
|
datasetName: name,
|
|
741
794
|
connection,
|
|
742
795
|
dsType,
|
|
796
|
+
...sqlSource,
|
|
743
797
|
}),
|
|
744
798
|
};
|
|
745
799
|
}
|
|
@@ -924,7 +978,9 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
|
|
|
924
978
|
}
|
|
925
979
|
const endpoint = projectEndpoint("/recipes/"); // throws without a project key
|
|
926
980
|
// Same flag mapping and body construction as the handler and RecipesResource.create.
|
|
927
|
-
const
|
|
981
|
+
const createOptions = recipeCreateOptionsFromFlags(flags);
|
|
982
|
+
enqueueRecipeCreateWarnings(createOptions);
|
|
983
|
+
const { recipePrototype, creationSettings, grouping, prepareSteps, } = buildRecipeCreateRequest(createOptions, projectKey);
|
|
928
984
|
const edits = grouping
|
|
929
985
|
? { grouping, }
|
|
930
986
|
: prepareSteps.length > 0
|
package/dist/src/cli/types.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface CommandPayloadSchema {
|
|
|
7
7
|
dataFileFlag?: boolean;
|
|
8
8
|
contentType?: "application/json" | "text/plain";
|
|
9
9
|
jsonShape?: "object" | "array";
|
|
10
|
+
/** Precise JSON Schema of the payload, published as the contract's `schemas.input`. */
|
|
11
|
+
jsonSchema?: Record<string, unknown>;
|
|
10
12
|
}
|
|
11
13
|
export interface CommandFlagChoice {
|
|
12
14
|
oneOf: string[][];
|
package/dist/src/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolve, } from "node:path";
|
|
|
5
5
|
import { num, seenJsonInputCredentials, unknownJsonInput, } from "./cli/coerce.js";
|
|
6
6
|
import { commands, } from "./cli/commands/index.js";
|
|
7
7
|
import { ambientProjectKey, loadEnvFile, } from "./cli/env.js";
|
|
8
|
-
import { executionMode, FLAG_ALIASES,
|
|
8
|
+
import { executionMode, FLAG_ALIASES, isFlagValueToken, parseArgs, rawPositionals, } from "./cli/flags.js";
|
|
9
9
|
import { cleanupLedgerEntry, } from "./cli/helpers/cleanup.js";
|
|
10
10
|
import { shapeListResult, } from "./cli/list-output.js";
|
|
11
11
|
import { commandFailureExitCode, CommandResultFailure, enqueueCliWarning, flushCliWarnings, isFailedWaitResult, planResult, projectResultFields, setOutputFieldProjection, writeCommandResult, } from "./cli/output.js";
|
|
@@ -533,8 +533,7 @@ function redactArgv(argv) {
|
|
|
533
533
|
else {
|
|
534
534
|
redacted.push(arg);
|
|
535
535
|
const next = argv[index + 1];
|
|
536
|
-
if (next !== undefined
|
|
537
|
-
&& (next === "-" || !next.startsWith("-") || isNegativeNumberToken(next))) {
|
|
536
|
+
if (next !== undefined && isFlagValueToken(next)) {
|
|
538
537
|
redacted.push("***");
|
|
539
538
|
index++;
|
|
540
539
|
}
|
|
@@ -1247,6 +1246,10 @@ function dssDiagnosticDetails(err) {
|
|
|
1247
1246
|
function isAmbiguousMutationFailure(err, context) {
|
|
1248
1247
|
if (!(err instanceof DataikuError))
|
|
1249
1248
|
return false;
|
|
1249
|
+
// DSS definitively refused the operation (e.g. the database rejected the SQL):
|
|
1250
|
+
// nothing took effect, so there is no outcome to verify.
|
|
1251
|
+
if (err.definiteFailure)
|
|
1252
|
+
return false;
|
|
1250
1253
|
const method = err.retry?.method.toUpperCase();
|
|
1251
1254
|
if (!method || method === "GET" || method === "HEAD")
|
|
1252
1255
|
return false;
|
|
@@ -1363,6 +1366,7 @@ function buildErrorReport(err, contextOverride) {
|
|
|
1363
1366
|
idempotency: "none",
|
|
1364
1367
|
...dssErrorDetails(err),
|
|
1365
1368
|
...dssDiagnosticDetails(err),
|
|
1369
|
+
...err.details,
|
|
1366
1370
|
...(err.retry ? { retry: err.retry, } : {}),
|
|
1367
1371
|
},
|
|
1368
1372
|
...context,
|
|
@@ -1370,7 +1374,11 @@ function buildErrorReport(err, contextOverride) {
|
|
|
1370
1374
|
}
|
|
1371
1375
|
if (err instanceof DataikuError) {
|
|
1372
1376
|
const { dssMessage, dssErrorType, } = dssDiagnosticDetails(err);
|
|
1373
|
-
const details = {
|
|
1377
|
+
const details = {
|
|
1378
|
+
...dssErrorDetails(err),
|
|
1379
|
+
...(dssErrorType ? { dssErrorType, } : {}),
|
|
1380
|
+
...err.details,
|
|
1381
|
+
};
|
|
1374
1382
|
// Generic hints name what the command targeted. DSS answers 403 (not
|
|
1375
1383
|
// 404) for a project that does not exist: "Failed to read project
|
|
1376
1384
|
// permissions". A 403 about something else keeps the generic hint.
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -20,12 +20,18 @@ export interface DataikuErrorTrustedMetadata {
|
|
|
20
20
|
elapsedMs?: number;
|
|
21
21
|
/** Set by the transport when the retained response body is only a bounded prefix. */
|
|
22
22
|
bodyTruncated?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Set by a resource that recognized DSS's answer as a definite failure of the
|
|
25
|
+
* whole operation: nothing took effect, so a non-idempotent mutation is not
|
|
26
|
+
* ambiguous.
|
|
27
|
+
*/
|
|
28
|
+
definiteFailure?: boolean;
|
|
23
29
|
}
|
|
24
30
|
export declare function canonicalStatusText(status: number): string;
|
|
25
31
|
export declare class ClientValidationError extends Error {
|
|
26
32
|
readonly code: StableErrorCode;
|
|
27
33
|
readonly hint?: string;
|
|
28
|
-
|
|
34
|
+
details?: Record<string, unknown>;
|
|
29
35
|
constructor(message: string, code?: StableErrorCode, hint?: string, details?: Record<string, unknown>, options?: ErrorOptions);
|
|
30
36
|
}
|
|
31
37
|
/**
|
|
@@ -62,6 +68,10 @@ export declare class DataikuError extends Error {
|
|
|
62
68
|
readonly trustedTarget?: string;
|
|
63
69
|
readonly trustedElapsedMs?: number;
|
|
64
70
|
readonly bodyTruncated?: boolean;
|
|
71
|
+
/** DSS definitively failed the whole operation; see `DataikuErrorTrustedMetadata.definiteFailure`. */
|
|
72
|
+
readonly definiteFailure?: boolean;
|
|
73
|
+
/** Client-side outcome context (e.g. rollback of partial mutations), surfaced in CLI `details`. */
|
|
74
|
+
details?: Record<string, unknown>;
|
|
65
75
|
constructor(status: number, statusText: string, body: string, retry?: DataikuRetryMetadata, requestId?: string, trustedMetadata?: DataikuErrorTrustedMetadata);
|
|
66
76
|
/** Status line only: never echoes response body text. */
|
|
67
77
|
get safeMessage(): string;
|
package/dist/src/errors.js
CHANGED
|
@@ -74,6 +74,8 @@ export const UNEXPECTED_RESPONSE_MARKER = "Unexpected DSS response";
|
|
|
74
74
|
export function unexpectedResponseError(detail, status = 200) {
|
|
75
75
|
return new DataikuError(status, "Unexpected Response", `${UNEXPECTED_RESPONSE_MARKER}: ${detail}`);
|
|
76
76
|
}
|
|
77
|
+
/** Hint for a DSS answer that definitively failed the request (e.g. the database rejected the SQL). */
|
|
78
|
+
const DEFINITE_FAILURE_HINT = "DSS reported that the request failed (for SQL: syntax error, missing object, or missing privileges of the connection's role). Fix the request or grants; do not retry unchanged.";
|
|
77
79
|
/** Prefix for a SQL query failure reported by DSS on a 2xx finish-streaming response. */
|
|
78
80
|
export const SQL_QUERY_FAILED_MARKER = "SQL query failed in DSS";
|
|
79
81
|
const TLS_CERTIFICATE_HINT = "TLS certificate verification failed. Trust the DSS/corporate CA with --ca-cert PATH or NODE_EXTRA_CA_CERTS, or save it for every process and directory with `dss auth login --ca-cert PATH` (a project .env applies only when dss runs in that directory); use --insecure only for temporary troubleshooting.";
|
|
@@ -140,6 +142,8 @@ function isCertificateTrustFailure(lowerBody) {
|
|
|
140
142
|
|| lowerBody.includes("unable to get local issuer certificate")
|
|
141
143
|
|| lowerBody.includes("unable to verify leaf signature")
|
|
142
144
|
|| lowerBody.includes("self-signed certificate")
|
|
145
|
+
|| lowerBody.includes("self signed certificate")
|
|
146
|
+
|| lowerBody.includes("depth_zero_self_signed_cert")
|
|
143
147
|
|| lowerBody.includes("certificate has expired")
|
|
144
148
|
|| lowerBody.includes("cert_has_expired")
|
|
145
149
|
|| lowerBody.includes("self_signed_cert_in_chain")
|
|
@@ -372,6 +376,10 @@ export class DataikuError extends Error {
|
|
|
372
376
|
trustedTarget;
|
|
373
377
|
trustedElapsedMs;
|
|
374
378
|
bodyTruncated;
|
|
379
|
+
/** DSS definitively failed the whole operation; see `DataikuErrorTrustedMetadata.definiteFailure`. */
|
|
380
|
+
definiteFailure;
|
|
381
|
+
/** Client-side outcome context (e.g. rollback of partial mutations), surfaced in CLI `details`. */
|
|
382
|
+
details;
|
|
375
383
|
constructor(status, statusText, body, retry, requestId, trustedMetadata) {
|
|
376
384
|
const details = DataikuError.buildDetails(status, body);
|
|
377
385
|
super(details.message);
|
|
@@ -379,14 +387,21 @@ export class DataikuError extends Error {
|
|
|
379
387
|
this.statusText = statusText;
|
|
380
388
|
this.body = body;
|
|
381
389
|
this.name = "DataikuError";
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
390
|
+
const definite = trustedMetadata?.definiteFailure === true;
|
|
391
|
+
// A definite DSS refusal is not a transport/server fault: replaying it
|
|
392
|
+
// unchanged cannot succeed. Keep a more specific forbidden/not_found class.
|
|
393
|
+
const reclassify = definite
|
|
394
|
+
&& (details.category === "transient" || details.category === "unknown"
|
|
395
|
+
|| details.category === "unexpected_response");
|
|
396
|
+
this.category = reclassify ? "validation" : details.category;
|
|
397
|
+
this.retryable = reclassify ? false : details.retryable;
|
|
398
|
+
this.retryHint = reclassify ? DEFINITE_FAILURE_HINT : details.retryHint;
|
|
385
399
|
this.retry = retry;
|
|
386
400
|
this.requestId = requestId;
|
|
387
401
|
this.trustedTarget = trustedMetadata?.target;
|
|
388
402
|
this.trustedElapsedMs = trustedMetadata?.elapsedMs;
|
|
389
403
|
this.bodyTruncated = trustedMetadata?.bodyTruncated === true ? true : undefined;
|
|
404
|
+
this.definiteFailure = definite ? true : undefined;
|
|
390
405
|
}
|
|
391
406
|
/** Status line only: never echoes response body text. */
|
|
392
407
|
get safeMessage() {
|
|
@@ -4,6 +4,24 @@
|
|
|
4
4
|
* public endpoint writes; without it every downstream build fails.
|
|
5
5
|
*/
|
|
6
6
|
export declare function assertDatasetTypeCreatable(dsType: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* Dataset types that read from a SQL connection. These are the types DSS
|
|
9
|
+
* detects schemas for through `testAndDetectSettings/externalSQL` (the list
|
|
10
|
+
* dataikuapi's `DSSDataset.autodetect_settings` uses).
|
|
11
|
+
*/
|
|
12
|
+
export declare const SQL_DATASET_TYPES: readonly ["JDBC", "PostgreSQL", "MySQL", "Vertica", "Snowflake", "Redshift", "Greenplum", "Teradata", "Oracle", "SQLServer", "SAPHANA", "Netezza", "BigQuery", "Athena", "hiveserver2", "Synapse", "FabricWarehouse", "Databricks", "DatabricksLakebase"];
|
|
13
|
+
export declare function isSqlDatasetType(dsType: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Validate the SQL source options (`table`/`dbSchema`/`catalog`/`query`) of a
|
|
16
|
+
* dataset creation against its type. Shared by `dataset create`, its plan, and
|
|
17
|
+
* the SDK so every entry point rejects the same combinations.
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertSqlDatasetSource(dsType: string, source: {
|
|
20
|
+
table?: string;
|
|
21
|
+
dbSchema?: string;
|
|
22
|
+
catalog?: string;
|
|
23
|
+
query?: string;
|
|
24
|
+
}): void;
|
|
7
25
|
/** Wire body for POST /datasets/; shared with `dataset create --plan` so the plan matches the request. */
|
|
8
26
|
export declare function buildDatasetCreateBody(opts: {
|
|
9
27
|
projectKey: string;
|
|
@@ -13,6 +31,7 @@ export declare function buildDatasetCreateBody(opts: {
|
|
|
13
31
|
table?: string;
|
|
14
32
|
dbSchema?: string;
|
|
15
33
|
catalog?: string;
|
|
34
|
+
query?: string;
|
|
16
35
|
formatType?: string;
|
|
17
36
|
formatParams?: Record<string, unknown>;
|
|
18
37
|
managed?: boolean;
|