@finos/legend-application-studio 28.21.3 → 28.21.5

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 (66) hide show
  1. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.d.ts +1 -1
  2. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.d.ts.map +1 -1
  3. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js +3 -3
  4. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js.map +1 -1
  5. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.d.ts +3 -0
  6. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.d.ts.map +1 -1
  7. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js +72 -52
  8. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js.map +1 -1
  9. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts.map +1 -1
  10. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js +22 -1
  11. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
  12. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.d.ts +23 -0
  13. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.d.ts.map +1 -0
  14. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js +267 -0
  15. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js.map +1 -0
  16. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.d.ts.map +1 -1
  17. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.js +113 -75
  18. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.js.map +1 -1
  19. package/lib/components/editor/editor-group/testable/TestableSharedComponents.d.ts.map +1 -1
  20. package/lib/components/editor/editor-group/testable/TestableSharedComponents.js +39 -5
  21. package/lib/components/editor/editor-group/testable/TestableSharedComponents.js.map +1 -1
  22. package/lib/components/editor/side-bar/DevMetadataPanel.d.ts.map +1 -1
  23. package/lib/components/editor/side-bar/DevMetadataPanel.js +37 -6
  24. package/lib/components/editor/side-bar/DevMetadataPanel.js.map +1 -1
  25. package/lib/index.css +2 -2
  26. package/lib/index.css.map +1 -1
  27. package/lib/package.json +1 -1
  28. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts +17 -2
  29. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts.map +1 -1
  30. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js +56 -49
  31. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js.map +1 -1
  32. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts +4 -1
  33. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts.map +1 -1
  34. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js +4 -0
  35. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js.map +1 -1
  36. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts +113 -0
  37. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts.map +1 -0
  38. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js +647 -0
  39. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js.map +1 -0
  40. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts +18 -4
  41. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts.map +1 -1
  42. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js +214 -53
  43. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js.map +1 -1
  44. package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.d.ts +17 -1
  45. package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.d.ts.map +1 -1
  46. package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.js +46 -1
  47. package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.js.map +1 -1
  48. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts +9 -0
  49. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts.map +1 -1
  50. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js +55 -0
  51. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js.map +1 -1
  52. package/package.json +16 -16
  53. package/src/components/editor/editor-group/data-editor/EmbeddedDataEditor.tsx +3 -0
  54. package/src/components/editor/editor-group/data-editor/RelationElementsDataEditor.tsx +331 -231
  55. package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +32 -0
  56. package/src/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.tsx +935 -0
  57. package/src/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.tsx +425 -308
  58. package/src/components/editor/editor-group/testable/TestableSharedComponents.tsx +160 -15
  59. package/src/components/editor/side-bar/DevMetadataPanel.tsx +194 -10
  60. package/src/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.ts +82 -51
  61. package/src/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.ts +4 -0
  62. package/src/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.ts +927 -0
  63. package/src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts +303 -72
  64. package/src/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.ts +66 -0
  65. package/src/stores/editor/sidebar-state/dev-metadata/DevMetadataState.ts +76 -0
  66. package/tsconfig.json +2 -0
