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
|
@@ -1,10 +1,104 @@
|
|
|
1
1
|
import { ClientValidationError, } from "../../errors.js";
|
|
2
2
|
import { deepMerge, } from "../../utils/deep-merge.js";
|
|
3
|
+
import { asRecord, } from "../../utils/records.js";
|
|
3
4
|
import { executionMode, } from "../flags.js";
|
|
4
|
-
import { ensureFlowZonePlanTarget, findFlowZoneForPlan, flowZoneColor, flowZoneContains, flowZoneCurrentPosition, flowZoneDetailSummary, flowZoneId, flowZoneMoveDelta, flowZoneMoveItems, flowZoneName, flowZoneOrganizeStep, flowZonePlanFromZones, flowZonePlanItemKeys, flowZonePruneItems, flowZoneSamePosition, flowZoneSummary, readFlowZoneOrganizePlan, resolveFlowZoneIdFromFlags, throwFlowZoneValidationError, validateFlowZoneOrganizeObjects, } from "../helpers/flow-zone.js";
|
|
5
|
-
import { readIfExists, skipResult, } from "../output.js";
|
|
5
|
+
import { applyFlowZoneUnits, ensureFlowZonePlanTarget, findFlowZoneForPlan, flowZoneColor, flowZoneContains, flowZoneCurrentPosition, flowZoneDetailSummary, flowZoneId, flowZoneMoveDelta, flowZoneMoveItems, flowZoneName, flowZoneOrganizeStep, flowZonePlanFromZones, flowZonePlanItemKeys, flowZonePruneItems, flowZoneRepositionTarget, flowZoneSamePosition, flowZoneSummary, flowZoneUnassignedItems, readFlowZoneOrganizePlan, resolveFlowZoneIdFromFlags, throwFlowZoneValidationError, validateFlowZoneOrganizeObjects, } from "../helpers/flow-zone.js";
|
|
6
|
+
import { enqueueCliWarning, readIfExists, skipResult, } from "../output.js";
|
|
6
7
|
import { commandUsage, withUsage, } from "../syntax.js";
|
|
7
8
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
9
|
+
const ZONE_OBJECT_IDS = { type: "array", items: { type: "string", minLength: 1, }, };
|
|
10
|
+
/** JSON Schema of the `flow-zone organize` plan, as parsed by `parseFlowZoneOrganizePlan`. */
|
|
11
|
+
const FLOW_ZONE_ORGANIZE_PLAN_SCHEMA = {
|
|
12
|
+
type: "object",
|
|
13
|
+
required: ["zones",],
|
|
14
|
+
properties: {
|
|
15
|
+
topologyFingerprint: {
|
|
16
|
+
type: "string",
|
|
17
|
+
pattern: "^[0-9a-f]{64}$",
|
|
18
|
+
description: "From dss flow-zone plan; organize refuses to apply if the flow topology changed since.",
|
|
19
|
+
},
|
|
20
|
+
zones: {
|
|
21
|
+
type: "array",
|
|
22
|
+
items: {
|
|
23
|
+
type: "object",
|
|
24
|
+
anyOf: [{ required: ["id",], }, { required: ["name",], },],
|
|
25
|
+
properties: {
|
|
26
|
+
id: { type: "string", description: "Existing zone id (alias zoneId); matched before name.", },
|
|
27
|
+
name: { type: "string", description: "Zone name; required to create a zone.", },
|
|
28
|
+
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", },
|
|
29
|
+
position: {
|
|
30
|
+
type: "object",
|
|
31
|
+
required: ["x", "y",],
|
|
32
|
+
properties: { x: { type: "number", }, y: { type: "number", }, },
|
|
33
|
+
description: "Applied when the zone is created; moving an existing zone requires --recreate-on-move.",
|
|
34
|
+
},
|
|
35
|
+
items: {
|
|
36
|
+
type: "array",
|
|
37
|
+
description: "Alias objects. Each entry is TYPE:ID or {objectType|type, objectId|id|name, projectKey?}.",
|
|
38
|
+
items: {
|
|
39
|
+
anyOf: [
|
|
40
|
+
{ type: "string", pattern: "^[A-Z_]+:.+$", },
|
|
41
|
+
{
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
object: { type: "string", },
|
|
45
|
+
objectType: { type: "string", },
|
|
46
|
+
type: { type: "string", },
|
|
47
|
+
objectId: { type: "string", },
|
|
48
|
+
id: { type: "string", },
|
|
49
|
+
name: { type: "string", },
|
|
50
|
+
projectKey: { type: "string", },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
datasets: ZONE_OBJECT_IDS,
|
|
57
|
+
recipes: ZONE_OBJECT_IDS,
|
|
58
|
+
folders: ZONE_OBJECT_IDS,
|
|
59
|
+
savedModels: ZONE_OBJECT_IDS,
|
|
60
|
+
modelEvaluationStores: ZONE_OBJECT_IDS,
|
|
61
|
+
streamingEndpoints: ZONE_OBJECT_IDS,
|
|
62
|
+
labelingTasks: ZONE_OBJECT_IDS,
|
|
63
|
+
knowledgeBanks: ZONE_OBJECT_IDS,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* DSS stores a zone `position` sent on creation only while the project's
|
|
71
|
+
* manual zone positioning is off (with it on, DSS auto-places the new zone),
|
|
72
|
+
* and draws stored positions only while it is on.
|
|
73
|
+
*/
|
|
74
|
+
async function readManualPositioning(c, projectKey) {
|
|
75
|
+
try {
|
|
76
|
+
const settings = asRecord(await c.projects.getSettings(projectKey));
|
|
77
|
+
const display = asRecord(asRecord(settings?.settings)?.flowDisplaySettings);
|
|
78
|
+
return { manual: display?.zonesManualPositioning === true, };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return { error: error instanceof Error ? error.message.split("\n")[0] : String(error), };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Read-modify-write of `flowDisplaySettings.zonesManualPositioning`, keeping every other setting. */
|
|
85
|
+
async function setManualPositioning(c, projectKey, manual) {
|
|
86
|
+
const current = await c.projects.getSettings(projectKey);
|
|
87
|
+
const settings = asRecord(current.settings) ?? {};
|
|
88
|
+
await c.projects.setSettings(projectKey, {
|
|
89
|
+
...current,
|
|
90
|
+
settings: {
|
|
91
|
+
...settings,
|
|
92
|
+
flowDisplaySettings: {
|
|
93
|
+
...asRecord(settings.flowDisplaySettings),
|
|
94
|
+
zonesManualPositioning: manual,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function enableManualPositioningCommand(projectKey) {
|
|
100
|
+
return `dss project settings-set --data '{"settings":{"flowDisplaySettings":{"zonesManualPositioning":true}}}'${projectKey ? ` --project-key ${projectKey}` : ""}`;
|
|
101
|
+
}
|
|
8
102
|
export const flowZoneCommands = withUsage("flow-zone", {
|
|
9
103
|
list: {
|
|
10
104
|
handler: async (c, _a, f) => {
|
|
@@ -165,36 +259,77 @@ export const flowZoneCommands = withUsage("flow-zone", {
|
|
|
165
259
|
plan: {
|
|
166
260
|
handler: async (c, _a, f) => {
|
|
167
261
|
const pk = f["project-key"];
|
|
168
|
-
const [zones, topologyFingerprint,] = await Promise.all([
|
|
262
|
+
const [zones, { graph, topologyFingerprint, },] = await Promise.all([
|
|
169
263
|
c.flowZones.list(pk),
|
|
170
|
-
c.projects.
|
|
264
|
+
c.projects.flowTopology(pk),
|
|
171
265
|
]);
|
|
172
|
-
return
|
|
266
|
+
return {
|
|
267
|
+
...flowZonePlanFromZones(zones, topologyFingerprint),
|
|
268
|
+
unassigned: flowZoneUnassignedItems(graph, zones),
|
|
269
|
+
};
|
|
173
270
|
},
|
|
174
|
-
description: "Export current visual flow-zone organization as an organize-compatible plan.",
|
|
271
|
+
description: "Export current visual flow-zone organization as an organize-compatible plan. `unassigned` lists default-zone recipes and source objects still to place (recipe outputs follow their recipe); organize ignores it.",
|
|
175
272
|
examples: ["dss flow-zone plan --project-key MYPROJ",],
|
|
176
273
|
},
|
|
177
274
|
organize: {
|
|
178
275
|
handler: async (c, _a, f) => {
|
|
179
276
|
const usage = commandUsage("flow-zone", "organize");
|
|
180
277
|
const pk = f["project-key"];
|
|
181
|
-
const
|
|
278
|
+
const parsed = readFlowZoneOrganizePlan(f, usage);
|
|
182
279
|
const sync = f["sync"] === true;
|
|
183
280
|
const validateObjects = f["validate-objects"] === true;
|
|
184
|
-
const [
|
|
281
|
+
const recreateOnMove = f["recreate-on-move"] === true;
|
|
282
|
+
const [zones, { graph, topologyFingerprint, },] = await Promise.all([
|
|
185
283
|
c.flowZones.list(pk),
|
|
186
|
-
c.projects.
|
|
284
|
+
c.projects.flowTopology(pk),
|
|
187
285
|
]);
|
|
188
|
-
if (
|
|
189
|
-
throw new ClientValidationError("Flow topology changed after this visual organization plan was generated.", "assertion_failed", "Regenerate the plan with dss flow-zone plan, then review it before applying.", { expected:
|
|
286
|
+
if (parsed.topologyFingerprint && parsed.topologyFingerprint !== topologyFingerprint) {
|
|
287
|
+
throw new ClientValidationError("Flow topology changed after this visual organization plan was generated.", "assertion_failed", "Regenerate the plan with dss flow-zone plan, then review it before applying.", { expected: parsed.topologyFingerprint, actual: topologyFingerprint, });
|
|
190
288
|
}
|
|
289
|
+
const plan = applyFlowZoneUnits(parsed, graph, zones, pk);
|
|
191
290
|
const plannedItemKeys = flowZonePlanItemKeys(plan);
|
|
192
|
-
const
|
|
291
|
+
const targets = plan.zones.map((zonePlan) => ({
|
|
292
|
+
zonePlan,
|
|
293
|
+
existing: findFlowZoneForPlan(zones, zonePlan),
|
|
294
|
+
}));
|
|
295
|
+
const planned = targets.map(({ zonePlan, existing, }) => flowZoneOrganizeStep(zonePlan, existing, sync, plannedItemKeys));
|
|
296
|
+
const repositions = targets.flatMap(({ zonePlan, existing, }) => {
|
|
297
|
+
const requestedPosition = existing && flowZoneRepositionTarget(zonePlan, existing);
|
|
298
|
+
return requestedPosition
|
|
299
|
+
? [{ ...flowZoneSummary(existing), position: existing.position, requestedPosition, },]
|
|
300
|
+
: [];
|
|
301
|
+
});
|
|
302
|
+
if (repositions.length > 0 && !recreateOnMove) {
|
|
303
|
+
throw new ClientValidationError(`DSS cannot move ${repositions.length} existing flow zone(s): its API applies a zone position only when the zone is created.`, "validation_failed", "Drop position from those zones, or pass --recreate-on-move to replace each with a new zone at the requested position (items, shared items, and metadata carry over; the zone id changes, so update scenarios that build the zone by id).", { zones: repositions, });
|
|
304
|
+
}
|
|
193
305
|
const validation = validateObjects
|
|
194
306
|
? await validateFlowZoneOrganizeObjects(c, plan, pk)
|
|
195
307
|
: undefined;
|
|
196
308
|
if (validation)
|
|
197
309
|
throwFlowZoneValidationError(validation);
|
|
310
|
+
const writesPositions = repositions.length > 0
|
|
311
|
+
|| targets.some(({ zonePlan, existing, }) => !existing && zonePlan.position !== undefined);
|
|
312
|
+
const positioning = writesPositions ? await readManualPositioning(c, pk) : undefined;
|
|
313
|
+
if (positioning?.error !== undefined) {
|
|
314
|
+
enqueueCliWarning({
|
|
315
|
+
code: "zone_manual_positioning_unverified",
|
|
316
|
+
message: `Could not read project settings (${positioning.error}). DSS stores a zone position only when manual zone positioning is off at creation, and draws it only when it is on.`,
|
|
317
|
+
hint: enableManualPositioningCommand(pk),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
else if (positioning?.manual === false) {
|
|
321
|
+
enqueueCliWarning({
|
|
322
|
+
code: "zone_manual_positioning_disabled",
|
|
323
|
+
message: "This plan sets zone positions, but the project's manual zone positioning is off, so DSS lays zones out automatically and the positions do not show.",
|
|
324
|
+
hint: enableManualPositioningCommand(pk),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// With manual positioning on, DSS auto-places new zones and drops the
|
|
328
|
+
// requested position, so it is switched off while zones are created.
|
|
329
|
+
const suspendManualPositioning = positioning?.manual === true;
|
|
330
|
+
const positionReport = positioning?.error === undefined && positioning
|
|
331
|
+
? { manualPositioning: positioning.manual, suspendManualPositioning, }
|
|
332
|
+
: {};
|
|
198
333
|
const itemCount = plan.zones.reduce((count, zonePlan) => count + zonePlan.items.length, 0);
|
|
199
334
|
const pruneItemCount = planned.reduce((count, step) => {
|
|
200
335
|
const pruneItems = Array.isArray(step.pruneItems) ? step.pruneItems : [];
|
|
@@ -208,10 +343,12 @@ export const flowZoneCommands = withUsage("flow-zone", {
|
|
|
208
343
|
projectKey: pk,
|
|
209
344
|
sync,
|
|
210
345
|
validateObjects,
|
|
346
|
+
recreateOnMove,
|
|
211
347
|
topologyFingerprint,
|
|
212
348
|
zoneCount: plan.zones.length,
|
|
213
349
|
itemCount,
|
|
214
350
|
pruneItemCount,
|
|
351
|
+
...positionReport,
|
|
215
352
|
...(validation ? { validation, } : {}),
|
|
216
353
|
planned,
|
|
217
354
|
};
|
|
@@ -219,54 +356,101 @@ export const flowZoneCommands = withUsage("flow-zone", {
|
|
|
219
356
|
const currentZones = [...zones,];
|
|
220
357
|
const created = [];
|
|
221
358
|
const updated = [];
|
|
359
|
+
const recreated = [];
|
|
222
360
|
const moved = [];
|
|
223
361
|
const pruned = [];
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
color: zonePlan.color,
|
|
233
|
-
position: zonePlan.position,
|
|
234
|
-
projectKey: pk,
|
|
362
|
+
const positionsNotApplied = [];
|
|
363
|
+
const checkPosition = (zone, requested) => {
|
|
364
|
+
if (requested && !flowZoneSamePosition(flowZoneCurrentPosition(zone), requested)) {
|
|
365
|
+
positionsNotApplied.push({
|
|
366
|
+
zoneId: zone.id,
|
|
367
|
+
name: zone.name,
|
|
368
|
+
requested,
|
|
369
|
+
stored: zone.position,
|
|
235
370
|
});
|
|
236
|
-
currentZones.push(zone);
|
|
237
|
-
created.push(zone);
|
|
238
371
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
zone = await c.flowZones.
|
|
251
|
-
|
|
372
|
+
};
|
|
373
|
+
let failure;
|
|
374
|
+
if (suspendManualPositioning)
|
|
375
|
+
await setManualPositioning(c, pk, false);
|
|
376
|
+
try {
|
|
377
|
+
for (const zonePlan of plan.zones) {
|
|
378
|
+
let zone = findFlowZoneForPlan(currentZones, zonePlan);
|
|
379
|
+
ensureFlowZonePlanTarget(zonePlan, zone);
|
|
380
|
+
const moveItems = flowZoneMoveDelta(zone, zonePlan.items);
|
|
381
|
+
const pruneItems = sync ? flowZonePruneItems(zone, plannedItemKeys) : [];
|
|
382
|
+
if (!zone) {
|
|
383
|
+
zone = await c.flowZones.create({
|
|
384
|
+
name: zonePlan.name,
|
|
385
|
+
color: zonePlan.color,
|
|
386
|
+
position: zonePlan.position,
|
|
387
|
+
projectKey: pk,
|
|
388
|
+
});
|
|
389
|
+
currentZones.push(zone);
|
|
390
|
+
created.push(zone);
|
|
391
|
+
checkPosition(zone, zonePlan.position);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
const name = zonePlan.name && zonePlan.name !== zone.name ? zonePlan.name : undefined;
|
|
395
|
+
const color = zonePlan.color && zonePlan.color !== zone.color ? zonePlan.color : undefined;
|
|
396
|
+
const replacedId = zone.id;
|
|
397
|
+
const position = flowZoneRepositionTarget(zonePlan, zone);
|
|
398
|
+
if (position) {
|
|
399
|
+
const result = await c.flowZones.recreate(replacedId, {
|
|
400
|
+
position,
|
|
401
|
+
name,
|
|
402
|
+
color,
|
|
403
|
+
projectKey: pk,
|
|
404
|
+
});
|
|
405
|
+
zone = result.zone;
|
|
406
|
+
recreated.push(result);
|
|
407
|
+
checkPosition(zone, position);
|
|
408
|
+
}
|
|
409
|
+
else if (name !== undefined || color !== undefined) {
|
|
410
|
+
zone = await c.flowZones.update(replacedId, { name, color, projectKey: pk, });
|
|
411
|
+
updated.push(zone);
|
|
412
|
+
}
|
|
413
|
+
const index = currentZones.findIndex((candidate) => candidate.id === replacedId);
|
|
252
414
|
if (index !== -1)
|
|
253
415
|
currentZones[index] = zone;
|
|
254
|
-
|
|
416
|
+
}
|
|
417
|
+
if (moveItems.length > 0) {
|
|
418
|
+
await c.flowZones.moveItems(zone.id, moveItems, pk);
|
|
419
|
+
moved.push({ zoneId: zone.id, name: zone.name, items: moveItems, });
|
|
420
|
+
}
|
|
421
|
+
if (pruneItems.length > 0) {
|
|
422
|
+
await c.flowZones.moveItems("default", pruneItems, pk);
|
|
423
|
+
pruned.push({ zoneId: "default", fromZoneId: zone.id, name: zone.name, items: pruneItems, });
|
|
255
424
|
}
|
|
256
425
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
failure = error;
|
|
429
|
+
}
|
|
430
|
+
if (suspendManualPositioning) {
|
|
431
|
+
try {
|
|
432
|
+
await setManualPositioning(c, pk, true);
|
|
260
433
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
pruned.push({ zoneId: "default", fromZoneId: zone.id, name: zone.name, items: pruneItems, });
|
|
434
|
+
catch (restoreError) {
|
|
435
|
+
throw new ClientValidationError("Manual zone positioning was switched off to place zones and could not be switched back on.", "ambiguous_outcome", enableManualPositioningCommand(pk), failure instanceof Error ? { organizeError: failure.message, } : undefined, { cause: restoreError, });
|
|
264
436
|
}
|
|
265
437
|
}
|
|
438
|
+
if (failure !== undefined)
|
|
439
|
+
throw failure;
|
|
266
440
|
const topologyFingerprintAfter = await c.projects.topologyFingerprint(pk);
|
|
267
441
|
if (topologyFingerprintAfter !== topologyFingerprint) {
|
|
268
442
|
throw new ClientValidationError("Visual flow-zone changes modified flow topology.", "assertion_failed", "Inspect the flow before applying any further organization changes.", { before: topologyFingerprint, after: topologyFingerprintAfter, });
|
|
269
443
|
}
|
|
444
|
+
if (positionsNotApplied.length > 0) {
|
|
445
|
+
throw new ClientValidationError(`DSS did not store the requested position for ${positionsNotApplied.length} flow zone(s).`, "assertion_failed", "Items were organized; inspect the zones with dss flow-zone list.", {
|
|
446
|
+
positionsNotApplied,
|
|
447
|
+
created: created.map((zone) => zone.id),
|
|
448
|
+
recreated: recreated.map(({ replacedZoneId, zone, }) => ({
|
|
449
|
+
replacedZoneId,
|
|
450
|
+
zoneId: zone.id,
|
|
451
|
+
})),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
270
454
|
return {
|
|
271
455
|
organized: true,
|
|
272
456
|
action: "organize",
|
|
@@ -274,22 +458,33 @@ export const flowZoneCommands = withUsage("flow-zone", {
|
|
|
274
458
|
projectKey: pk,
|
|
275
459
|
sync,
|
|
276
460
|
validateObjects,
|
|
461
|
+
recreateOnMove,
|
|
277
462
|
topologyFingerprint,
|
|
278
463
|
topologyUnchanged: true,
|
|
279
464
|
zoneCount: plan.zones.length,
|
|
280
465
|
itemCount,
|
|
281
466
|
pruneItemCount,
|
|
467
|
+
...positionReport,
|
|
282
468
|
created,
|
|
283
469
|
updated,
|
|
470
|
+
recreated,
|
|
284
471
|
moved,
|
|
285
472
|
pruned,
|
|
286
473
|
};
|
|
287
474
|
},
|
|
288
|
-
description: "Create/update flow zones and move objects from a declarative
|
|
475
|
+
description: "Create/update flow zones and move objects from a declarative plan. A recipe's outputs share its zone: listing either places both (impliedItems); splitting them is rejected. DSS stores a zone position only at creation: moving an existing zone needs --recreate-on-move (new id); manual zone positioning is switched off while zones are placed, then restored.",
|
|
289
476
|
examples: [
|
|
290
477
|
"dss flow-zone organize --file flow-zones.json --dry-run",
|
|
291
478
|
`dss flow-zone organize --data '{"zones":[{"name":"Raw","color":"#64748b","datasets":["raw_orders"]}]}'`,
|
|
479
|
+
"dss flow-zone organize --file flow-zones.json --recreate-on-move --dry-run",
|
|
292
480
|
],
|
|
481
|
+
payloadSchema: {
|
|
482
|
+
stdin: true,
|
|
483
|
+
dataFlag: true,
|
|
484
|
+
dataFileFlag: true,
|
|
485
|
+
jsonShape: "object",
|
|
486
|
+
jsonSchema: FLOW_ZONE_ORGANIZE_PLAN_SCHEMA,
|
|
487
|
+
},
|
|
293
488
|
},
|
|
294
489
|
graph: {
|
|
295
490
|
handler: (c, a, f) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile, } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve, } from "node:path";
|
|
3
|
+
import { ClientValidationError, DataikuError, } from "../../errors.js";
|
|
3
4
|
import { asRecord, } from "../../utils/records.js";
|
|
4
5
|
import { jobBuildTargetTypeFromFlags, jobLogFilterFromFlag, maxLogLinesFromFlags, num, } from "../coerce.js";
|
|
5
6
|
import { executionMode, } from "../flags.js";
|
|
@@ -14,8 +15,22 @@ import { requireArgs, UsageError, } from "../usage.js";
|
|
|
14
15
|
async function warnIfBuiltDatasetHasNoColumns(c, target, targetType, success, projectKey) {
|
|
15
16
|
if (!success || targetType !== "DATASET")
|
|
16
17
|
return;
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
let schema;
|
|
19
|
+
try {
|
|
20
|
+
schema = await c.datasets.schema(target, projectKey);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (!(error instanceof DataikuError || error instanceof ClientValidationError))
|
|
24
|
+
throw error;
|
|
25
|
+
enqueueCliWarning({
|
|
26
|
+
code: "built_dataset_schema_unchecked",
|
|
27
|
+
dataset: target,
|
|
28
|
+
error: error.message.split("\n")[0],
|
|
29
|
+
hint: "The build succeeded but the dataset schema could not be read to check it is not empty.",
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (schema.columns.length > 0)
|
|
19
34
|
return;
|
|
20
35
|
enqueueCliWarning({
|
|
21
36
|
code: "built_dataset_has_no_columns",
|
|
@@ -350,7 +365,7 @@ export const jobCommands = withUsage("job", {
|
|
|
350
365
|
await warnIfBuiltDatasetHasNoColumns(c, a[0], options.targetType, result.success, pk);
|
|
351
366
|
return result;
|
|
352
367
|
},
|
|
353
|
-
description: "Build a dataset or managed folder and wait for completion. --auto-update-schema lets DSS update each recipe's output schema before it runs; a successful dataset build that leaves an empty schema emits a built_dataset_has_no_columns warning.",
|
|
368
|
+
description: "Build a dataset or managed folder and wait for completion. --auto-update-schema lets DSS update each recipe's output schema before it runs; a successful dataset build that leaves an empty schema emits a built_dataset_has_no_columns warning. A managed dataset no recipe produces fails (exit 4).",
|
|
354
369
|
examples: [
|
|
355
370
|
"dss job build-and-wait orders",
|
|
356
371
|
"dss job build-and-wait orders --include-logs --log-filter stdout --summary",
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { recipeCreateWarnings } from "../../resources/recipe-create.js";
|
|
1
2
|
import type { CommandMeta } from "../types.js";
|
|
3
|
+
/** Queue the stderr warnings for `recipe create` options (also used by `--plan` and `--dry-run`). */
|
|
4
|
+
export declare function enqueueRecipeCreateWarnings(options: Parameters<typeof recipeCreateWarnings>[0]): void;
|
|
2
5
|
/** Maps `recipe create` flags to SDK options; shared by the handler and `--plan`. */
|
|
3
6
|
export declare function recipeCreateOptionsFromFlags(f: Record<string, string | boolean>): {
|
|
4
7
|
type: string;
|
|
@@ -16,6 +19,7 @@ export declare function recipeCreateOptionsFromFlags(f: Record<string, string |
|
|
|
16
19
|
orderBy?: string | undefined;
|
|
17
20
|
formula?: string | undefined;
|
|
18
21
|
filter?: string | undefined;
|
|
22
|
+
stepOrder?: string[] | undefined;
|
|
19
23
|
projectKey: string | undefined;
|
|
20
24
|
aggregate?: string[] | undefined;
|
|
21
25
|
dropColumns?: string[] | undefined;
|
|
@@ -2,7 +2,7 @@ import { readFileSync, } from "node:fs";
|
|
|
2
2
|
import { writeFile, } from "node:fs/promises";
|
|
3
3
|
import { join, } from "node:path";
|
|
4
4
|
import { ClientValidationError, DataikuError, } from "../../errors.js";
|
|
5
|
-
import { recipeOutputSchemaIsComputable, schemaUpdateComputableId, schemaUpdateIsPending, } from "../../resources/recipe-create.js";
|
|
5
|
+
import { recipeCreateWarnings, recipeOutputSchemaIsComputable, schemaUpdateComputableId, schemaUpdateIsPending, } from "../../resources/recipe-create.js";
|
|
6
6
|
import { deepMerge, } from "../../utils/deep-merge.js";
|
|
7
7
|
import { jobLogFilterFromFlag, jsonInput, maxLogLinesFromFlags, normalizeLineEndings, num, recipeInputDatasetsFromFlags, requiredJsonInput, requiredStringFlag, rewritePairsFromFlags, sha256Hex, splitCsvFlag, stableHash, } from "../coerce.js";
|
|
8
8
|
import { executionMode, } from "../flags.js";
|
|
@@ -107,6 +107,19 @@ function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
|
|
|
107
107
|
}
|
|
108
108
|
return lines.join("\n");
|
|
109
109
|
}
|
|
110
|
+
const PREPARE_STEP_FLAGS = {
|
|
111
|
+
"rename": "rename",
|
|
112
|
+
"fill-empty": "fillEmpty",
|
|
113
|
+
"formula": "formula",
|
|
114
|
+
"filter": "filter",
|
|
115
|
+
"drop-columns": "dropColumns",
|
|
116
|
+
"keep-columns": "keepColumns",
|
|
117
|
+
};
|
|
118
|
+
/** Queue the stderr warnings for `recipe create` options (also used by `--plan` and `--dry-run`). */
|
|
119
|
+
export function enqueueRecipeCreateWarnings(options) {
|
|
120
|
+
for (const warning of recipeCreateWarnings(options))
|
|
121
|
+
enqueueCliWarning(warning);
|
|
122
|
+
}
|
|
110
123
|
/** Maps `recipe create` flags to SDK options; shared by the handler and `--plan`. */
|
|
111
124
|
export function recipeCreateOptionsFromFlags(f) {
|
|
112
125
|
const type = f["type"];
|
|
@@ -145,6 +158,11 @@ export function recipeCreateOptionsFromFlags(f) {
|
|
|
145
158
|
fuzzyDistance = normalized;
|
|
146
159
|
}
|
|
147
160
|
const fuzzyThreshold = num(f["fuzzy-threshold"], "--fuzzy-threshold");
|
|
161
|
+
// Prepare option groups run in the order their flags were typed (object keys keep first-use order).
|
|
162
|
+
const stepOrder = Object.keys(f).flatMap((flag) => {
|
|
163
|
+
const kind = Object.hasOwn(PREPARE_STEP_FLAGS, flag) ? PREPARE_STEP_FLAGS[flag] : undefined;
|
|
164
|
+
return kind ? [kind,] : [];
|
|
165
|
+
});
|
|
148
166
|
const payload = {
|
|
149
167
|
type,
|
|
150
168
|
name,
|
|
@@ -167,6 +185,7 @@ export function recipeCreateOptionsFromFlags(f) {
|
|
|
167
185
|
...(typeof f["filter"] === "string" ? { filter: f["filter"], } : {}),
|
|
168
186
|
...csvOption("dropColumns", f["drop-columns"]),
|
|
169
187
|
...csvOption("keepColumns", f["keep-columns"]),
|
|
188
|
+
...(stepOrder.length > 0 ? { stepOrder, } : {}),
|
|
170
189
|
projectKey: pk,
|
|
171
190
|
};
|
|
172
191
|
return payload;
|
|
@@ -330,6 +349,7 @@ export const recipeCommands = withUsage("recipe", {
|
|
|
330
349
|
create: {
|
|
331
350
|
handler: async (c, _a, f) => {
|
|
332
351
|
const payload = recipeCreateOptionsFromFlags(f);
|
|
352
|
+
enqueueRecipeCreateWarnings(payload);
|
|
333
353
|
const name = payload.name;
|
|
334
354
|
const pk = payload.projectKey;
|
|
335
355
|
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
@@ -369,6 +389,23 @@ export const recipeCommands = withUsage("recipe", {
|
|
|
369
389
|
objectId: createdName,
|
|
370
390
|
objectType: "RECIPE",
|
|
371
391
|
},], pk);
|
|
392
|
+
if (created.requestedName) {
|
|
393
|
+
// DSS appends _N when the requested name is taken; any other difference is DSS naming
|
|
394
|
+
// the recipe itself. The public API has no recipe rename.
|
|
395
|
+
const suffix = createdName.startsWith(`${created.requestedName}_`)
|
|
396
|
+
? createdName.slice(created.requestedName.length + 1)
|
|
397
|
+
: "";
|
|
398
|
+
const nameTaken = /^\d+$/.test(suffix);
|
|
399
|
+
enqueueCliWarning({
|
|
400
|
+
code: "recipe_renamed",
|
|
401
|
+
requestedName: created.requestedName,
|
|
402
|
+
recipe: createdName,
|
|
403
|
+
reason: nameTaken ? "name_taken" : "dss_chose_name",
|
|
404
|
+
hint: nameTaken
|
|
405
|
+
? `A recipe named ${created.requestedName} already exists; use ${createdName} in follow-up commands.`
|
|
406
|
+
: `DSS ignored the requested name; use ${createdName} in follow-up commands.`,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
372
409
|
if (created.outputSchemaUpdateError) {
|
|
373
410
|
enqueueCliWarning({
|
|
374
411
|
code: "recipe_output_schema_not_computed",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findWikiAttachment, statWikiAttachmentFile, wikiAttachmentNotFound, wikiAttachments, } from "../../resources/wiki.js";
|
|
1
2
|
import { deepMerge, } from "../../utils/deep-merge.js";
|
|
2
3
|
import { jsonInput, textInput, } from "../coerce.js";
|
|
3
4
|
import { executionMode, } from "../flags.js";
|
|
@@ -117,6 +118,61 @@ export const wikiCommands = withUsage("wiki", {
|
|
|
117
118
|
description: "Delete a wiki article.",
|
|
118
119
|
examples: ["dss wiki delete ARTICLE_ID --dry-run",],
|
|
119
120
|
},
|
|
121
|
+
attach: {
|
|
122
|
+
handler: async (c, a, f) => {
|
|
123
|
+
requireArgs(a, 2, commandUsage("wiki", "attach"));
|
|
124
|
+
const pk = f["project-key"];
|
|
125
|
+
const file = await statWikiAttachmentFile(a[1]);
|
|
126
|
+
if (executionMode(f).dryRun) {
|
|
127
|
+
const current = await c.wiki.get(a[0], pk);
|
|
128
|
+
return {
|
|
129
|
+
dryRun: true,
|
|
130
|
+
action: "attach",
|
|
131
|
+
resource: "wiki",
|
|
132
|
+
article: a[0],
|
|
133
|
+
file,
|
|
134
|
+
current: wikiAttachments(current),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const result = await c.wiki.attach(a[0], a[1], pk);
|
|
138
|
+
return { attached: result.attachment.smartId, resource: "wiki", ...result, };
|
|
139
|
+
},
|
|
140
|
+
description: "Upload a local file and attach it to a wiki article; returns its smartId.",
|
|
141
|
+
examples: ["dss wiki attach ARTICLE_ID ./diagram.png --dry-run",],
|
|
142
|
+
},
|
|
143
|
+
detach: {
|
|
144
|
+
handler: async (c, a, f) => {
|
|
145
|
+
requireArgs(a, 2, commandUsage("wiki", "detach"));
|
|
146
|
+
const pk = f["project-key"];
|
|
147
|
+
const ifExists = f["if-exists"] === true;
|
|
148
|
+
const dryRun = executionMode(f).dryRun;
|
|
149
|
+
if (dryRun || ifExists) {
|
|
150
|
+
const current = ifExists
|
|
151
|
+
? await readIfExists(() => c.wiki.get(a[0], pk))
|
|
152
|
+
: await c.wiki.get(a[0], pk);
|
|
153
|
+
const attachment = current && findWikiAttachment(current, a[1]);
|
|
154
|
+
if (!current || !attachment) {
|
|
155
|
+
if (!ifExists && current)
|
|
156
|
+
throw wikiAttachmentNotFound(current, a[1]);
|
|
157
|
+
return skipResult("wiki", a[1], "missing", { article: a[0], });
|
|
158
|
+
}
|
|
159
|
+
if (dryRun) {
|
|
160
|
+
return {
|
|
161
|
+
dryRun: true,
|
|
162
|
+
action: "detach",
|
|
163
|
+
resource: "wiki",
|
|
164
|
+
article: a[0],
|
|
165
|
+
smartId: a[1],
|
|
166
|
+
current: attachment,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const result = await c.wiki.detach(a[0], a[1], pk);
|
|
171
|
+
return { resource: "wiki", ...result, };
|
|
172
|
+
},
|
|
173
|
+
description: "Remove one attachment from a wiki article by smartId.",
|
|
174
|
+
examples: ["dss wiki detach ARTICLE_ID SMART_ID --if-exists",],
|
|
175
|
+
},
|
|
120
176
|
"update-settings": {
|
|
121
177
|
handler: async (c, _a, f) => {
|
|
122
178
|
const pk = f["project-key"];
|
package/dist/src/cli/contract.js
CHANGED
|
@@ -411,6 +411,8 @@ function payloadJsonSchema(payloadSchema) {
|
|
|
411
411
|
if (payloadSchema.contentType === "text/plain") {
|
|
412
412
|
return { type: "string", contentMediaType: "text/plain", };
|
|
413
413
|
}
|
|
414
|
+
if (payloadSchema.jsonSchema)
|
|
415
|
+
return payloadSchema.jsonSchema;
|
|
414
416
|
return payloadSchema.jsonShape === "array"
|
|
415
417
|
? { type: "array", items: true, }
|
|
416
418
|
: { type: "object", additionalProperties: true, };
|
|
@@ -722,7 +724,7 @@ function inferSideEffect(resource, action) {
|
|
|
722
724
|
}
|
|
723
725
|
if (READ_ACTIONS.has(action))
|
|
724
726
|
return "read";
|
|
725
|
-
if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout|add|remove|publish|activate|deploy|import|export|preload|upgrade|start|stop|restart|duplicate|put|rename|reply|compute|organize)/
|
|
727
|
+
if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout|add|remove|publish|activate|deploy|import|export|preload|upgrade|start|stop|restart|duplicate|put|rename|reply|compute|organize|attach|detach)/
|
|
726
728
|
.test(action)
|
|
727
729
|
// Compound actions whose mutating verb is a suffix (e.g. permissions-set,
|
|
728
730
|
// dataset-compute, saved-model external-metadata-put).
|
|
@@ -876,6 +878,8 @@ export function supportsCleanupLedger(resource, action) {
|
|
|
876
878
|
const EXPLICIT_DESTRUCTIVE_KEYS = {
|
|
877
879
|
"dataset.upload-file": true,
|
|
878
880
|
"sql.query": true,
|
|
881
|
+
// Detaching drops the attachment from the article; no public route restores it.
|
|
882
|
+
"wiki.detach": true,
|
|
879
883
|
// Plugin dev-Git and content deletions discard work with no undo route.
|
|
880
884
|
"plugin.delete": true,
|
|
881
885
|
"plugin.delete-git-remote": true,
|
|
@@ -961,8 +965,10 @@ function inferIdempotency(resource, sideEffect, action, syntax) {
|
|
|
961
965
|
if (action.startsWith("create") && syntaxHasFlag(syntax, "if-not-exists")) {
|
|
962
966
|
return "if-not-exists";
|
|
963
967
|
}
|
|
964
|
-
if (action.startsWith("delete")
|
|
968
|
+
if ((action.startsWith("delete") || `${resource}.${action}` === "wiki.detach")
|
|
969
|
+
&& syntaxHasFlag(syntax, "if-exists")) {
|
|
965
970
|
return "if-exists";
|
|
971
|
+
}
|
|
966
972
|
if (`${resource}.${action}` === "app.set-manifest-version")
|
|
967
973
|
return "none";
|
|
968
974
|
// `app delete-instance` converges without an `--if-exists` flag: an absent
|
|
@@ -1062,6 +1068,8 @@ export function buildRegistryEntry(resource, action, meta) {
|
|
|
1062
1068
|
const payloadSchema = meta.payloadSchema
|
|
1063
1069
|
?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.payloadSchema
|
|
1064
1070
|
?? inferPayloadSchema(inputContract);
|
|
1071
|
+
// The precise JSON Schema is published once, as `schemas.input`.
|
|
1072
|
+
const { jsonSchema: _jsonSchema, ...payloadSummary } = payloadSchema ?? {};
|
|
1065
1073
|
const examplePayload = meta.examplePayload
|
|
1066
1074
|
?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.examplePayload;
|
|
1067
1075
|
const inferredCleanupCommand = meta.cleanupCommand
|
|
@@ -1126,7 +1134,7 @@ export function buildRegistryEntry(resource, action, meta) {
|
|
|
1126
1134
|
optionalFlags: uniqueOptionalFlags,
|
|
1127
1135
|
...(requiredOneOf.length > 0 ? { requiredOneOf, } : {}),
|
|
1128
1136
|
...(inputGroups.length > 0 ? { requiredInputGroups: inputGroups, } : {}),
|
|
1129
|
-
...(payloadSchema ? { payloadSchema, } : {}),
|
|
1137
|
+
...(payloadSchema ? { payloadSchema: payloadSummary, } : {}),
|
|
1130
1138
|
get schemas() {
|
|
1131
1139
|
return schemas ??= buildCommandSchemas(resource, action, flagMetadata, uniqueRequiredFlags, requiredOneOf, payloadSchema, outputShape, meta.usage);
|
|
1132
1140
|
},
|
package/dist/src/cli/doctor.js
CHANGED
|
@@ -11,6 +11,8 @@ export function errorDetails(error) {
|
|
|
11
11
|
retryable: error.retryable,
|
|
12
12
|
status: error.status,
|
|
13
13
|
statusText: error.statusText,
|
|
14
|
+
message: error.message,
|
|
15
|
+
retryHint: error.retryHint,
|
|
14
16
|
};
|
|
15
17
|
}
|
|
16
18
|
return { message: error instanceof Error ? error.message : String(error), };
|