dataiku-sdk 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +4 -2
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +40 -4
  4. package/dist/packages/types/src/index.js +22 -1
  5. package/dist/src/cli/coerce.d.ts +1 -0
  6. package/dist/src/cli/coerce.js +1 -1
  7. package/dist/src/cli/command-syntax.json +156 -0
  8. package/dist/src/cli/commands/dataset.js +44 -8
  9. package/dist/src/cli/commands/flow-zone.js +241 -46
  10. package/dist/src/cli/commands/job.js +52 -9
  11. package/dist/src/cli/commands/project.js +4 -3
  12. package/dist/src/cli/commands/recipe.d.ts +4 -0
  13. package/dist/src/cli/commands/recipe.js +38 -1
  14. package/dist/src/cli/commands/wiki.js +56 -0
  15. package/dist/src/cli/contract.js +13 -8
  16. package/dist/src/cli/doctor.js +2 -0
  17. package/dist/src/cli/flags.d.ts +7 -0
  18. package/dist/src/cli/flags.js +28 -2
  19. package/dist/src/cli/helpers/dataset.d.ts +12 -0
  20. package/dist/src/cli/helpers/dataset.js +36 -0
  21. package/dist/src/cli/helpers/flow-zone.d.ts +21 -0
  22. package/dist/src/cli/helpers/flow-zone.js +144 -4
  23. package/dist/src/cli/output-schemas.js +27 -3
  24. package/dist/src/cli/output.js +6 -0
  25. package/dist/src/cli/plans.js +58 -2
  26. package/dist/src/cli/project-map-output.d.ts +24 -0
  27. package/dist/src/cli/project-map-output.js +21 -0
  28. package/dist/src/cli/read-shapes.d.ts +14 -0
  29. package/dist/src/cli/read-shapes.js +114 -0
  30. package/dist/src/cli/result-shapes.d.ts +6 -0
  31. package/dist/src/cli/result-shapes.js +235 -0
  32. package/dist/src/cli/types.d.ts +2 -0
  33. package/dist/src/cli/version.d.ts +2 -2
  34. package/dist/src/cli/version.js +2 -2
  35. package/dist/src/cli.js +23 -20
  36. package/dist/src/errors.d.ts +11 -1
  37. package/dist/src/errors.js +18 -3
  38. package/dist/src/generated/action-output-schemas.json +224 -35
  39. package/dist/src/resources/dataset-create.d.ts +19 -0
  40. package/dist/src/resources/dataset-create.js +69 -0
  41. package/dist/src/resources/datasets.d.ts +21 -0
  42. package/dist/src/resources/datasets.js +61 -1
  43. package/dist/src/resources/flow-zones.d.ts +29 -3
  44. package/dist/src/resources/flow-zones.js +53 -3
  45. package/dist/src/resources/jobs.d.ts +36 -1
  46. package/dist/src/resources/jobs.js +111 -5
  47. package/dist/src/resources/projects.d.ts +9 -0
  48. package/dist/src/resources/projects.js +10 -3
  49. package/dist/src/resources/recipe-create.d.ts +17 -3
  50. package/dist/src/resources/recipe-create.js +149 -33
  51. package/dist/src/resources/recipes.js +76 -23
  52. package/dist/src/resources/sql.js +52 -13
  53. package/dist/src/resources/wiki.d.ts +47 -1
  54. package/dist/src/resources/wiki.js +89 -0
  55. package/dist/src/utils/flow-analysis.js +4 -0
  56. package/dist/src/utils/secret-sanitize.d.ts +11 -2
  57. package/dist/src/utils/secret-sanitize.js +23 -6
  58. package/package.json +4 -4
  59. package/plugin.json +1 -1
  60. package/skills/dataiku-dss/references/discovery.md +2 -2
  61. package/skills/dataiku-dss/references/flow-maps.md +2 -2
  62. package/skills/dataiku-dss/references/troubleshooting.md +3 -3
@@ -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.topologyFingerprint(pk),
264
+ c.projects.flowTopology(pk),
171
265
  ]);
172
- return flowZonePlanFromZones(zones, topologyFingerprint);
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 plan = readFlowZoneOrganizePlan(f, usage);
278
+ const parsed = readFlowZoneOrganizePlan(f, usage);
182
279
  const sync = f["sync"] === true;
183
280
  const validateObjects = f["validate-objects"] === true;
184
- const [zones, topologyFingerprint,] = await Promise.all([
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.topologyFingerprint(pk),
284
+ c.projects.flowTopology(pk),
187
285
  ]);