@@ -0,0 +1,647 @@
1
+ /**
2
+ * Copyright (c) 2020-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { DataProduct, FunctionAccessPoint, LakehouseAccessPoint, DataProductTestSuite, BaseDataResolver, ReferenceDataResolver, DataProductAccessPointTest, RelationElementsData, RelationElement, PackageableElementExplicitReference, TestExecuted, TestError, TestExecutionStatus, EqualToRelation, RelationRowTestData, observe_RelationElement, observe_RelationRowTestData, observe_RelationElementsData, observe_DataProductTestSuite, IngestDefinition, getAccessorItemLabelForElement, } from '@finos/legend-graph';
17
+ import { ActionState, assertErrorThrown, deleteEntry, addUniqueEntry, uuid, noop, } from '@finos/legend-shared';
18
+ import { action, flow, flowResult, makeObservable, observable, runInAction, } from 'mobx';
19
+ import { RelationElementsDataState, RelationElementState, } from '../../data/EmbeddedDataState.js';
20
+ import { TESTABLE_RESULT } from '../../../../sidebar-state/testable/GlobalTestRunnerState.js';
21
+ import { testSuite_addTest } from '../../../../../graph-modifier/Testable_GraphModifierHelper.js';
22
+ import { TestableTestEditorState, TestableTestSuiteEditorState, } from '../../testable/TestableEditorState.js';
23
+ const createEmptyRelationElement = (itemId, columns = []) => {
24
+ const row = observe_RelationRowTestData(new RelationRowTestData());
25
+ row.values = columns.map(() => '');
26
+ const relationElement = new RelationElement();
27
+ relationElement.paths = [itemId];
28
+ relationElement.columns = columns;
29
+ relationElement.rows = [row];
30
+ return observe_RelationElement(relationElement);
31
+ };
32
+ /**
33
+ * Returns the lambda for an access point (handles both LakehouseAccessPoint
34
+ * and FunctionAccessPoint).
35
+ */
36
+ const getAccessPointLambda = (accessPoint) => accessPoint instanceof LakehouseAccessPoint
37
+ ? accessPoint.func
38
+ : accessPoint instanceof FunctionAccessPoint
39
+ ? accessPoint.query
40
+ : undefined;
41
+ /**
42
+ * Builds a RelationElementsData from resolved accessors using the accessor
43
+ * relation type as the source of truth for column definitions.
44
+ */
45
+ const buildRelationElementsDataWithColumns = (accs) => {
46
+ const relData = new RelationElementsData();
47
+ relData.relationElements = accs.map((acc) => {
48
+ const itemId = acc.accessor || 'UNKNOWN';
49
+ const columns = acc.relationType.columns.map((column) => column.name);
50
+ return createEmptyRelationElement(itemId, columns);
51
+ });
52
+ return relData;
53
+ };
54
+ const isIngestOrDataProductAccessor = (accessor) => accessor.parentElement instanceof DataProduct ||
55
+ accessor.parentElement instanceof IngestDefinition;
56
+ const getAccessPointDisplayLabel = (accessPoint) => accessPoint.id;
57
+ const getElementDataItems = (element, graphManager) => {
58
+ if (element instanceof DataProduct) {
59
+ return element.accessPointGroups
60
+ .flatMap((g) => g.accessPoints)
61
+ .map((ap) => ({
62
+ id: ap.id,
63
+ label: getAccessPointDisplayLabel(ap),
64
+ }));
65
+ }
66
+ if (element instanceof IngestDefinition) {
67
+ return graphManager
68
+ .getIngestDefinitionDatasetNames(element)
69
+ .map((name) => ({
70
+ id: name,
71
+ label: name,
72
+ }));
73
+ }
74
+ return [];
75
+ };
76
+ const inferDataProductItemColumns = async (editorStore, dataProduct, itemId) => {
77
+ const accessPoint = dataProduct.accessPointGroups
78
+ .flatMap((group) => group.accessPoints)
79
+ .find((ap) => ap.id === itemId);
80
+ if (!accessPoint) {
81
+ return undefined;
82
+ }
83
+ const lambda = getAccessPointLambda(accessPoint);
84
+ if (!lambda) {
85
+ return undefined;
86
+ }
87
+ const relationMetadata = await editorStore.graphManagerState.graphManager.getLambdaRelationType(lambda, editorStore.graphManagerState.graph);
88
+ return relationMetadata.columns.map((column) => column.name);
89
+ };
90
+ // ─── Per-test state ──────────────────────────────────────────────────────────
91
+ export class DataProductTestState extends TestableTestEditorState {
92
+ suiteState;
93
+ test;
94
+ uuid = uuid();
95
+ /** Wraps assertion.expected — drives both column definitions and test data rows. */
96
+ testDataRelationState;
97
+ constructor(suiteState, test) {
98
+ super(suiteState.testableState.dataProduct, test, suiteState.testableState.dataProductEditorState.isReadOnly, suiteState.editorStore);
99
+ makeObservable(this, {
100
+ // observable fields from base class
101
+ selectedAsertionState: observable,
102
+ selectedTab: observable,
103
+ assertionToRename: observable,
104
+ assertionEditorStates: observable,
105
+ testResultState: observable,
106
+ runningTestAction: observable,
107
+ // own observable
108
+ testDataRelationState: observable,
109
+ // actions from base class
110
+ setSelectedTab: action,
111
+ setAssertionToRename: action,
112
+ addAssertion: action,
113
+ deleteAssertion: action,
114
+ openAssertion: action,
115
+ resetResult: action,
116
+ handleTestResult: action,
117
+ // flow from base class
118
+ runTest: flow,
119
+ });
120
+ this.suiteState = suiteState;
121
+ this.test = test;
122
+ this.buildTestDataRelationState().catch(noop());
123
+ }
124
+ async buildTestDataRelationState() {
125
+ const assertion = this.test.assertions.find((a) => a instanceof EqualToRelation);
126
+ if (!assertion) {
127
+ return;
128
+ }
129
+ // Populate columns from the engine if not yet set
130
+ if (assertion.expected.columns.length === 0) {
131
+ try {
132
+ const engineColumns = await inferDataProductItemColumns(this.editorStore, this.suiteState.testableState.dataProduct, this.test.accessPointId);
133
+ if (engineColumns && engineColumns.length > 0) {
134
+ runInAction(() => {
135
+ assertion.expected.columns = engineColumns;
136
+ });
137
+ }
138
+ }
139
+ catch {
140
+ // best-effort; continue with empty columns
141
+ }
142
+ }
143
+ runInAction(() => {
144
+ this.testDataRelationState = new RelationElementState(assertion.expected);
145
+ });
146
+ }
147
+ get accessPointLabel() {
148
+ return this.suiteState.testableState.getOwnAccessPointLabel(this.test.accessPointId);
149
+ }
150
+ }
151
+ // ─── Per-element test data state ─────────────────────────────────────────────
152
+ export class DataProductElementTestDataState {
153
+ testDataState;
154
+ testData;
155
+ editorStore;
156
+ relationElementsDataState;
157
+ constructor(testDataState, testData) {
158
+ this.testDataState = testDataState;
159
+ this.testData = testData;
160
+ this.editorStore = testDataState.editorStore;
161
+ if (testData.data instanceof RelationElementsData) {
162
+ this.relationElementsDataState = new RelationElementsDataState(this.editorStore, testData.data);
163
+ this.initAccessorOptions();
164
+ }
165
+ }
166
+ get element() {
167
+ return this.testData.element.value;
168
+ }
169
+ get elementName() {
170
+ return this.testData.element.value.name;
171
+ }
172
+ get itemLabel() {
173
+ return getAccessorItemLabelForElement(this.element);
174
+ }
175
+ initAccessorOptions() {
176
+ const dataState = this.relationElementsDataState;
177
+ if (!dataState) {
178
+ return;
179
+ }
180
+ this.refreshAccessorOptions(dataState).catch(noop);
181
+ dataState.setRefreshAccessorOptions(() => this.refreshAccessorOptions(dataState));
182
+ }
183
+ async refreshAccessorOptions(dataState) {
184
+ const element = this.element;
185
+ const graphManager = this.editorStore.graphManagerState.graphManager;
186
+ const graph = this.editorStore.graphManagerState.graph;
187
+ const items = getElementDataItems(element, graphManager);
188
+ if (items.length === 0) {
189
+ dataState.setAccessorOptions(undefined, undefined);
190
+ return;
191
+ }
192
+ const typeLabel = this.itemLabel;
193
+ const options = await Promise.all(items.map(async (item) => {
194
+ let columns = [];
195
+ try {
196
+ if (element instanceof IngestDefinition) {
197
+ const accessor = graphManager.createAccessorFromPackageableElement(element, graph, { schemaName: undefined, tableName: item.id });
198
+ if (accessor) {
199
+ columns = accessor.relationType.columns.map((c) => c.name);
200
+ }
201
+ }
202
+ else if (element instanceof DataProduct) {
203
+ const accessor = await graphManager.buildDataProductAccessor(element, graph, { tableName: item.id });
204
+ if (accessor) {
205
+ columns = accessor.relationType.columns.map((c) => c.name);
206
+ }
207
+ }
208
+ }
209
+ catch {
210
+ // best-effort column resolution
211
+ }
212
+ return {
213
+ label: item.label,
214
+ value: item.id,
215
+ columns,
216
+ };
217
+ }));
218
+ runInAction(() => {
219
+ dataState.setAccessorOptions(options, typeLabel);
220
+ // Back-fill columns on existing relation elements that have none
221
+ const columnsByItem = new Map(options
222
+ .filter((o) => o.columns.length > 0)
223
+ .map((o) => [o.value, o.columns]));
224
+ for (const relState of dataState.relationElementStates) {
225
+ const rel = relState.relationElement;
226
+ if (rel.columns.length === 0) {
227
+ const key = rel.paths[rel.paths.length - 1];
228
+ const cols = key ? columnsByItem.get(key) : undefined;
229
+ if (cols) {
230
+ rel.columns = cols;
231
+ }
232
+ }
233
+ }
234
+ });
235
+ }
236
+ }
237
+ // ─── Test data state for a suite ─────────────────────────────────────────────
238
+ export class DataProductTestDataState {
239
+ editorStore;
240
+ suiteState;
241
+ elementTestDataStates = [];
242
+ selectedElementTestDataState;
243
+ showAddElementModal = false;
244
+ constructor(suiteState, options) {
245
+ makeObservable(this, {
246
+ elementTestDataStates: observable,
247
+ selectedElementTestDataState: observable,
248
+ showAddElementModal: observable,
249
+ setSelectedElementTestDataState: action,
250
+ setShowAddElementModal: action,
251
+ addElement: action,
252
+ deleteElement: action,
253
+ refreshElementTestDataStates: action,
254
+ });
255
+ this.editorStore = suiteState.editorStore;
256
+ this.suiteState = suiteState;
257
+ this.refreshElementTestDataStates(options);
258
+ }
259
+ get availableElementsToAdd() {
260
+ const suite = this.suiteState.suite;
261
+ const existingPaths = new Set((suite.testData ?? [])
262
+ .filter((td) => td instanceof BaseDataResolver ||
263
+ td instanceof ReferenceDataResolver)
264
+ .map((td) => td.element.value.path));
265
+ const graph = this.editorStore.graphManagerState.graph;
266
+ const currentDpPath = this.suiteState.testableState.dataProduct.path;
267
+ const candidates = [
268
+ ...graph.ingests,
269
+ ...graph.allElements.filter((e) => e instanceof DataProduct && e.path !== currentDpPath),
270
+ ];
271
+ return candidates.filter((e) => !existingPaths.has(e.path));
272
+ }
273
+ setShowAddElementModal(val) {
274
+ this.showAddElementModal = val;
275
+ }
276
+ addElement(path) {
277
+ const element = this.editorStore.graphManagerState.graph.getNullableElement(path);
278
+ if (!element) {
279
+ return;
280
+ }
281
+ const resolver = new BaseDataResolver();
282
+ resolver.element = PackageableElementExplicitReference.create(element);
283
+ const relData = new RelationElementsData();
284
+ relData.relationElements = [];
285
+ observe_RelationElementsData(relData);
286
+ resolver.data = relData;
287
+ const suite = this.suiteState.suite;
288
+ suite.testData = [...(suite.testData ?? []), resolver];
289
+ this.refreshElementTestDataStates({ selectedElementPath: path });
290
+ }
291
+ deleteElement(elementState) {
292
+ const suite = this.suiteState.suite;
293
+ if (suite.testData) {
294
+ const idx = suite.testData.indexOf(elementState.testData);
295
+ suite.testData.splice(idx, 1);
296
+ }
297
+ this.refreshElementTestDataStates();
298
+ }
299
+ refreshElementTestDataStates(options) {
300
+ const previouslySelectedElementPath = options?.selectedElementPath ??
301
+ this.selectedElementTestDataState?.element.path;
302
+ const suite = this.suiteState.suite;
303
+ this.elementTestDataStates = (suite.testData ?? [])
304
+ .filter((td) => td instanceof BaseDataResolver)
305
+ .map((td) => new DataProductElementTestDataState(this, td));
306
+ this.selectedElementTestDataState =
307
+ this.elementTestDataStates.find((state) => state.element.path === previouslySelectedElementPath) ?? this.elementTestDataStates[0];
308
+ }
309
+ setSelectedElementTestDataState(val) {
310
+ this.selectedElementTestDataState = val;
311
+ }
312
+ }
313
+ // ─── Per-suite state ─────────────────────────────────────────────────────────
314
+ export class DataProductTestSuiteState extends TestableTestSuiteEditorState {
315
+ testableState;
316
+ suite;
317
+ testStates = [];
318
+ testDataState;
319
+ constructor(editorStore, testableState, suite) {
320
+ super(testableState.dataProduct, suite, testableState.dataProductEditorState.isReadOnly, editorStore);
321
+ makeObservable(this, {
322
+ testStates: observable,
323
+ selectTestState: observable,
324
+ testDataState: observable,
325
+ addNewTest: flow,
326
+ deleteTest: action,
327
+ runSuite: flow,
328
+ runFailingTests: flow,
329
+ buildTestStates: action,
330
+ });
331
+ this.testableState = testableState;
332
+ this.suite = suite;
333
+ this.testDataState = new DataProductTestDataState(this);
334
+ this.buildTestStates();
335
+ }
336
+ refreshTestDataState() {
337
+ this.testDataState = new DataProductTestDataState(this, {
338
+ selectedElementPath: this.testDataState.selectedElementTestDataState?.element.path,
339
+ });
340
+ }
341
+ buildTestStates() {
342
+ this.testStates = this.suite.tests.map((t) => new DataProductTestState(this, t));
343
+ this.selectTestState = this.testStates[0];
344
+ }
345
+ deleteTest(test) {
346
+ super.deleteTest(test);
347
+ }
348
+ *addNewTest(testName, accessPointId) {
349
+ const observerContext = this.editorStore.changeDetectionState.observerContext;
350
+ let inferredColumns = [];
351
+ try {
352
+ const cols = (yield inferDataProductItemColumns(this.editorStore, this.testableState.dataProduct, accessPointId));
353
+ if (cols) {
354
+ inferredColumns = cols;
355
+ }
356
+ }
357
+ catch {
358
+ // best-effort
359
+ }
360
+ const test = new DataProductAccessPointTest();
361
+ test.id = testName;
362
+ test.__parent = this.suite;
363
+ test.accessPointId = accessPointId;
364
+ // Resolve sources through the graph (follows function calls, no self-refs)
365
+ const accessPointForTest = this.testableState.dataProduct.accessPointGroups
366
+ .flatMap((g) => g.accessPoints)
367
+ .find((ap) => ap.id === accessPointId);
368
+ const rawLambdaForTest = accessPointForTest
369
+ ? getAccessPointLambda(accessPointForTest)
370
+ : undefined;
371
+ let resolvedAccessors = [];
372
+ if (rawLambdaForTest) {
373
+ const all = (yield this.editorStore.graphManagerState.graphManager.collectAccessorsInRawLambda(rawLambdaForTest, this.editorStore.graphManagerState.graph));
374
+ resolvedAccessors = all.filter((accessor) => isIngestOrDataProductAccessor(accessor) &&
375
+ accessor.parentElement.path !== this.testableState.dataProduct.path);
376
+ }
377
+ if (resolvedAccessors.length > 0) {
378
+ // Group by element, merge into existing resolvers or create new ones
379
+ const byElement = new Map();
380
+ for (const acc of resolvedAccessors) {
381
+ const grp = byElement.get(acc.parentElement.path) ?? [];
382
+ grp.push(acc);
383
+ byElement.set(acc.parentElement.path, grp);
384
+ }
385
+ for (const [elementPath, accs] of byElement) {
386
+ const element = this.editorStore.graphManagerState.graph.getNullableElement(elementPath);
387
+ if (!element) {
388
+ continue;
389
+ }
390
+ const existingResolver = this.suite.testData?.find((td) => td instanceof BaseDataResolver && td.element.value === element);
391
+ const relData = existingResolver?.data instanceof RelationElementsData
392
+ ? existingResolver.data
393
+ : undefined;
394
+ if (!existingResolver || !relData) {
395
+ const resolver = new BaseDataResolver();
396
+ resolver.element =
397
+ PackageableElementExplicitReference.create(element);
398
+ const newRelData = buildRelationElementsDataWithColumns(accs);
399
+ observe_RelationElementsData(newRelData);
400
+ resolver.data = newRelData;
401
+ this.suite.testData = [...(this.suite.testData ?? []), resolver];
402
+ }
403
+ else {
404
+ for (const acc of accs) {
405
+ const itemId = acc.accessor;
406
+ if (!relData.relationElements.find((re) => re.paths[0] === itemId)) {
407
+ const columns = acc.relationType.columns.map((column) => column.name);
408
+ relData.relationElements.push(createEmptyRelationElement(itemId, columns));
409
+ }
410
+ }
411
+ }
412
+ }
413
+ }
414
+ else {
415
+ this.editorStore.applicationStore.notificationService.notifyWarning('Access Point accessors cannot be resolved');
416
+ }
417
+ const assertion = new EqualToRelation();
418
+ assertion.id = 'assert_1';
419
+ const expectedRelElement = new RelationElement();
420
+ const expectedRow = observe_RelationRowTestData(new RelationRowTestData());
421
+ expectedRow.values = inferredColumns.map(() => '');
422
+ expectedRelElement.paths = [accessPointId];
423
+ expectedRelElement.columns = inferredColumns;
424
+ expectedRelElement.rows = [expectedRow];
425
+ observe_RelationElement(expectedRelElement);
426
+ assertion.expected = expectedRelElement;
427
+ test.assertions = [assertion];
428
+ testSuite_addTest(this.suite, test, observerContext);
429
+ this.refreshTestDataState();
430
+ const testState = new DataProductTestState(this, test);
431
+ this.testStates.push(testState);
432
+ this.selectTestState = testState;
433
+ return undefined;
434
+ }
435
+ get result() {
436
+ if (this.runningSuiteState.isInProgress) {
437
+ return TESTABLE_RESULT.IN_PROGRESS;
438
+ }
439
+ if (this.testStates.length === 0) {
440
+ return TESTABLE_RESULT.DID_NOT_RUN;
441
+ }
442
+ if (this.testStates.every((ts) => ts.testResultState.result === undefined)) {
443
+ return TESTABLE_RESULT.DID_NOT_RUN;
444
+ }
445
+ let hasFailure = false;
446
+ let hasError = false;
447
+ for (const testState of this.testStates) {
448
+ const result = testState.testResultState.result;
449
+ if (result instanceof TestError) {
450
+ hasError = true;
451
+ }
452
+ else if (result instanceof TestExecuted &&
453
+ result.testExecutionStatus === TestExecutionStatus.FAIL) {
454
+ hasFailure = true;
455
+ }
456
+ }
457
+ if (hasError) {
458
+ return TESTABLE_RESULT.ERROR;
459
+ }
460
+ if (hasFailure) {
461
+ return TESTABLE_RESULT.FAILED;
462
+ }
463
+ return TESTABLE_RESULT.PASSED;
464
+ }
465
+ }
466
+ // ─── Top-level testable state ────────────────────────────────────────────────
467
+ export class DataProductTestableState {
468
+ editorStore;
469
+ dataProductEditorState;
470
+ suiteStates = [];
471
+ selectedSuiteState;
472
+ runningAllTestsState = ActionState.create();
473
+ showCreateSuiteModal = false;
474
+ showCreateTestModal = false;
475
+ suiteToRename;
476
+ constructor(dataProductEditorState) {
477
+ makeObservable(this, {
478
+ suiteStates: observable,
479
+ selectedSuiteState: observable,
480
+ showCreateSuiteModal: observable,
481
+ showCreateTestModal: observable,
482
+ suiteToRename: observable,
483
+ setSelectedSuiteState: action,
484
+ setShowCreateSuiteModal: action,
485
+ setShowCreateTestModal: action,
486
+ setSuiteToRename: action,
487
+ changeSuite: action,
488
+ createSuite: flow,
489
+ deleteSuite: action,
490
+ init: action,
491
+ runAllTests: flow,
492
+ });
493
+ this.editorStore = dataProductEditorState.editorStore;
494
+ this.dataProductEditorState = dataProductEditorState;
495
+ }
496
+ get dataProduct() {
497
+ return this.dataProductEditorState.product;
498
+ }
499
+ setSelectedSuiteState(val) {
500
+ this.selectedSuiteState = val;
501
+ }
502
+ setShowCreateSuiteModal(val) {
503
+ this.showCreateSuiteModal = val;
504
+ }
505
+ setShowCreateTestModal(val) {
506
+ this.showCreateTestModal = val;
507
+ }
508
+ setSuiteToRename(val) {
509
+ this.suiteToRename = val;
510
+ }
511
+ changeSuite(suite) {
512
+ const suiteState = this.suiteStates.find((s) => s.suite === suite);
513
+ if (suiteState) {
514
+ this.selectedSuiteState = suiteState;
515
+ }
516
+ }
517
+ /**
518
+ * Build suite states from the DataProduct.tests array.
519
+ * Call this on init and after grammar→form roundtrip.
520
+ */
521
+ init() {
522
+ const dp = this.dataProduct;
523
+ this.suiteStates = dp.tests.map((s) => new DataProductTestSuiteState(this.editorStore, this, s));
524
+ this.selectedSuiteState = this.suiteStates[0];
525
+ }
526
+ /** Returns all graph ingest elements available in the model. */
527
+ get availableIngestSources() {
528
+ const graph = this.editorStore.graphManagerState.graph;
529
+ return graph.ingests;
530
+ }
531
+ /** Access points on the DataProduct being edited (used for test's accessPointId). */
532
+ get ownAccessPoints() {
533
+ return this.dataProduct.accessPointGroups.flatMap((g) => g.accessPoints);
534
+ }
535
+ getOwnAccessPointLabel(accessPointId) {
536
+ const accessPoint = this.ownAccessPoints.find((candidate) => candidate.id === accessPointId);
537
+ return accessPoint
538
+ ? getAccessPointDisplayLabel(accessPoint)
539
+ : accessPointId;
540
+ }
541
+ /**
542
+ * Create a new test suite with one initial test on the DataProduct.
543
+ * Test data is auto-seeded for the selected access point on the current
544
+ * DataProduct (no element picker required).
545
+ * Columns are inferred via the engine when possible.
546
+ */
547
+ *createSuite(suiteName, testName, accessPointId) {
548
+ const dp = this.dataProduct;
549
+ const observerContext = this.editorStore.changeDetectionState.observerContext;
550
+ const suite = new DataProductTestSuite();
551
+ suite.id = suiteName;
552
+ // Try to infer columns from the access-point lambda
553
+ let inferredColumns = [];
554
+ try {
555
+ const cols = (yield inferDataProductItemColumns(this.editorStore, dp, accessPointId));
556
+ if (cols) {
557
+ inferredColumns = cols;
558
+ }
559
+ }
560
+ catch {
561
+ // Column inference is best-effort; continue with empty columns
562
+ }
563
+ // Resolve INPUT sources via the graph (follows function calls, avoids self-refs)
564
+ const accessPointForSuite = dp.accessPointGroups
565
+ .flatMap((g) => g.accessPoints)
566
+ .find((ap) => ap.id === accessPointId);
567
+ const rawLambdaForSuite = accessPointForSuite
568
+ ? getAccessPointLambda(accessPointForSuite)
569
+ : undefined;
570
+ suite.testData = [];
571
+ if (rawLambdaForSuite) {
572
+ const all = (yield this.editorStore.graphManagerState.graphManager.collectAccessorsInRawLambda(rawLambdaForSuite, this.editorStore.graphManagerState.graph));
573
+ const externalAccessors = all.filter((accessor) => isIngestOrDataProductAccessor(accessor) &&
574
+ accessor.parentElement.path !== dp.path);
575
+ const byElement = new Map();
576
+ for (const acc of externalAccessors) {
577
+ const grp = byElement.get(acc.parentElement.path) ?? [];
578
+ grp.push(acc);
579
+ byElement.set(acc.parentElement.path, grp);
580
+ }
581
+ for (const [elementPath, accs] of byElement) {
582
+ const element = this.editorStore.graphManagerState.graph.getNullableElement(elementPath);
583
+ if (!element) {
584
+ continue;
585
+ }
586
+ const resolver = new BaseDataResolver();
587
+ resolver.element = PackageableElementExplicitReference.create(element);
588
+ const relData = buildRelationElementsDataWithColumns(accs);
589
+ observe_RelationElementsData(relData);
590
+ resolver.data = relData;
591
+ suite.testData.push(resolver);
592
+ }
593
+ }
594
+ // If no external sources were resolved, notify the user and leave test data empty
595
+ if (suite.testData.length === 0) {
596
+ this.editorStore.applicationStore.notificationService.notifyWarning('Access Point accessors cannot be resolved');
597
+ }
598
+ // Create one initial test with EqualToRelation assertion
599
+ const test = new DataProductAccessPointTest();
600
+ test.id = testName;
601
+ test.__parent = suite;
602
+ test.accessPointId = accessPointId;
603
+ const assertion = new EqualToRelation();
604
+ assertion.id = 'assert_1';
605
+ const expectedRelElement = new RelationElement();
606
+ const expectedRow = observe_RelationRowTestData(new RelationRowTestData());
607
+ expectedRow.values = inferredColumns.map(() => '');
608
+ expectedRelElement.paths = [accessPointId];
609
+ expectedRelElement.columns = inferredColumns;
610
+ expectedRelElement.rows = [expectedRow];
611
+ observe_RelationElement(expectedRelElement);
612
+ assertion.expected = expectedRelElement;
613
+ test.assertions = [assertion];
614
+ suite.tests = [test];
615
+ const observed = observe_DataProductTestSuite(suite, observerContext);
616
+ addUniqueEntry(dp.tests, observed);
617
+ const suiteState = new DataProductTestSuiteState(this.editorStore, this, observed);
618
+ this.suiteStates.push(suiteState);
619
+ this.selectedSuiteState = suiteState;
620
+ return undefined;
621
+ }
622
+ deleteSuite(suiteState) {
623
+ const dp = this.dataProduct;
624
+ deleteEntry(dp.tests, suiteState.suite);
625
+ deleteEntry(this.suiteStates, suiteState);
626
+ if (this.selectedSuiteState === suiteState) {
627
+ this.selectedSuiteState = this.suiteStates[0];
628
+ }
629
+ }
630
+ *runAllTests() {
631
+ try {
632
+ this.runningAllTestsState.inProgress();
633
+ for (const suiteState of this.suiteStates) {
634
+ if (suiteState.suite.tests.length > 0) {
635
+ yield flowResult(suiteState.runSuite());
636
+ }
637
+ }
638
+ this.runningAllTestsState.complete();
639
+ }
640
+ catch (error) {
641
+ assertErrorThrown(error);
642
+ this.editorStore.applicationStore.notificationService.notifyError(error);
643
+ this.runningAllTestsState.fail();
644
+ }
645
+ }
646
+ }
647
+ //# sourceMappingURL=DataProductTestableState.js.map