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
|
@@ -9,6 +9,60 @@ export function assertDatasetTypeCreatable(dsType) {
|
|
|
9
9
|
return;
|
|
10
10
|
throw new ClientValidationError("Inline (editable) datasets cannot be created here: the DSS public API cannot write their rows, and an Inline dataset without rows fails every downstream build (SourceDatasetNotReadyException). For a small static table, create an UploadedFiles dataset and upload a CSV with dss dataset upload-file, or write the rows from a python recipe into a managed dataset.", "validation_failed");
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Dataset types that read from a SQL connection. These are the types DSS
|
|
14
|
+
* detects schemas for through `testAndDetectSettings/externalSQL` (the list
|
|
15
|
+
* dataikuapi's `DSSDataset.autodetect_settings` uses).
|
|
16
|
+
*/
|
|
17
|
+
export const SQL_DATASET_TYPES = [
|
|
18
|
+
"JDBC",
|
|
19
|
+
"PostgreSQL",
|
|
20
|
+
"MySQL",
|
|
21
|
+
"Vertica",
|
|
22
|
+
"Snowflake",
|
|
23
|
+
"Redshift",
|
|
24
|
+
"Greenplum",
|
|
25
|
+
"Teradata",
|
|
26
|
+
"Oracle",
|
|
27
|
+
"SQLServer",
|
|
28
|
+
"SAPHANA",
|
|
29
|
+
"Netezza",
|
|
30
|
+
"BigQuery",
|
|
31
|
+
"Athena",
|
|
32
|
+
"hiveserver2",
|
|
33
|
+
"Synapse",
|
|
34
|
+
"FabricWarehouse",
|
|
35
|
+
"Databricks",
|
|
36
|
+
"DatabricksLakebase",
|
|
37
|
+
];
|
|
38
|
+
export function isSqlDatasetType(dsType) {
|
|
39
|
+
const lower = dsType.toLowerCase();
|
|
40
|
+
return SQL_DATASET_TYPES.some((type) => type.toLowerCase() === lower);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate the SQL source options (`table`/`dbSchema`/`catalog`/`query`) of a
|
|
44
|
+
* dataset creation against its type. Shared by `dataset create`, its plan, and
|
|
45
|
+
* the SDK so every entry point rejects the same combinations.
|
|
46
|
+
*/
|
|
47
|
+
export function assertSqlDatasetSource(dsType, source) {
|
|
48
|
+
const hasTable = source.table !== undefined;
|
|
49
|
+
const hasQuery = source.query !== undefined;
|
|
50
|
+
if (hasTable && hasQuery) {
|
|
51
|
+
throw new ClientValidationError("Pass either a table or a query for a SQL dataset, not both.", "conflicting_input_sources");
|
|
52
|
+
}
|
|
53
|
+
if (!hasTable && (source.dbSchema !== undefined || source.catalog !== undefined)) {
|
|
54
|
+
throw new ClientValidationError("A schema or catalog only applies together with a table.", "validation_failed", "Add --table, or drop --schema/--catalog (a query names its own tables).");
|
|
55
|
+
}
|
|
56
|
+
if (hasTable && source.table.trim() === "") {
|
|
57
|
+
throw new ClientValidationError("table must be a non-empty table name.", "invalid_flag_value");
|
|
58
|
+
}
|
|
59
|
+
if (hasQuery && source.query.trim() === "") {
|
|
60
|
+
throw new ClientValidationError("query must be a non-empty SQL query.", "invalid_flag_value");
|
|
61
|
+
}
|
|
62
|
+
if ((hasTable || hasQuery) && !isSqlDatasetType(dsType)) {
|
|
63
|
+
throw new ClientValidationError(`A table or query only applies to SQL dataset types, not "${dsType}".`, "validation_failed", `SQL types: ${SQL_DATASET_TYPES.join(", ")}.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
12
66
|
/** Wire body for POST /datasets/; shared with `dataset create --plan` so the plan matches the request. */
|
|
13
67
|
export function buildDatasetCreateBody(opts) {
|
|
14
68
|
assertDatasetTypeCreatable(opts.dsType);
|
|
@@ -23,6 +77,21 @@ export function buildDatasetCreateBody(opts) {
|
|
|
23
77
|
if (!opts.connection) {
|
|
24
78
|
throw new ClientValidationError("connection is required unless dsType is UploadedFiles.");
|
|
25
79
|
}
|
|
80
|
+
if (opts.query !== undefined) {
|
|
81
|
+
if (opts.table !== undefined) {
|
|
82
|
+
throw new ClientValidationError("Pass either a table or a query for a SQL dataset, not both.", "conflicting_input_sources");
|
|
83
|
+
}
|
|
84
|
+
if (opts.query.trim() === "") {
|
|
85
|
+
throw new ClientValidationError("query must be a non-empty SQL query.", "invalid_flag_value");
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
projectKey: opts.projectKey,
|
|
89
|
+
name: opts.datasetName,
|
|
90
|
+
type: opts.dsType,
|
|
91
|
+
params: { connection: opts.connection, mode: "query", query: opts.query, },
|
|
92
|
+
managed: opts.managed ?? false,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
26
95
|
if (opts.table) {
|
|
27
96
|
const params = {
|
|
28
97
|
connection: opts.connection,
|
|
@@ -55,6 +55,18 @@ export interface DatasetManagedCreateResult {
|
|
|
55
55
|
projectKey: string;
|
|
56
56
|
creationSettings: Record<string, unknown>;
|
|
57
57
|
}
|
|
58
|
+
/** Outcome of {@link DatasetsResource.detectSqlSchema}. */
|
|
59
|
+
export interface DatasetSqlSchemaDetection {
|
|
60
|
+
/** True when DSS suggested a schema and it was stored on the dataset. */
|
|
61
|
+
detected: boolean;
|
|
62
|
+
/** Stored columns (empty when detection failed). */
|
|
63
|
+
columns: Array<{
|
|
64
|
+
name: string;
|
|
65
|
+
type: string;
|
|
66
|
+
}>;
|
|
67
|
+
/** Why detection failed. */
|
|
68
|
+
error?: string;
|
|
69
|
+
}
|
|
58
70
|
export interface DatasetCloneOptions {
|
|
59
71
|
projectKey?: string;
|
|
60
72
|
path?: string;
|
|
@@ -106,6 +118,15 @@ export declare class DatasetsResource extends BaseResource {
|
|
|
106
118
|
schema(datasetName: string, projectKey?: string): Promise<DatasetSchema>;
|
|
107
119
|
/** Replace dataset schema columns directly through the schema endpoint. */
|
|
108
120
|
updateSchema(datasetName: string, columns: DatasetSchemaColumnInput[], projectKey?: string): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Detect the schema of a SQL dataset (table or query mode) with DSS and
|
|
123
|
+
* store it: `POST /actions/testAndDetectSettings/externalSQL` (what
|
|
124
|
+
* dataikuapi's `autodetect_settings` calls), then `PUT /schema` with the
|
|
125
|
+
* suggested schema. DSS reports connection, table, and query problems in the
|
|
126
|
+
* response body (HTTP 200), so a failed detection is returned with
|
|
127
|
+
* `detected: false` and the reason instead of thrown.
|
|
128
|
+
*/
|
|
129
|
+
detectSqlSchema(datasetName: string, projectKey?: string): Promise<DatasetSqlSchemaDetection>;
|
|
109
130
|
/**
|
|
110
131
|
* Get the raw dataset schema object exactly as the DSS schema endpoint
|
|
111
132
|
* returns it, without schema validation — used for deterministic comparison.
|
|
@@ -4,6 +4,7 @@ import { StringDecoder, } from "node:string_decoder";
|
|
|
4
4
|
import { ClientValidationError, DataikuError, } from "../errors.js";
|
|
5
5
|
import { DatasetDetailsSchema, DatasetSchemaSchema, DatasetSummaryArraySchema, } from "../schemas.js";
|
|
6
6
|
import { deepMerge, } from "../utils/deep-merge.js";
|
|
7
|
+
import { isRecord, } from "../utils/records.js";
|
|
7
8
|
import { sanitizeFileName, } from "../utils/sanitize.js";
|
|
8
9
|
import { BaseResource, } from "./base.js";
|
|
9
10
|
import { resolveAdminManagedStorageConnection, } from "./connections.js";
|
|
@@ -519,6 +520,28 @@ function cloneDatasetParams(params) {
|
|
|
519
520
|
}
|
|
520
521
|
return cloned;
|
|
521
522
|
}
|
|
523
|
+
/** Explain an empty detection result from the first error DSS put in the response body. */
|
|
524
|
+
function describeSqlDetectionFailure(raw) {
|
|
525
|
+
if (!isRecord(raw))
|
|
526
|
+
return "DSS returned no schema detection result.";
|
|
527
|
+
for (const [key, value,] of Object.entries(raw)) {
|
|
528
|
+
if (!key.endsWith("Error"))
|
|
529
|
+
continue;
|
|
530
|
+
if (typeof value === "string" && value !== "")
|
|
531
|
+
return value;
|
|
532
|
+
if (isRecord(value)) {
|
|
533
|
+
const message = value["detailedMessage"] ?? value["message"];
|
|
534
|
+
if (typeof message === "string" && message !== "")
|
|
535
|
+
return message;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const failedChecks = Object.entries(raw)
|
|
539
|
+
.filter(([key, value,]) => key.endsWith("OK") && value === false)
|
|
540
|
+
.map(([key,]) => key);
|
|
541
|
+
return failedChecks.length > 0
|
|
542
|
+
? `DSS detected no columns (failed checks: ${failedChecks.join(", ")}).`
|
|
543
|
+
: "DSS detected no columns.";
|
|
544
|
+
}
|
|
522
545
|
function parseUploadedFiles(raw, datasetName) {
|
|
523
546
|
if (!Array.isArray(raw)) {
|
|
524
547
|
throw new ClientValidationError(`DSS returned an invalid uploaded-files list for dataset ${datasetName}.`, "validation_failed", "Expected an array from the UploadedFiles dataset file endpoint.");
|
|
@@ -592,6 +615,40 @@ export class DatasetsResource extends BaseResource {
|
|
|
592
615
|
const dsEnc = encodeURIComponent(datasetName);
|
|
593
616
|
await this.client.put(`/public/api/projects/${this.enc(projectKey)}/datasets/${dsEnc}/schema`, { columns, });
|
|
594
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Detect the schema of a SQL dataset (table or query mode) with DSS and
|
|
620
|
+
* store it: `POST /actions/testAndDetectSettings/externalSQL` (what
|
|
621
|
+
* dataikuapi's `autodetect_settings` calls), then `PUT /schema` with the
|
|
622
|
+
* suggested schema. DSS reports connection, table, and query problems in the
|
|
623
|
+
* response body (HTTP 200), so a failed detection is returned with
|
|
624
|
+
* `detected: false` and the reason instead of thrown.
|
|
625
|
+
*/
|
|
626
|
+
async detectSqlSchema(datasetName, projectKey) {
|
|
627
|
+
const base = `/public/api/projects/${this.enc(projectKey)}/datasets/${encodeURIComponent(datasetName)}`;
|
|
628
|
+
try {
|
|
629
|
+
const raw = await this.client.post(`${base}/actions/testAndDetectSettings/externalSQL`);
|
|
630
|
+
const detection = isRecord(raw) ? raw["schemaDetection"] : undefined;
|
|
631
|
+
const newSchema = isRecord(detection) ? detection["newSchema"] : undefined;
|
|
632
|
+
const rawColumns = isRecord(newSchema) && Array.isArray(newSchema["columns"])
|
|
633
|
+
? newSchema["columns"]
|
|
634
|
+
: [];
|
|
635
|
+
const columns = rawColumns.flatMap((column) => isRecord(column) && typeof column["name"] === "string"
|
|
636
|
+
&& typeof column["type"] === "string"
|
|
637
|
+
? [{ name: column["name"], type: column["type"], },]
|
|
638
|
+
: []);
|
|
639
|
+
if (!isRecord(newSchema) || columns.length === 0) {
|
|
640
|
+
return { detected: false, columns: [], error: describeSqlDetectionFailure(raw), };
|
|
641
|
+
}
|
|
642
|
+
await this.client.put(`${base}/schema`, newSchema);
|
|
643
|
+
return { detected: true, columns, };
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
if (error instanceof DataikuError) {
|
|
647
|
+
return { detected: false, columns: [], error: error.message.split("\n")[0] ?? "", };
|
|
648
|
+
}
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
595
652
|
/**
|
|
596
653
|
* Get the raw dataset schema object exactly as the DSS schema endpoint
|
|
597
654
|
* returns it, without schema validation — used for deterministic comparison.
|
|
@@ -859,7 +916,7 @@ export class DatasetsResource extends BaseResource {
|
|
|
859
916
|
const enc = encodeURIComponent(pk);
|
|
860
917
|
const explicitType = opts.dsType;
|
|
861
918
|
let dsType = explicitType
|
|
862
|
-
?? (opts.table ? DEFAULT_DATABASE_DATASET_TYPE : DEFAULT_FILESYSTEM_DATASET_TYPE);
|
|
919
|
+
?? (opts.table || opts.query ? DEFAULT_DATABASE_DATASET_TYPE : DEFAULT_FILESYSTEM_DATASET_TYPE);
|
|
863
920
|
let body = buildDatasetCreateBody({
|
|
864
921
|
projectKey: pk,
|
|
865
922
|
datasetName: opts.datasetName,
|
|
@@ -868,6 +925,7 @@ export class DatasetsResource extends BaseResource {
|
|
|
868
925
|
table: opts.table,
|
|
869
926
|
dbSchema: opts.dbSchema,
|
|
870
927
|
catalog: opts.catalog,
|
|
928
|
+
query: opts.query,
|
|
871
929
|
formatType: opts.formatType,
|
|
872
930
|
formatParams: opts.formatParams,
|
|
873
931
|
managed: opts.managed,
|
|
@@ -899,6 +957,7 @@ export class DatasetsResource extends BaseResource {
|
|
|
899
957
|
table: opts.table,
|
|
900
958
|
dbSchema: opts.dbSchema,
|
|
901
959
|
catalog: opts.catalog,
|
|
960
|
+
query: opts.query,
|
|
902
961
|
formatType: opts.formatType,
|
|
903
962
|
formatParams: opts.formatParams,
|
|
904
963
|
managed: opts.managed,
|
|
@@ -923,6 +982,7 @@ export class DatasetsResource extends BaseResource {
|
|
|
923
982
|
table: opts.table,
|
|
924
983
|
dbSchema: opts.dbSchema,
|
|
925
984
|
catalog: opts.catalog,
|
|
985
|
+
query: opts.query,
|
|
926
986
|
formatType: opts.formatType,
|
|
927
987
|
formatParams: opts.formatParams,
|
|
928
988
|
managed: opts.managed,
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import type { DataikuGetOptions } from "../client.js";
|
|
2
|
-
import type { FlowZone, FlowZoneCreateOptions, FlowZoneObjectType, FlowZoneUpdateOptions } from "../schemas.js";
|
|
2
|
+
import type { FlowZone, FlowZoneCreateOptions, FlowZoneObjectType, FlowZonePosition, FlowZoneUpdateOptions } from "../schemas.js";
|
|
3
3
|
import { BaseResource } from "./base.js";
|
|
4
4
|
export type FlowZoneItemInput = {
|
|
5
5
|
objectId: string;
|
|
6
6
|
objectType: FlowZoneObjectType;
|
|
7
7
|
projectKey?: string;
|
|
8
8
|
};
|
|
9
|
+
export type FlowZoneRecreateOptions = {
|
|
10
|
+
position: FlowZonePosition;
|
|
11
|
+
name?: string;
|
|
12
|
+
color?: string;
|
|
13
|
+
projectKey?: string;
|
|
14
|
+
};
|
|
15
|
+
export type FlowZoneRecreateResult = {
|
|
16
|
+
zone: FlowZone;
|
|
17
|
+
replacedZoneId: string;
|
|
18
|
+
};
|
|
9
19
|
export declare class FlowZonesResource extends BaseResource {
|
|
10
20
|
/**
|
|
11
21
|
* List all flow zones in a project. Pass `timeoutMs` to bound the whole
|
|
@@ -14,10 +24,26 @@ export declare class FlowZonesResource extends BaseResource {
|
|
|
14
24
|
list(projectKey?: string, options?: DataikuGetOptions): Promise<FlowZone[]>;
|
|
15
25
|
/** Get one flow zone by id. */
|
|
16
26
|
get(zoneId: string, projectKey?: string): Promise<FlowZone>;
|
|
17
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Create a flow zone. DSS stores `position` only while the project's manual
|
|
29
|
+
* zone positioning is off; with it on, DSS auto-places the zone.
|
|
30
|
+
*/
|
|
18
31
|
create(opts: FlowZoneCreateOptions): Promise<FlowZone>;
|
|
19
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Update flow zone name and color. DSS's public API ignores `position` on
|
|
34
|
+
* this endpoint; use {@link recreate} to move an existing zone.
|
|
35
|
+
*/
|
|
20
36
|
update(zoneId: string, opts: FlowZoneUpdateOptions): Promise<FlowZone>;
|
|
37
|
+
/**
|
|
38
|
+
* Replace a zone with a new one at `position`: DSS honors a zone position
|
|
39
|
+
* only on creation, and only while the project's manual zone positioning is
|
|
40
|
+
* off (otherwise it auto-places the new zone). The replacement receives the
|
|
41
|
+
* old zone's name, color, metadata (tags, custom fields, checklists, ...),
|
|
42
|
+
* items, and shared items; the old zone is deleted once it is empty. The zone
|
|
43
|
+
* id changes, so anything referencing the old id (for example scenario zone
|
|
44
|
+
* builds) must be updated.
|
|
45
|
+
*/
|
|
46
|
+
recreate(zoneId: string, opts: FlowZoneRecreateOptions): Promise<FlowZoneRecreateResult>;
|
|
21
47
|
/** Delete a flow zone. DSS moves its items back to the default zone. */
|
|
22
48
|
delete(zoneId: string, projectKey?: string): Promise<void>;
|
|
23
49
|
/** Move items into a flow zone. */
|
|
@@ -23,7 +23,10 @@ export class FlowZonesResource extends BaseResource {
|
|
|
23
23
|
const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/flow/zones/${encodeURIComponent(zoneId)}`);
|
|
24
24
|
return this.client.safeParse(FlowZoneSchema, raw, "flowZones.get");
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Create a flow zone. DSS stores `position` only while the project's manual
|
|
28
|
+
* zone positioning is off; with it on, DSS auto-places the zone.
|
|
29
|
+
*/
|
|
27
30
|
async create(opts) {
|
|
28
31
|
const raw = await this.client.post(`/public/api/projects/${this.enc(opts.projectKey)}/flow/zones`, {
|
|
29
32
|
name: opts.name,
|
|
@@ -32,17 +35,64 @@ export class FlowZonesResource extends BaseResource {
|
|
|
32
35
|
});
|
|
33
36
|
return this.client.safeParse(FlowZoneSchema, raw, "flowZones.create");
|
|
34
37
|
}
|
|
35
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Update flow zone name and color. DSS's public API ignores `position` on
|
|
40
|
+
* this endpoint; use {@link recreate} to move an existing zone.
|
|
41
|
+
*/
|
|
36
42
|
async update(zoneId, opts) {
|
|
37
43
|
const current = await this.get(zoneId, opts.projectKey);
|
|
38
44
|
const merged = deepMerge(current, {
|
|
39
45
|
...(opts.name !== undefined ? { name: opts.name, } : {}),
|
|
40
46
|
...(opts.color !== undefined ? { color: opts.color, } : {}),
|
|
41
|
-
...(opts.position !== undefined ? { position: opts.position, } : {}),
|
|
42
47
|
});
|
|
43
48
|
await this.client.putVoid(`/public/api/projects/${this.enc(opts.projectKey)}/flow/zones/${encodeURIComponent(zoneId)}`, merged);
|
|
44
49
|
return this.get(zoneId, opts.projectKey);
|
|
45
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Replace a zone with a new one at `position`: DSS honors a zone position
|
|
53
|
+
* only on creation, and only while the project's manual zone positioning is
|
|
54
|
+
* off (otherwise it auto-places the new zone). The replacement receives the
|
|
55
|
+
* old zone's name, color, metadata (tags, custom fields, checklists, ...),
|
|
56
|
+
* items, and shared items; the old zone is deleted once it is empty. The zone
|
|
57
|
+
* id changes, so anything referencing the old id (for example scenario zone
|
|
58
|
+
* builds) must be updated.
|
|
59
|
+
*/
|
|
60
|
+
async recreate(zoneId, opts) {
|
|
61
|
+
const base = `/public/api/projects/${this.enc(opts.projectKey)}/flow/zones`;
|
|
62
|
+
const old = await this.get(zoneId, opts.projectKey);
|
|
63
|
+
const created = await this.create({
|
|
64
|
+
name: opts.name ?? old.name,
|
|
65
|
+
color: opts.color ?? old.color,
|
|
66
|
+
position: opts.position,
|
|
67
|
+
projectKey: opts.projectKey,
|
|
68
|
+
});
|
|
69
|
+
const completed = ["create",];
|
|
70
|
+
try {
|
|
71
|
+
const { id: _id, items, shared, position: _position, ...metadata } = old;
|
|
72
|
+
await this.client.putVoid(`${base}/${encodeURIComponent(created.id)}`, {
|
|
73
|
+
...created,
|
|
74
|
+
...metadata,
|
|
75
|
+
id: created.id,
|
|
76
|
+
name: created.name,
|
|
77
|
+
color: created.color,
|
|
78
|
+
position: created.position,
|
|
79
|
+
});
|
|
80
|
+
completed.push("metadata");
|
|
81
|
+
if (items && items.length > 0) {
|
|
82
|
+
await this.moveItems(created.id, items, opts.projectKey);
|
|
83
|
+
}
|
|
84
|
+
completed.push("items");
|
|
85
|
+
for (const item of shared ?? []) {
|
|
86
|
+
await this.client.post(`${base}/${encodeURIComponent(created.id)}/shared`, normalizeZoneItem(item));
|
|
87
|
+
}
|
|
88
|
+
completed.push("shared");
|
|
89
|
+
await this.delete(old.id, opts.projectKey);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new ClientValidationError(`Recreating flow zone ${old.id} at a new position stopped after ${completed.at(-1)}; both zones may now exist.`, "ambiguous_outcome", "Inspect both zones with dss flow-zone list, then move remaining items and delete the zone you no longer need.", { replacedZoneId: old.id, zoneId: created.id, completed, }, { cause: error, });
|
|
93
|
+
}
|
|
94
|
+
return { zone: await this.get(created.id, opts.projectKey), replacedZoneId: old.id, };
|
|
95
|
+
}
|
|
46
96
|
/** Delete a flow zone. DSS moves its items back to the default zone. */
|
|
47
97
|
async delete(zoneId, projectKey) {
|
|
48
98
|
await this.client.del(`/public/api/projects/${this.enc(projectKey)}/flow/zones/${encodeURIComponent(zoneId)}`);
|
|
@@ -23,12 +23,30 @@ export interface JobLogSummary {
|
|
|
23
23
|
}
|
|
24
24
|
/** Why a requested job log could not be retrieved. */
|
|
25
25
|
export type JobLogUnavailableReason = "not_found" | "error";
|
|
26
|
+
/**
|
|
27
|
+
* A requested build target that no flow object can ever produce: a managed
|
|
28
|
+
* dataset with no recipe writing it.
|
|
29
|
+
*/
|
|
30
|
+
export interface JobUnbuildableTarget {
|
|
31
|
+
type: "DATASET";
|
|
32
|
+
id: string;
|
|
33
|
+
projectKey: string;
|
|
34
|
+
reason: "no_producing_recipe";
|
|
35
|
+
}
|
|
36
|
+
/** Why a job that ended DONE is reported as failed because it built nothing. */
|
|
37
|
+
export interface JobNothingToBuildFailure {
|
|
38
|
+
code: "nothing_to_build";
|
|
39
|
+
message: string;
|
|
40
|
+
targets: JobUnbuildableTarget[];
|
|
41
|
+
}
|
|
26
42
|
/** Result of a completed job wait, including log-unavailable metadata. */
|
|
27
43
|
export type JobWaitOutcome = JobWaitResult & {
|
|
28
44
|
logSummary?: JobLogSummary;
|
|
29
45
|
logUnavailable?: JobLogUnavailableReason;
|
|
30
46
|
/** True when the job itself no longer exists on the server. */
|
|
31
47
|
removed?: boolean;
|
|
48
|
+
/** Set (with `success: false`) when a DONE job ran no activity and a target has no producer. */
|
|
49
|
+
failure?: JobNothingToBuildFailure;
|
|
32
50
|
};
|
|
33
51
|
export interface JobBuildTarget {
|
|
34
52
|
id: string;
|
|
@@ -120,6 +138,12 @@ export declare class JobsResource extends BaseResource {
|
|
|
120
138
|
* confirmed gone. Explicit `includeLogs`/`summary` requests stay honest:
|
|
121
139
|
* `log`/`logSummary` are only present when logs were actually retrieved.
|
|
122
140
|
*
|
|
141
|
+
* A DONE job that ran no activity (`progress.total === 0`) is also what DSS
|
|
142
|
+
* reports for an already-up-to-date build, so it stays successful unless an
|
|
143
|
+
* output is a managed dataset that no recipe writes: then nothing can ever
|
|
144
|
+
* build it, and the outcome is `success: false` with
|
|
145
|
+
* `failure.code === "nothing_to_build"`.
|
|
146
|
+
*
|
|
123
147
|
* On timeout, returns `{ success: false, ... }` rather than throwing.
|
|
124
148
|
*/
|
|
125
149
|
wait(jobId: string, opts?: {
|
|
@@ -133,6 +157,16 @@ export declare class JobsResource extends BaseResource {
|
|
|
133
157
|
timeoutMs?: number;
|
|
134
158
|
projectKey?: string;
|
|
135
159
|
}): Promise<JobWaitOutcome>;
|
|
160
|
+
/**
|
|
161
|
+
* Dataset outputs of a job that ran no activity which nothing can produce.
|
|
162
|
+
* DSS ends such a job DONE exactly like an already-up-to-date build, so the
|
|
163
|
+
* job status cannot tell them apart; the flow graph can: a target is
|
|
164
|
+
* unbuildable when it is a managed dataset (DSS owns its storage, so a
|
|
165
|
+
* recipe is the only way to fill it) that no recipe writes. Unmanaged
|
|
166
|
+
* datasets (uploads, external tables) are sources and legitimately build
|
|
167
|
+
* nothing. Lookups that fail leave the DONE outcome untouched.
|
|
168
|
+
*/
|
|
169
|
+
private unbuildableTargets;
|
|
136
170
|
/**
|
|
137
171
|
* Probe whether a job still exists. Returns `true` when the server reports
|
|
138
172
|
* it as not found, `false` when it is still visible, and `undefined` when
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ClientValidationError, DataikuError, } from "../errors.js";
|
|
2
2
|
import { JobSummaryArraySchema, } from "../schemas.js";
|
|
3
3
|
import { computeNextPollDelayMs, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, isRequestDeadlineError, } from "../utils/polling.js";
|
|
4
|
+
import { asRecord, } from "../utils/records.js";
|
|
4
5
|
import { BaseResource, } from "./base.js";
|
|
5
6
|
const DEFAULT_MAX_LOG_LINES = 500;
|
|
6
7
|
/**
|
|
@@ -263,6 +264,10 @@ function summarizeJobLog(state, log, maxLines, elapsedMs) {
|
|
|
263
264
|
...(progress ? { progress, } : {}),
|
|
264
265
|
};
|
|
265
266
|
}
|
|
267
|
+
function nothingToBuildMessage(targets) {
|
|
268
|
+
const names = targets.map((target) => `${target.projectKey}.${target.id}`).join(", ");
|
|
269
|
+
return `The job ran no activity: ${names} ${targets.length === 1 ? "is a managed dataset" : "are managed datasets"} that no recipe produces, so nothing was built. Create a recipe that writes ${targets.length === 1 ? "it" : "them"} or build the recipe's output instead.`;
|
|
270
|
+
}
|
|
266
271
|
export class JobsResource extends BaseResource {
|
|
267
272
|
/** List jobs in a project. */
|
|
268
273
|
async list(projectKey) {
|
|
@@ -375,6 +380,12 @@ export class JobsResource extends BaseResource {
|
|
|
375
380
|
* confirmed gone. Explicit `includeLogs`/`summary` requests stay honest:
|
|
376
381
|
* `log`/`logSummary` are only present when logs were actually retrieved.
|
|
377
382
|
*
|
|
383
|
+
* A DONE job that ran no activity (`progress.total === 0`) is also what DSS
|
|
384
|
+
* reports for an already-up-to-date build, so it stays successful unless an
|
|
385
|
+
* output is a managed dataset that no recipe writes: then nothing can ever
|
|
386
|
+
* build it, and the outcome is `success: false` with
|
|
387
|
+
* `failure.code === "nothing_to_build"`.
|
|
388
|
+
*
|
|
378
389
|
* On timeout, returns `{ success: false, ... }` rather than throwing.
|
|
379
390
|
*/
|
|
380
391
|
async wait(jobId, opts) {
|
|
@@ -492,8 +503,18 @@ export class JobsResource extends BaseResource {
|
|
|
492
503
|
}
|
|
493
504
|
}
|
|
494
505
|
}
|
|
506
|
+
const unbuildable = success && gs.total === 0
|
|
507
|
+
? await this.unbuildableTargets(def.outputs, def.projectKey ?? opts?.projectKey)
|
|
508
|
+
: [];
|
|
509
|
+
const failure = unbuildable.length > 0
|
|
510
|
+
? {
|
|
511
|
+
code: "nothing_to_build",
|
|
512
|
+
message: nothingToBuildMessage(unbuildable),
|
|
513
|
+
targets: unbuildable,
|
|
514
|
+
}
|
|
515
|
+
: undefined;
|
|
495
516
|
return {
|
|
496
|
-
success,
|
|
517
|
+
success: success && failure === undefined,
|
|
497
518
|
jobId: def.id ?? jobId,
|
|
498
519
|
state,
|
|
499
520
|
type: def.type ?? "unknown",
|
|
@@ -505,6 +526,7 @@ export class JobsResource extends BaseResource {
|
|
|
505
526
|
running: gs.running ?? 0,
|
|
506
527
|
total: gs.total ?? null,
|
|
507
528
|
},
|
|
529
|
+
...(failure !== undefined ? { failure, } : {}),
|
|
508
530
|
...(log !== undefined ? { log, } : {}),
|
|
509
531
|
...(logSummary !== undefined ? { logSummary, } : {}),
|
|
510
532
|
...(logUnavailable !== undefined ? { logUnavailable, } : {}),
|
|
@@ -523,6 +545,51 @@ export class JobsResource extends BaseResource {
|
|
|
523
545
|
await sleep(Math.min(nextDelayMs, timeout - elapsedMs));
|
|
524
546
|
}
|
|
525
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Dataset outputs of a job that ran no activity which nothing can produce.
|
|
550
|
+
* DSS ends such a job DONE exactly like an already-up-to-date build, so the
|
|
551
|
+
* job status cannot tell them apart; the flow graph can: a target is
|
|
552
|
+
* unbuildable when it is a managed dataset (DSS owns its storage, so a
|
|
553
|
+
* recipe is the only way to fill it) that no recipe writes. Unmanaged
|
|
554
|
+
* datasets (uploads, external tables) are sources and legitimately build
|
|
555
|
+
* nothing. Lookups that fail leave the DONE outcome untouched.
|
|
556
|
+
*/
|
|
557
|
+
async unbuildableTargets(outputs, fallbackProjectKey) {
|
|
558
|
+
if (!Array.isArray(outputs))
|
|
559
|
+
return [];
|
|
560
|
+
const writtenByProject = new Map();
|
|
561
|
+
const unbuildable = [];
|
|
562
|
+
for (const output of outputs) {
|
|
563
|
+
const record = asRecord(output);
|
|
564
|
+
if (record?.type !== "DATASET" || typeof record.targetDataset !== "string")
|
|
565
|
+
continue;
|
|
566
|
+
const id = record.targetDataset;
|
|
567
|
+
const projectKey = typeof record.targetDatasetProjectKey === "string"
|
|
568
|
+
? record.targetDatasetProjectKey
|
|
569
|
+
: fallbackProjectKey;
|
|
570
|
+
if (projectKey === undefined)
|
|
571
|
+
continue;
|
|
572
|
+
try {
|
|
573
|
+
const details = await this.client.datasets.get(id, projectKey);
|
|
574
|
+
if (details.managed !== true)
|
|
575
|
+
continue;
|
|
576
|
+
let written = writtenByProject.get(projectKey);
|
|
577
|
+
if (written === undefined) {
|
|
578
|
+
const { graph, } = await this.client.projects.flowTopology(projectKey);
|
|
579
|
+
written = new Set(graph.edges.filter((edge) => edge.relation === "writes").map((edge) => edge.to));
|
|
580
|
+
writtenByProject.set(projectKey, written);
|
|
581
|
+
}
|
|
582
|
+
if (!written.has(id)) {
|
|
583
|
+
unbuildable.push({ type: "DATASET", id, projectKey, reason: "no_producing_recipe", });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
if (!(error instanceof DataikuError || error instanceof ClientValidationError))
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return unbuildable;
|
|
592
|
+
}
|
|
526
593
|
/**
|
|
527
594
|
* Probe whether a job still exists. Returns `true` when the server reports
|
|
528
595
|
* it as not found, `false` when it is still visible, and `undefined` when
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FlowMapOptions, ProjectDetails, ProjectMetadata, ProjectSummary, ProjectTags } from "../schemas.js";
|
|
2
2
|
import type { AnalyzedFlowMap, FlowMapRendering } from "../utils/flow-analysis.js";
|
|
3
|
+
import type { NormalizedFlowMap } from "../utils/flow-map.js";
|
|
3
4
|
import { BaseResource } from "./base.js";
|
|
4
5
|
interface FlowMapTruncationSummary {
|
|
5
6
|
truncated: boolean;
|
|
@@ -150,6 +151,14 @@ export declare class ProjectsResource extends BaseResource {
|
|
|
150
151
|
setTags(projectKey: string | undefined, tags: ProjectTags): Promise<void>;
|
|
151
152
|
/** Get the raw flow graph for a project. */
|
|
152
153
|
flow(projectKey?: string): Promise<unknown>;
|
|
154
|
+
/**
|
|
155
|
+
* Normalized flow topology (nodes and edges, no metadata lookups) plus its
|
|
156
|
+
* stable fingerprint, excluding visual metadata.
|
|
157
|
+
*/
|
|
158
|
+
flowTopology(projectKey?: string): Promise<{
|
|
159
|
+
graph: NormalizedFlowMap;
|
|
160
|
+
topologyFingerprint: string;
|
|
161
|
+
}>;
|
|
153
162
|
/** Compute a stable fingerprint of flow nodes and edges, excluding visual metadata. */
|
|
154
163
|
topologyFingerprint(projectKey?: string): Promise<string>;
|
|
155
164
|
/**
|
|
@@ -340,11 +340,18 @@ export class ProjectsResource extends BaseResource {
|
|
|
340
340
|
const enc = this.enc(projectKey);
|
|
341
341
|
return this.client.get(`/public/api/projects/${enc}/flow/graph/`);
|
|
342
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Normalized flow topology (nodes and edges, no metadata lookups) plus its
|
|
345
|
+
* stable fingerprint, excluding visual metadata.
|
|
346
|
+
*/
|
|
347
|
+
async flowTopology(projectKey) {
|
|
348
|
+
const pk = this.resolveProjectKey(projectKey);
|
|
349
|
+
const graph = normalizeFlowGraph(await this.flow(pk), pk);
|
|
350
|
+
return { graph, topologyFingerprint: flowTopologyFingerprint(graph), };
|
|
351
|
+
}
|
|
343
352
|
/** Compute a stable fingerprint of flow nodes and edges, excluding visual metadata. */
|
|
344
353
|
async topologyFingerprint(projectKey) {
|
|
345
|
-
|
|
346
|
-
const rawGraph = await this.flow(pk);
|
|
347
|
-
return flowTopologyFingerprint(normalizeFlowGraph(rawGraph, pk));
|
|
354
|
+
return (await this.flowTopology(projectKey)).topologyFingerprint;
|
|
348
355
|
}
|
|
349
356
|
/**
|
|
350
357
|
* Build a normalized, optionally truncated flow map for a project.
|
|
@@ -33,12 +33,26 @@ export declare function groupingPayloadConfig(opts: RecipeCreateOptions): Groupi
|
|
|
33
33
|
* aggregate flags (and `orderColumn` for first/last).
|
|
34
34
|
*/
|
|
35
35
|
export declare function applyGroupingPayload(payload: Record<string, unknown>, config: GroupingPayloadConfig): Record<string, unknown>;
|
|
36
|
+
/** Prepare option groups, in the default step order. */
|
|
37
|
+
export declare const PREPARE_STEP_KINDS: readonly ["rename", "fillEmpty", "formula", "filter", "dropColumns", "keepColumns"];
|
|
38
|
+
export type PrepareStepKind = (typeof PREPARE_STEP_KINDS)[number];
|
|
36
39
|
/**
|
|
37
|
-
* Prepare (shaker) steps from create options,
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
+
* Prepare (shaker) steps from create options: rename, fill empty, formula
|
|
41
|
+
* column, row filter, drop columns, keep columns. They run in `stepOrder`
|
|
42
|
+
* (flag order on the CLI), defaulting to that sequence, so a step sees the
|
|
43
|
+
* columns the steps before it produced. Empty when no prepare option was given.
|
|
40
44
|
*/
|
|
41
45
|
export declare function prepareSteps(opts: RecipeCreateOptions): Record<string, unknown>[];
|
|
46
|
+
export interface RecipeCreateWarning {
|
|
47
|
+
code: string;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Warnings about the prepare options that DSS cannot report itself: a
|
|
52
|
+
* `--formula`/`--filter` that runs after a `--rename` but still names a
|
|
53
|
+
* column that rename removed (it would fail or read an empty column).
|
|
54
|
+
*/
|
|
55
|
+
export declare function recipeCreateWarnings(opts: RecipeCreateOptions): RecipeCreateWarning[];
|
|
42
56
|
/**
|
|
43
57
|
* Pure request construction for POST /recipes/ (no DSS calls). Shared with
|
|
44
58
|
* `recipe create --plan` so the plan equals the request the command sends.
|