dataiku-sdk 0.6.0 → 0.6.1

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.
@@ -1,7 +1,152 @@
1
+ import { DataikuError, } from "../errors.js";
1
2
  import { ScenarioDetailsSchema, ScenarioStatusSchema, ScenarioSummaryArraySchema, } from "../schemas.js";
2
3
  import { deepMerge, } from "../utils/deep-merge.js";
3
4
  import { BaseResource, } from "./base.js";
4
5
  import { computeNextPollDelayMs, } from "./jobs.js";
6
+ export const SCENARIO_CANONICAL_EDITABLE_FIELDS = [
7
+ "params.steps",
8
+ "params.triggers",
9
+ "params.reporters",
10
+ "params.customScript",
11
+ "active",
12
+ "name",
13
+ ];
14
+ function isRecord(value) {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+ function jsonValueEqual(left, right) {
18
+ if (Object.is(left, right))
19
+ return true;
20
+ if (Array.isArray(left) || Array.isArray(right)) {
21
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
22
+ return false;
23
+ return left.every((value, index) => jsonValueEqual(value, right[index]));
24
+ }
25
+ if (!isRecord(left) || !isRecord(right))
26
+ return false;
27
+ const leftKeys = Object.keys(left);
28
+ const rightKeys = Object.keys(right);
29
+ if (leftKeys.length !== rightKeys.length)
30
+ return false;
31
+ return leftKeys.every((key) => Object.hasOwn(right, key) && jsonValueEqual(left[key], right[key]));
32
+ }
33
+ function jsonValueContains(actual, expected) {
34
+ if (jsonValueEqual(actual, expected))
35
+ return true;
36
+ if (Array.isArray(actual) || Array.isArray(expected)) {
37
+ if (!Array.isArray(actual) || !Array.isArray(expected) || actual.length !== expected.length) {
38
+ return false;
39
+ }
40
+ return expected.every((value, index) => jsonValueContains(actual[index], value));
41
+ }
42
+ if (!isRecord(actual) || !isRecord(expected))
43
+ return false;
44
+ return Object.entries(expected).every(([key, value,]) => Object.hasOwn(actual, key) && jsonValueContains(actual[key], value));
45
+ }
46
+ function collectPatchPaths(value, prefix = "", paths = []) {
47
+ if (!isRecord(value)) {
48
+ if (prefix)
49
+ paths.push(prefix);
50
+ return paths;
51
+ }
52
+ const entries = Object.entries(value);
53
+ if (entries.length === 0) {
54
+ if (prefix)
55
+ paths.push(prefix);
56
+ return paths;
57
+ }
58
+ for (const [key, child,] of entries) {
59
+ collectPatchPaths(child, prefix ? `${prefix}.${key}` : key, paths);
60
+ }
61
+ return paths;
62
+ }
63
+ function valueAtPath(value, path) {
64
+ let current = value;
65
+ for (const part of path.split(".")) {
66
+ if (!isRecord(current))
67
+ return undefined;
68
+ current = current[part];
69
+ }
70
+ return current;
71
+ }
72
+ function scenarioFieldChanges(before, after, patch) {
73
+ const changes = [];
74
+ const unchangedPaths = [];
75
+ for (const path of collectPatchPaths(patch)) {
76
+ const beforeValue = valueAtPath(before, path);
77
+ const afterValue = valueAtPath(after, path);
78
+ if (jsonValueEqual(beforeValue, afterValue)) {
79
+ unchangedPaths.push(path);
80
+ continue;
81
+ }
82
+ changes.push({ path, before: beforeValue, after: afterValue, });
83
+ }
84
+ return { changes, unchangedPaths, };
85
+ }
86
+ function scenarioFieldMismatches(actual, expected, patch) {
87
+ const mismatches = [];
88
+ for (const path of collectPatchPaths(patch)) {
89
+ const actualValue = valueAtPath(actual, path);
90
+ const expectedValue = valueAtPath(expected, path);
91
+ if (!jsonValueContains(actualValue, expectedValue)) {
92
+ mismatches.push({ path, expected: expectedValue, actual: actualValue, });
93
+ }
94
+ }
95
+ return mismatches;
96
+ }
97
+ export function normalizeScenarioUpdateData(data) {
98
+ const normalizedData = { ...data, };
99
+ const normalization = [];
100
+ const rawParams = data.rawParams;
101
+ if (!isRecord(rawParams) || !isRecord(rawParams.params)) {
102
+ return { normalizedData, normalization, };
103
+ }
104
+ const canonicalParams = isRecord(data.params) ? data.params : undefined;
105
+ const mergedParams = canonicalParams === undefined
106
+ ? rawParams.params
107
+ : deepMerge(rawParams.params, canonicalParams);
108
+ const promoted = canonicalParams === undefined || !jsonValueEqual(mergedParams, canonicalParams);
109
+ if (promoted)
110
+ normalizedData.params = mergedParams;
111
+ const rawParamsWithoutParams = { ...rawParams, };
112
+ delete rawParamsWithoutParams.params;
113
+ if (Object.keys(rawParamsWithoutParams).length === 0)
114
+ delete normalizedData.rawParams;
115
+ else
116
+ normalizedData.rawParams = rawParamsWithoutParams;
117
+ normalization.push({
118
+ from: "rawParams.params",
119
+ to: "params",
120
+ action: promoted ? "promoted" : "ignored",
121
+ message: promoted
122
+ ? "rawParams.params is a DSS echo; the editable scenario definition uses params."
123
+ : "rawParams.params was ignored because canonical params already supplied the same editable fields.",
124
+ });
125
+ return { normalizedData, normalization, };
126
+ }
127
+ export function scenarioUpdatePreview(current, data) {
128
+ const { normalizedData, normalization, } = normalizeScenarioUpdateData(data);
129
+ const next = deepMerge(current, normalizedData);
130
+ const { changes, unchangedPaths, } = scenarioFieldChanges(current, next, normalizedData);
131
+ return {
132
+ canonicalEditableFields: SCENARIO_CANONICAL_EDITABLE_FIELDS,
133
+ normalization,
134
+ normalizedData,
135
+ current,
136
+ next,
137
+ changes,
138
+ unchangedPaths,
139
+ };
140
+ }
141
+ function scenarioUpdateVerificationError(mismatches, normalization) {
142
+ const mismatchPaths = mismatches.map((mismatch) => mismatch.path).join(", ");
143
+ return new DataikuError(400, "Scenario Update Verification Failed", JSON.stringify({
144
+ message: `Scenario update did not persist requested fields after refetch: ${mismatchPaths}`,
145
+ mismatches,
146
+ canonicalEditableFields: SCENARIO_CANONICAL_EDITABLE_FIELDS,
147
+ ...(normalization.length > 0 ? { normalization, } : {}),
148
+ }));
149
+ }
5
150
  export class ScenariosResource extends BaseResource {
6
151
  /** List all scenarios in a project. */
7
152
  async list(projectKey) {
@@ -42,13 +187,27 @@ export class ScenariosResource extends BaseResource {
42
187
  const raw = await this.client.get(`/public/api/projects/${this.enc(projectKey)}/scenarios/${scEnc}/light/`);
43
188
  return this.client.safeParse(ScenarioStatusSchema, raw, "scenarios.status");
44
189
  }
45
- /** Merge-update a scenario's definition. */
190
+ /** Merge-update a scenario's definition, then refetch and verify requested fields persisted. */
46
191
  async update(scenarioId, data, projectKey) {
47
192
  const scEnc = encodeURIComponent(scenarioId);
48
193
  const pkEnc = this.enc(projectKey);
49
194
  const current = await this.client.get(`/public/api/projects/${pkEnc}/scenarios/${scEnc}/`);
50
- const merged = deepMerge(current, data);
51
- await this.client.put(`/public/api/projects/${pkEnc}/scenarios/${scEnc}/`, merged);
195
+ const preview = scenarioUpdatePreview(current, data);
196
+ await this.client.put(`/public/api/projects/${pkEnc}/scenarios/${scEnc}/`, preview.next);
197
+ const after = await this.client.get(`/public/api/projects/${pkEnc}/scenarios/${scEnc}/`);
198
+ const mismatches = scenarioFieldMismatches(after, preview.next, preview.normalizedData);
199
+ if (mismatches.length > 0) {
200
+ throw scenarioUpdateVerificationError(mismatches, preview.normalization);
201
+ }
202
+ const verified = scenarioFieldChanges(current, after, preview.normalizedData);
203
+ return {
204
+ ...preview,
205
+ after,
206
+ changes: verified.changes,
207
+ unchangedPaths: verified.unchangedPaths,
208
+ verified: true,
209
+ mismatches: [],
210
+ };
52
211
  }
53
212
  /** Delete a scenario. */
54
213
  async delete(scenarioId, projectKey) {
@@ -24,8 +24,8 @@ export interface DetectedAgent {
24
24
  }
25
25
  export declare function detectAgents(): DetectedAgent[];
26
26
  /**
27
- * Walk upward from startDir looking for common workspace markers.
28
- * Returns the first directory containing a marker, or startDir if none found.
27
+ * Walk upward from startDir looking for strong project markers.
28
+ * Agent config directories are install targets, not workspace roots.
29
29
  */
30
30
  export declare function findWorkspaceRoot(startDir: string): string;
31
31
  export interface InstallResult {
package/dist/src/skill.js CHANGED
@@ -208,10 +208,10 @@ export function detectAgents() {
208
208
  // ---------------------------------------------------------------------------
209
209
  // Workspace root detection
210
210
  // ---------------------------------------------------------------------------
211
- const WORKSPACE_MARKERS = [".git", ".cursor", ".claude", ".codex", ".pi", ".omp", ".vscode",];
211
+ const WORKSPACE_MARKERS = [".git",];
212
212
  /**
213
- * Walk upward from startDir looking for common workspace markers.
214
- * Returns the first directory containing a marker, or startDir if none found.
213
+ * Walk upward from startDir looking for strong project markers.
214
+ * Agent config directories are install targets, not workspace roots.
215
215
  */
216
216
  export function findWorkspaceRoot(startDir) {
217
217
  let dir = startDir;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataiku-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Dataiku DSS SDK and CLI for programmatic access to DSS REST APIs",
5
5
  "type": "module",
6
6
  "workspaces": [