@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.
- package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.d.ts +1 -1
- package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js +3 -3
- package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js.map +1 -1
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.d.ts +3 -0
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js +72 -52
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js.map +1 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js +22 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
- package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.d.ts +23 -0
- package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.d.ts.map +1 -0
- package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js +267 -0
- package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js.map +1 -0
- package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.js +113 -75
- package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.js.map +1 -1
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.d.ts.map +1 -1
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.js +39 -5
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.js.map +1 -1
- package/lib/components/editor/side-bar/DevMetadataPanel.d.ts.map +1 -1
- package/lib/components/editor/side-bar/DevMetadataPanel.js +37 -6
- package/lib/components/editor/side-bar/DevMetadataPanel.js.map +1 -1
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/package.json +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts +17 -2
- package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js +56 -49
- package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts +4 -1
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.d.ts.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js +4 -0
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts +113 -0
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts.map +1 -0
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js +647 -0
- package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js.map +1 -0
- package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts +18 -4
- package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js +214 -53
- package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.d.ts +17 -1
- package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.d.ts.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.js +46 -1
- package/lib/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.js.map +1 -1
- package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts +9 -0
- package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts.map +1 -1
- package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js +55 -0
- package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js.map +1 -1
- package/package.json +16 -16
- package/src/components/editor/editor-group/data-editor/EmbeddedDataEditor.tsx +3 -0
- package/src/components/editor/editor-group/data-editor/RelationElementsDataEditor.tsx +331 -231
- package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +32 -0
- package/src/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.tsx +935 -0
- package/src/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.tsx +425 -308
- package/src/components/editor/editor-group/testable/TestableSharedComponents.tsx +160 -15
- package/src/components/editor/side-bar/DevMetadataPanel.tsx +194 -10
- package/src/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.ts +82 -51
- package/src/stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.ts +4 -0
- package/src/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.ts +927 -0
- package/src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts +303 -72
- package/src/stores/editor/editor-state/element-editor-state/testable/TestAssertionState.ts +66 -0
- package/src/stores/editor/sidebar-state/dev-metadata/DevMetadataState.ts +76 -0
- 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
|