188
- if (plan.topologyFingerprint && plan.topologyFingerprint !== topologyFingerprint) {
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: plan.topologyFingerprint, actual: topologyFingerprint, });
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 planned = plan.zones.map((zonePlan) => flowZoneOrganizeStep(zonePlan, findFlowZoneForPlan(zones, zonePlan), sync, plannedItemKeys));
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
- for (const zonePlan of plan.zones) {
225
- let zone = findFlowZoneForPlan(currentZones, zonePlan);
226
- ensureFlowZonePlanTarget(zonePlan, zone);
227
- const moveItems = flowZoneMoveDelta(zone, zonePlan.items);
228
- const pruneItems = sync ? flowZonePruneItems(zone, plannedItemKeys) : [];
229
- if (!zone) {
230
- zone = await c.flowZones.create({
231
- name: zonePlan.name,
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
- else {
240
- const patch = {
241
- ...(zonePlan.name && zonePlan.name !== zone.name ? { name: zonePlan.name, } : {}),
242
- ...(zonePlan.color && zonePlan.color !== zone.color ? { color: zonePlan.color, } : {}),
243
- ...(zonePlan.position !== undefined
244
- && !flowZoneSamePosition(flowZoneCurrentPosition(zone), zonePlan.position)
245
- ? { position: zonePlan.position, }
246
- : {}),
247
- projectKey: pk,
248
- };
249
- if (patch.name !== undefined || patch.color !== undefined || patch.position !== undefined) {
250
- zone = await c.flowZones.update(zone.id, patch);
251
- const index = currentZones.findIndex((candidate) => candidate.id === zone.id);
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
- updated.push(zone);
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
- if (moveItems.length > 0) {
258
- await c.flowZones.moveItems(zone.id, moveItems, pk);
259
- moved.push({ zoneId: zone.id, name: zone.name, items: moveItems, });
426
+ }
427
+ catch (error) {
428
+ failure = error;
429
+ }
430
+ if (suspendManualPositioning) {
431
+ try {
432
+ await setManualPositioning(c, pk, true);
260
433
  }
261
- if (pruneItems.length > 0) {
262
- await c.flowZones.moveItems("default", pruneItems, pk);
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 visual organization plan.",
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,11 +1,38 @@
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";
6
7
  import { encodedProjectEndpoint, enqueueCliWarning, } from "../output.js";
7
8
  import { commandUsage, withUsage, } from "../syntax.js";
8
9
  import { requireArgs, UsageError, } from "../usage.js";
10
+ /** Lines `job log` prints to stdout when no `--max-lines` is given. */
11
+ const DEFAULT_STDOUT_LOG_LINES = 100;
12
+ /**
13
+ * Tail a (filtered) job log for stdout. `0`/`-1` return everything; a
14
+ * truncated tail enqueues a `log_truncated` warning carrying the shown and
15
+ * total line counts.
16
+ */
17
+ function tailJobLogForStdout(log, limit) {
18
+ if (!log || limit === 0 || limit === -1)
19
+ return log;
20
+ const lines = log.split(/\r?\n/);
21
+ const hasTrailingLineBreak = lines[lines.length - 1] === "";
22
+ if (hasTrailingLineBreak)
23
+ lines.pop();
24
+ const shown = Math.max(1, limit);
25
+ if (lines.length <= shown)
26
+ return log;
27
+ enqueueCliWarning({
28
+ code: "log_truncated",
29
+ shown,
30
+ total: lines.length,
31
+ hint: "Raise --max-lines (0 = all), add --errors-only, or use --output PATH.",
32
+ });
33
+ const tail = lines.slice(-shown).join("\n");
34
+ return hasTrailingLineBreak ? `${tail}\n` : tail;
35
+ }
9
36
  /**
10
37
  * With an empty output schema DSS ends a dataset build DONE after writing no
11
38
  * columns and no rows, without an error. Flag that after a successful build so
@@ -14,8 +41,22 @@ import { requireArgs, UsageError, } from "../usage.js";
14
41
  async function warnIfBuiltDatasetHasNoColumns(c, target, targetType, success, projectKey) {
15
42
  if (!success || targetType !== "DATASET")
16
43
  return;
17
- const schema = await c.datasets.schema(target, projectKey).catch(() => undefined);
18
- if (!schema || schema.columns.length > 0)
44
+ let schema;
45
+ try {
46
+ schema = await c.datasets.schema(target, projectKey);
47
+ }
48
+ catch (error) {
49
+ if (!(error instanceof DataikuError || error instanceof ClientValidationError))
50
+ throw error;
51
+ enqueueCliWarning({
52
+ code: "built_dataset_schema_unchecked",
53
+ dataset: target,
54
+ error: error.message.split("\n")[0],
55
+ hint: "The build succeeded but the dataset schema could not be read to check it is not empty.",
56
+ });
57
+ return;
58
+ }
59
+ if (schema.columns.length > 0)
19
60
  return;
20
61
  enqueueCliWarning({
21
62
  code: "built_dataset_has_no_columns",
@@ -220,7 +261,7 @@ export const jobCommands = withUsage("job", {
220
261
  requireArgs(a, 1, commandUsage("job", "get"));
221
262
  return c.jobs.get(a[0], f["project-key"]);
222
263
  },
223
- description: "Get job details.",
264
+ description: "Get job state, outputs, progress, error, and per-activity status (compact by default; --full returns the raw DSS job object). The log tail is never included: use `dss job log ID --errors-only`.",
224
265
  examples: ["dss job get JOB_ID",],
225
266
  },
226
267
  summary: {
@@ -237,23 +278,25 @@ export const jobCommands = withUsage("job", {
237
278
  const logFilter = f["errors-only"] === true
238
279
  ? "errors"
239
280
  : jobLogFilterFromFlag(f["log-filter"]);
281
+ const outputFile = f["output"]
282
+ ?? f["output-file"];
283
+ const requestedLines = maxLogLinesFromFlags(f);
284
+ // Stdout is tailed here (not in the SDK) so the total can be reported.
240
285
  const log = await c.jobs.log(a[0], {
241
286
  activity: f["activity"],
242
287
  logId: f["log-id"],
243
288
  logFilter,
244
- maxLogLines: maxLogLinesFromFlags(f),
289
+ maxLogLines: outputFile ? requestedLines : -1,
245
290
  projectKey: f["project-key"],
246
291
  });
247
- const outputFile = f["output"]
248
- ?? f["output-file"];
249
292
  if (!outputFile)
250
- return log;
293
+ return tailJobLogForStdout(log, requestedLines ?? DEFAULT_STDOUT_LOG_LINES);
251
294
  const outputPath = resolve(outputFile);
252
295
  await mkdir(dirname(outputPath), { recursive: true, });
253
296
  await writeFile(outputPath, log.endsWith("\n") ? log : `${log}\n`, "utf-8");
254
297
  return outputPath;
255
298
  },
256
- description: "Get public API job log output; find why a job failed. Use --errors-only (or --log-filter errors) to surface just error/traceback lines, and --output PATH to write the log to a file (stdout returns the path). --log-id is accepted for UI parity but DSS API-key auth cannot select browser-only cat-activity-log files.",
299
+ description: "Get public API job log output (last 100 lines by default; --max-lines N, 0 = all); find why a job failed. Use --errors-only for error/traceback lines and --output PATH to write it to a file (stdout returns the path). --log-id is accepted for UI parity; API keys cannot read browser-only cat-activity-log files.",
257
300
  examples: [
258
301
  "dss job log JOB_ID",
259
302
  "dss job log JOB_ID --activity main --max-log-lines 200",
@@ -350,7 +393,7 @@ export const jobCommands = withUsage("job", {
350
393
  await warnIfBuiltDatasetHasNoColumns(c, a[0], options.targetType, result.success, pk);
351
394
  return result;
352
395
  },
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.",
396
+ 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
397
  examples: [
355
398
  "dss job build-and-wait orders",
356
399
  "dss job build-and-wait orders --include-logs --log-filter stdout --summary",
@@ -5,6 +5,7 @@ import { projectIncarnationHash, } from "../../utils/project-incarnation.js";
5
5
  import { jsonInput, num, requiredJsonInput, requiredStringFlag, } from "../coerce.js";
6
6
  import { executionMode, } from "../flags.js";
7
7
  import { CommandResultFailure, } from "../output.js";
8
+ import { compactProjectMapOutput, } from "../project-map-output.js";
8
9
  import { commandUsage, withUsage, } from "../syntax.js";
9
10
  import { requireArgs, UsageError, } from "../usage.js";
10
11
  const INSPECT_ARCHIVE_USAGE = commandUsage("project", "inspect-archive");
@@ -28,7 +29,7 @@ export const projectCommands = withUsage("project", {
28
29
  },
29
30
  get: {
30
31
  handler: (c, _a, f) => c.projects.get(f["project-key"]),
31
- description: "Get project settings and metadata.",
32
+ description: "Get project settings and metadata (compact by default: empty values, metrics/version/image metadata dropped, permission flags collapsed to deniedPermissions; --full returns the raw DSS object).",
32
33
  examples: ["dss project get", "dss project get --project-key MYPROJ",],
33
34
  },
34
35
  metadata: {
@@ -42,13 +43,13 @@ export const projectCommands = withUsage("project", {
42
43
  examples: ["dss project flow", "dss project flow --project-key MYPROJ",],
43
44
  },
44
45
  map: {
45
- handler: (c, _a, f) => c.projects.map({
46
+ handler: async (c, _a, f) => compactProjectMapOutput(await c.projects.map({
46
47
  projectKey: f["project-key"],
47
48
  maxNodes: num(f["max-nodes"], "--max-nodes"),
48
49
  maxEdges: num(f["max-edges"], "--max-edges"),
49
50
  includeRaw: f["include-raw"] === true,
50
51
  render: flowRenderFormat(f["render"]),
51
- }),
52
+ })),
52
53
  validate: (_a, f) => {
53
54
  flowRenderFormat(f["render"]);
54
55
  num(f["max-nodes"], "--max-nodes");
@@ -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",