@finos/legend-application-studio 28.21.4 → 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 (54) 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 +12 -35
  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 +19 -6
  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.map +1 -1
  13. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js +59 -22
  14. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js.map +1 -1
  15. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.d.ts.map +1 -1
  16. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.js +113 -75
  17. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.js.map +1 -1
  18. package/lib/components/editor/editor-group/testable/TestableSharedComponents.d.ts.map +1 -1
  19. package/lib/components/editor/editor-group/testable/TestableSharedComponents.js +1 -1
  20. package/lib/components/editor/editor-group/testable/TestableSharedComponents.js.map +1 -1
  21. package/lib/components/editor/side-bar/DevMetadataPanel.d.ts.map +1 -1
  22. package/lib/components/editor/side-bar/DevMetadataPanel.js +37 -6
  23. package/lib/components/editor/side-bar/DevMetadataPanel.js.map +1 -1
  24. package/lib/index.css +2 -2
  25. package/lib/index.css.map +1 -1
  26. package/lib/package.json +1 -1
  27. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts +1 -1
  28. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts.map +1 -1
  29. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js +20 -48
  30. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js.map +1 -1
  31. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts +9 -14
  32. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts.map +1 -1
  33. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js +125 -78
  34. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js.map +1 -1
  35. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts +18 -4
  36. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts.map +1 -1
  37. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js +214 -53
  38. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js.map +1 -1
  39. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts +9 -0
  40. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts.map +1 -1
  41. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js +55 -0
  42. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js.map +1 -1
  43. package/package.json +16 -16
  44. package/src/components/editor/editor-group/data-editor/EmbeddedDataEditor.tsx +3 -0
  45. package/src/components/editor/editor-group/data-editor/RelationElementsDataEditor.tsx +200 -186
  46. package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +25 -7
  47. package/src/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.tsx +149 -86
  48. package/src/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.tsx +425 -308
  49. package/src/components/editor/editor-group/testable/TestableSharedComponents.tsx +2 -11
  50. package/src/components/editor/side-bar/DevMetadataPanel.tsx +194 -10
  51. package/src/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.ts +28 -50
  52. package/src/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.ts +164 -100
  53. package/src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts +303 -72
  54. package/src/stores/editor/sidebar-state/dev-metadata/DevMetadataState.ts +76 -0
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { action, flow, makeObservable, observable } from 'mobx';
17
+ import { action, flow, flowResult, makeObservable, observable } from 'mobx';
18
18
  import type { EditorStore } from '../../../../EditorStore.js';
19
19
  import type { FunctionEditorState } from '../../FunctionEditorState.js';
20
20
  import {
@@ -31,6 +31,8 @@ import {
31
31
  filterByType,
32
32
  deleteEntry,
33
33
  UnsupportedOperationError,
34
+ noop,
35
+ type GeneratorFn,
34
36
  } from '@finos/legend-shared';
35
37
  import {
36
38
  type ConcreteFunctionDefinition,
@@ -40,6 +42,7 @@ import {
40
42
  type ObserverContext,
41
43
  type ValueSpecification,
42
44
  type TestAssertion,
45
+ type AccessorOwner,
43
46
  FunctionParameterValue,
44
47
  VariableExpression,
45
48
  FunctionTest,
@@ -58,9 +61,17 @@ import {
58
61
  observe_ValueSpecification,
59
62
  buildLambdaVariableExpressions,
60
63
  EqualTo,
64
+ EqualToRelation,
65
+ RelationElement,
66
+ observe_RelationElement,
61
67
  ModelStore,
62
68
  RelationElementsData,
63
69
  CORE_PURE_PATH,
70
+ type Accessor,
71
+ DataProductAccessor,
72
+ IngestionAccessor,
73
+ RelationalStoreAccessor,
74
+ V1_buildRelationElementsDataFromAccessors,
64
75
  } from '@finos/legend-graph';
65
76
  import {
66
77
  TestablePackageableElementEditorState,
@@ -68,6 +79,7 @@ import {
68
79
  TestableTestSuiteEditorState,
69
80
  } from '../../testable/TestableEditorState.js';
70
81
  import { EmbeddedDataEditorState } from '../../data/DataEditorState.js';
82
+ import { RelationElementsDataState } from '../../data/EmbeddedDataState.js';
71
83
  import {
72
84
  functionTestable_deleteDataStore,
73
85
  functionTestable_setEmbeddedData,
@@ -207,6 +219,69 @@ export class FunctionStoreTestDataState {
207
219
  hideSource: true,
208
220
  },
209
221
  );
222
+ this.initAccessorOptions();
223
+ }
224
+
225
+ private initAccessorOptions(): void {
226
+ const embeddedDataState = this.embeddedEditorState.embeddedDataState;
227
+ if (!(embeddedDataState instanceof RelationElementsDataState)) {
228
+ return;
229
+ }
230
+ this.refreshAccessorOptions(embeddedDataState).catch(noop());
231
+ embeddedDataState.setRefreshAccessorOptions(() =>
232
+ this.refreshAccessorOptions(embeddedDataState),
233
+ );
234
+ }
235
+
236
+ private async refreshAccessorOptions(
237
+ embeddedDataState: RelationElementsDataState,
238
+ ): Promise<void> {
239
+ const parentElement = this.storeTestData.element.value;
240
+ const accessors =
241
+ this.testDataState.functionTestableState
242
+ .resolvedIngestOrDataProductAccessors;
243
+ const parentAccessors = accessors.filter(
244
+ (a) => a.accessorOwner === parentElement.path,
245
+ );
246
+ if (!parentAccessors.length) {
247
+ embeddedDataState.setAccessorOptions(undefined, undefined);
248
+ return;
249
+ }
250
+ const firstAccessor = parentAccessors[0];
251
+ if (!firstAccessor) {
252
+ embeddedDataState.setAccessorOptions(undefined, undefined);
253
+ return;
254
+ }
255
+
256
+ const columnOverrides = new Map<string, string[]>();
257
+ for (const accessor of parentAccessors) {
258
+ const cols = accessor.relationType.columns.map((col) => col.name);
259
+ if (cols.length > 0) {
260
+ columnOverrides.set(accessor.accessor, cols);
261
+ }
262
+ }
263
+
264
+ const typeLabel = firstAccessor.accessorLabel;
265
+ const options = parentAccessors.map((a) => ({
266
+ label: a.accessor,
267
+ value: a.accessor,
268
+ columns: columnOverrides.get(a.accessor) ?? [],
269
+ }));
270
+
271
+ if (columnOverrides.size > 0) {
272
+ for (const relState of embeddedDataState.relationElementStates) {
273
+ const rel = relState.relationElement;
274
+ if (rel.columns.length === 0) {
275
+ const key = rel.paths[rel.paths.length - 1];
276
+ const cols = key ? columnOverrides.get(key) : undefined;
277
+ if (cols) {
278
+ rel.columns = cols;
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ embeddedDataState.setAccessorOptions(options, typeLabel);
210
285
  }
211
286
 
212
287
  setDataElementModal(val: boolean): void {
@@ -532,6 +607,7 @@ class FunctionTestDataState {
532
607
  selectedDataState: FunctionStoreTestDataState | undefined;
533
608
  dataHolder: FunctionTestSuite;
534
609
  showNewModal = false;
610
+ showAddElementModal = false;
535
611
 
536
612
  constructor(
537
613
  editorStore: EditorStore,
@@ -542,9 +618,12 @@ class FunctionTestDataState {
542
618
  selectedDataState: observable,
543
619
  dataHolder: observable,
544
620
  showNewModal: observable,
621
+ showAddElementModal: observable,
545
622
  initDefaultStore: action,
546
623
  setShowModal: action,
624
+ setShowAddElementModal: action,
547
625
  deleteStoreTestData: action,
626
+ addDataElement: action,
548
627
  openStoreTestData: action,
549
628
  });
550
629
  this.editorStore = editorStore;
@@ -566,6 +645,49 @@ class FunctionTestDataState {
566
645
  this.showNewModal = val;
567
646
  }
568
647
 
648
+ setShowAddElementModal(val: boolean): void {
649
+ this.showAddElementModal = val;
650
+ }
651
+
652
+ get existingElementPaths(): string[] {
653
+ return (this.dataHolder.testData ?? []).map((td) => td.element.value.path);
654
+ }
655
+
656
+ get availableElementsToAdd(): { path: string }[] {
657
+ const accessors =
658
+ this.functionTestableState.resolvedIngestOrDataProductAccessors;
659
+ const existingPaths = new Set(this.existingElementPaths);
660
+ const parentPaths = new Set<string>();
661
+ for (const accessor of accessors) {
662
+ const parentPath = accessor.path[0];
663
+ if (parentPath && !existingPaths.has(parentPath)) {
664
+ parentPaths.add(parentPath);
665
+ }
666
+ }
667
+ return Array.from(parentPaths).map((path) => ({ path }));
668
+ }
669
+
670
+ addDataElement(elementPath: string): void {
671
+ const accessors =
672
+ this.functionTestableState.resolvedIngestOrDataProductAccessors;
673
+ const group = accessors.filter((a) => a.path[0] === elementPath);
674
+ if (!group.length) {
675
+ return;
676
+ }
677
+ const element =
678
+ this.editorStore.graphManagerState.graph.getElement(elementPath);
679
+ const data = new FunctionTestData();
680
+ data.element = PackageableElementExplicitReference.create(
681
+ element as AccessorOwner,
682
+ );
683
+ data.data = V1_buildRelationElementsDataFromAccessors(group);
684
+ if (!this.dataHolder.testData) {
685
+ this.dataHolder.testData = [];
686
+ }
687
+ this.dataHolder.testData.push(data);
688
+ this.openStoreTestData(data);
689
+ }
690
+
569
691
  deleteStoreTestData(val: FunctionTestData): void {
570
692
  functionTestable_deleteDataStore(this.dataHolder, val);
571
693
  this.initDefaultStore();
@@ -580,26 +702,60 @@ class FunctionTestDataState {
580
702
  }
581
703
  }
582
704
 
583
- export const createFunctionTest = (
705
+ export const createFunctionTest = async (
584
706
  id: string,
585
707
  observerContext: ObserverContext,
586
708
  containsRuntime: boolean,
587
709
  functionDefinition: ConcreteFunctionDefinition,
588
710
  editorStore: EditorStore,
589
711
  suite?: FunctionTestSuite | undefined,
590
- ): FunctionTest => {
712
+ ): Promise<FunctionTest> => {
591
713
  const funcionTest = new FunctionTest();
592
714
  funcionTest.id = id;
593
715
  funcionTest.assertions = [];
594
716
  let _assertion: TestAssertion;
595
- if (containsRuntime) {
717
+ const type = functionDefinition.returnType.value.rawType;
718
+ if (
719
+ type.path === CORE_PURE_PATH.RELATION ||
720
+ type.path === CORE_PURE_PATH.TABULAR_DATASET
721
+ ) {
722
+ const assertion = new EqualToRelation();
723
+ assertion.id = DEFAULT_TEST_ASSERTION_ID;
724
+ const expectedRelElement = new RelationElement();
725
+ expectedRelElement.paths = [];
726
+ expectedRelElement.rows = [];
727
+ let inferredColumns: string[] = [];
728
+ if (type.path === CORE_PURE_PATH.RELATION) {
729
+ try {
730
+ const rawLambda = new RawLambda(
731
+ functionDefinition.parameters.map((_param) =>
732
+ editorStore.graphManagerState.graphManager.serializeRawValueSpecification(
733
+ _param,
734
+ ),
735
+ ),
736
+ functionDefinition.expressionSequence,
737
+ );
738
+ const relationTypeMetadata =
739
+ await editorStore.graphManagerState.graphManager.getLambdaRelationType(
740
+ rawLambda,
741
+ editorStore.graphManagerState.graph,
742
+ );
743
+ inferredColumns = relationTypeMetadata.columns.map((col) => col.name);
744
+ } catch {
745
+ // best-effort: leave columns empty
746
+ }
747
+ }
748
+ expectedRelElement.columns = inferredColumns;
749
+ observe_RelationElement(expectedRelElement);
750
+ assertion.expected = expectedRelElement;
751
+ _assertion = assertion;
752
+ } else if (containsRuntime) {
596
753
  _assertion = createDefaultEqualToJSONTestAssertion(
597
754
  DEFAULT_TEST_ASSERTION_ID,
598
755
  );
599
756
  } else {
600
757
  const equalTo = new EqualTo();
601
758
  equalTo.id = DEFAULT_TEST_ASSERTION_ID;
602
- const type = functionDefinition.returnType.value.rawType;
603
759
  const valSpec = buildDefaultInstanceValue(
604
760
  editorStore.graphManagerState.graph,
605
761
  type,
@@ -652,7 +808,7 @@ export class FunctionTestSuiteState extends TestableTestSuiteEditorState {
652
808
  deleteTest: action,
653
809
  buildTestStates: action,
654
810
  setShowModal: action,
655
- addNewTest: action,
811
+ addNewTest: flow,
656
812
  });
657
813
  this.functionTestableState = functionTestableState;
658
814
  this.suite = suite;
@@ -682,15 +838,15 @@ export class FunctionTestSuiteState extends TestableTestSuiteEditorState {
682
838
  return undefined;
683
839
  }
684
840
 
685
- addNewTest(id: string): void {
686
- const test = createFunctionTest(
841
+ *addNewTest(id: string): GeneratorFn<void> {
842
+ const test = (yield createFunctionTest(
687
843
  id,
688
844
  this.editorStore.changeDetectionState.observerContext,
689
845
  this.functionTestableState.containsRuntime,
690
846
  this.functionTestableState.function,
691
847
  this.editorStore,
692
848
  this.suite,
693
- );
849
+ )) as FunctionTest;
694
850
  testSuite_addTest(
695
851
  this.suite,
696
852
  test,
@@ -711,6 +867,7 @@ export class FunctionTestableState extends TestablePackageableElementEditorState
711
867
  declare runningSuite: FunctionTestSuite | undefined;
712
868
 
713
869
  createSuiteModal = false;
870
+ cachedAccessors: Accessor[] = [];
714
871
 
715
872
  constructor(functionEditorState: FunctionEditorState) {
716
873
  super(functionEditorState, functionEditorState.functionElement);
@@ -722,6 +879,7 @@ export class FunctionTestableState extends TestablePackageableElementEditorState
722
879
  runningSuite: observable,
723
880
  testableComponentToRename: observable,
724
881
  createSuiteModal: observable,
882
+ cachedAccessors: observable,
725
883
  init: action,
726
884
  buildTestSuiteState: action,
727
885
  deleteTestSuite: action,
@@ -730,6 +888,8 @@ export class FunctionTestableState extends TestablePackageableElementEditorState
730
888
  setRenameComponent: action,
731
889
  clearTestResultsForSuite: action,
732
890
  setCreateSuite: action,
891
+ resolveAccessors: flow,
892
+ createSuite: flow,
733
893
  runTestable: flow,
734
894
  runSuite: flow,
735
895
  runAllFailingSuites: flow,
@@ -750,6 +910,36 @@ export class FunctionTestableState extends TestablePackageableElementEditorState
750
910
  return Boolean(this.associatedRuntimes?.length);
751
911
  }
752
912
 
913
+ get resolvedIngestOrDataProductAccessors(): Accessor[] {
914
+ return this.cachedAccessors.filter(
915
+ (a) => a instanceof DataProductAccessor || a instanceof IngestionAccessor,
916
+ );
917
+ }
918
+
919
+ private buildRawLambdaFromFunction(): RawLambda {
920
+ return new RawLambda(
921
+ this.function.parameters.map((_param) =>
922
+ this.editorStore.graphManagerState.graphManager.serializeRawValueSpecification(
923
+ _param,
924
+ ),
925
+ ),
926
+ this.function.expressionSequence,
927
+ );
928
+ }
929
+
930
+ *resolveAccessors(): GeneratorFn<void> {
931
+ try {
932
+ const rawLambda = this.buildRawLambdaFromFunction();
933
+ this.cachedAccessors =
934
+ (yield this.editorStore.graphManagerState.graphManager.collectAccessorsInRawLambda(
935
+ rawLambda,
936
+ this.editorStore.graphManagerState.graph,
937
+ )) as Accessor[];
938
+ } catch {
939
+ this.cachedAccessors = [];
940
+ }
941
+ }
942
+
753
943
  override init(): void {
754
944
  if (!this.selectedTestSuite) {
755
945
  const suite = this.function.tests[0];
@@ -757,84 +947,125 @@ export class FunctionTestableState extends TestablePackageableElementEditorState
757
947
  ? this.buildTestSuiteState(suite)
758
948
  : undefined;
759
949
  }
950
+ flowResult(this.resolveAccessors()).catch(noop);
760
951
  }
761
952
 
762
953
  setCreateSuite(val: boolean): void {
763
954
  this.createSuiteModal = val;
764
955
  }
765
956
 
766
- createSuite(suiteName: string, testName: string): void {
957
+ *createSuite(suiteName: string, testName: string): GeneratorFn<void> {
767
958
  const functionSuite = new FunctionTestSuite();
768
959
  functionSuite.id = suiteName;
769
- const engineRuntimes = this.associatedRuntimes;
770
- if (!engineRuntimes?.length) {
771
- const type = this.function.returnType.value.rawType;
772
- if (
773
- type.path === CORE_PURE_PATH.RELATION ||
774
- type.path === CORE_PURE_PATH.TABULAR_DATASET
775
- ) {
776
- this.editorStore.applicationStore.notificationService.notifyWarning(
777
- `Unable to find runtime or function contains accessors incompatible for test suite creation`,
778
- );
779
- return;
780
- }
781
- } else {
782
- try {
783
- assertTrue(
784
- engineRuntimes.length === 1,
785
- `Function Test Suite Only supports One Runtime at this time. Found ${engineRuntimes.length}`,
786
- );
787
- const engineRuntime = guaranteeNonNullable(engineRuntimes[0]);
788
- assertTrue(
789
- !(
790
- engineRuntime.connectionStores.length &&
791
- engineRuntime.connections.length
792
- ),
793
- `Runtime found has two connection types defined. Please use connection stores only`,
794
- );
795
- const stores = [
796
- ...engineRuntime.connections
797
- .map((e) =>
798
- e.storeConnections.map((s) => s.connection.store?.value).flat(),
799
- )
800
- .flat(),
801
- ...engineRuntime.connectionStores
802
- .map((e) => e.storePointers.map((sPt) => sPt.value))
803
- .flat(),
804
- ].filter(isNonNullable);
805
- assertTrue(Boolean(stores.length), 'No runtime store found');
806
- assertTrue(
807
- stores.length === 1,
808
- 'Only one store supported in runtime for function tests',
960
+
961
+ yield flowResult(this.resolveAccessors());
962
+
963
+ const ingestOrDataProductAccessors =
964
+ this.resolvedIngestOrDataProductAccessors;
965
+ const databaseAccessors = this.cachedAccessors.filter(
966
+ (a) => a instanceof RelationalStoreAccessor,
967
+ );
968
+
969
+ try {
970
+ if (ingestOrDataProductAccessors.length) {
971
+ // Group by parent element path and create test data per parent
972
+ const parentElementMap = new Map<string, Accessor[]>();
973
+ for (const accessor of ingestOrDataProductAccessors) {
974
+ const key = accessor.accessorOwner;
975
+ if (key) {
976
+ const group = parentElementMap.get(key) ?? [];
977
+ group.push(accessor);
978
+ parentElementMap.set(key, group);
979
+ }
980
+ }
981
+ functionSuite.testData = Array.from(parentElementMap.entries()).map(
982
+ ([parentPath, group]) => {
983
+ const data = new FunctionTestData();
984
+ const element =
985
+ this.editorStore.graphManagerState.graph.getElement(parentPath);
986
+ data.element = PackageableElementExplicitReference.create(
987
+ element as AccessorOwner,
988
+ );
989
+ data.data = V1_buildRelationElementsDataFromAccessors(group);
990
+ return data;
991
+ },
809
992
  );
810
- const store = guaranteeNonNullable(stores[0]);
811
- const data = new FunctionTestData();
812
- if (store instanceof Database) {
813
- const relation = new RelationElementsData();
814
- data.element = PackageableElementExplicitReference.create(store);
815
- data.data = relation;
816
- } else if (store instanceof ModelStore) {
817
- const modelStoreData = createBareExternalFormat();
818
- data.element = PackageableElementExplicitReference.create(store);
819
- data.data = modelStoreData;
820
- } else {
821
- throw new UnsupportedOperationError(
822
- `function test store data does not support store: ${store.path}`,
993
+ } else {
994
+ // No ingest/data product accessors found — try runtime-based approach
995
+ const engineRuntimes = this.associatedRuntimes;
996
+ if (engineRuntimes?.length) {
997
+ assertTrue(
998
+ engineRuntimes.length === 1,
999
+ `Function Test Suite Only supports One Runtime at this time. Found ${engineRuntimes.length}`,
1000
+ );
1001
+ const engineRuntime = guaranteeNonNullable(engineRuntimes[0]);
1002
+ assertTrue(
1003
+ !(
1004
+ engineRuntime.connectionStores.length &&
1005
+ engineRuntime.connections.length
1006
+ ),
1007
+ `Runtime found has two connection types defined. Please use connection stores only`,
823
1008
  );
1009
+ const stores = [
1010
+ ...engineRuntime.connections
1011
+ .map((e) =>
1012
+ e.storeConnections.map((s) => s.connection.store?.value).flat(),
1013
+ )
1014
+ .flat(),
1015
+ ...engineRuntime.connectionStores
1016
+ .map((e) => e.storePointers.map((sPt) => sPt.value))
1017
+ .flat(),
1018
+ ].filter(isNonNullable);
1019
+ assertTrue(Boolean(stores.length), 'No runtime store found');
1020
+ assertTrue(
1021
+ stores.length === 1,
1022
+ 'Only one store supported in runtime for function tests',
1023
+ );
1024
+ const store = guaranteeNonNullable(stores[0]);
1025
+ const data = new FunctionTestData();
1026
+ if (store instanceof Database) {
1027
+ const relation = new RelationElementsData();
1028
+ data.element = PackageableElementExplicitReference.create(store);
1029
+ data.data = relation;
1030
+ } else if (store instanceof ModelStore) {
1031
+ const modelStoreData = createBareExternalFormat();
1032
+ data.element = PackageableElementExplicitReference.create(store);
1033
+ data.data = modelStoreData;
1034
+ } else {
1035
+ throw new UnsupportedOperationError(
1036
+ `function test store data does not support store: ${store.path}`,
1037
+ );
1038
+ }
1039
+ functionSuite.testData = [data];
1040
+ } else {
1041
+ const type = this.function.returnType.value.rawType;
1042
+ if (
1043
+ type.path === CORE_PURE_PATH.RELATION ||
1044
+ type.path === CORE_PURE_PATH.TABULAR_DATASET
1045
+ ) {
1046
+ this.editorStore.applicationStore.notificationService.notifyError(
1047
+ `Unable to create function test suite: no runtime or accessors found, or they could not be resolved`,
1048
+ );
1049
+ return;
1050
+ }
824
1051
  }
825
- functionSuite.testData = [data];
826
- } catch (error) {
827
- assertErrorThrown(error);
828
- this.editorStore.applicationStore.notificationService.notifyError(
829
- `Unable to create function test suite: ${error.message}`,
830
- );
831
- return;
832
1052
  }
1053
+ } catch (error) {
1054
+ assertErrorThrown(error);
1055
+ this.editorStore.applicationStore.notificationService.notifyError(
1056
+ `Unable to create function test suite: ${error.message}`,
1057
+ );
1058
+ return;
833
1059
  }
834
- createFunctionTest(
1060
+
1061
+ const hasTestData =
1062
+ this.containsRuntime ||
1063
+ ingestOrDataProductAccessors.length > 0 ||
1064
+ databaseAccessors.length > 0;
1065
+ yield createFunctionTest(
835
1066
  testName,
836
1067
  this.editorStore.changeDetectionState.observerContext,
837
- this.containsRuntime,
1068
+ hasTestData,
838
1069
  this.function,
839
1070
  this.editorStore,
840
1071
  functionSuite,
@@ -20,6 +20,7 @@ import {
20
20
  assertNonNullable,
21
21
  assertTrue,
22
22
  type GeneratorFn,
23
+ type PlainObject,
23
24
  } from '@finos/legend-shared';
24
25
  import type { EditorStore } from '../../EditorStore.js';
25
26
  import { action, flow, makeObservable, observable } from 'mobx';
@@ -27,14 +28,24 @@ import {
27
28
  type DeployProjectResponse,
28
29
  MetadataRequestOptions,
29
30
  } from '@finos/legend-graph';
31
+ import type { Entity } from '@finos/legend-storage';
30
32
  import { LegendStudioTelemetryHelper } from '../../../../__lib__/LegendStudioTelemetryHelper.js';
31
33
 
34
+ export const DEV_SNAPSHOT_VERSION = '1.0.0-SNAPSHOT';
35
+
32
36
  export class DevMetadataState {
33
37
  readonly editorStore: EditorStore;
34
38
  result: DeployProjectResponse | undefined;
35
39
  options: MetadataRequestOptions = new MetadataRequestOptions();
36
40
  pushState = ActionState.create();
37
41
 
42
+ // Compare-with-dev state
43
+ compareState = ActionState.create();
44
+ currentWorkspaceCode: string | undefined;
45
+ snapshotCode: string | undefined;
46
+ snapshotNotAvailable = false;
47
+ isCompareModalOpen = false;
48
+
38
49
  constructor(editorStore: EditorStore) {
39
50
  this.editorStore = editorStore;
40
51
 
@@ -43,6 +54,14 @@ export class DevMetadataState {
43
54
  push: flow,
44
55
  options: observable,
45
56
  setOptions: action,
57
+ compareState: observable,
58
+ currentWorkspaceCode: observable,
59
+ snapshotCode: observable,
60
+ snapshotNotAvailable: observable,
61
+ isCompareModalOpen: observable,
62
+ openCompareModal: action,
63
+ closeCompareModal: action,
64
+ compareWithSnapshot: flow,
46
65
  });
47
66
  }
48
67
 
@@ -50,6 +69,14 @@ export class DevMetadataState {
50
69
  this.options = options;
51
70
  }
52
71
 
72
+ openCompareModal(): void {
73
+ this.isCompareModalOpen = true;
74
+ }
75
+
76
+ closeCompareModal(): void {
77
+ this.isCompareModalOpen = false;
78
+ }
79
+
53
80
  get projectGAV(): { groupId: string; artifactId: string } | undefined {
54
81
  const currentProjectConfiguration =
55
82
  this.editorStore.projectConfigurationEditorState.projectConfiguration;
@@ -62,6 +89,55 @@ export class DevMetadataState {
62
89
  return undefined;
63
90
  }
64
91
 
92
+ *compareWithSnapshot(): GeneratorFn<void> {
93
+ try {
94
+ this.compareState.inProgress();
95
+ this.snapshotNotAvailable = false;
96
+ this.currentWorkspaceCode = undefined;
97
+ this.snapshotCode = undefined;
98
+ const gav = this.projectGAV;
99
+ assertNonNullable(gav, 'Project configuration is required to compare');
100
+ const graphManager = this.editorStore.graphManagerState.graphManager;
101
+
102
+ // 1) Current workspace -> Pure code (same as toggling to text mode)
103
+ const currentCode = (yield graphManager.graphToPureCode(
104
+ this.editorStore.graphManagerState.graph,
105
+ { pretty: true, excludeUnknown: true },
106
+ )) as string;
107
+ this.currentWorkspaceCode = currentCode;
108
+
109
+ // 2) Snapshot from depot -> Pure code; if the call fails we treat
110
+ // it as "nothing deployed yet"
111
+ let snapshotEntities: Entity[] | undefined;
112
+ try {
113
+ const entitiesJson =
114
+ (yield this.editorStore.depotServerClient.getVersionEntities(
115
+ gav.groupId,
116
+ gav.artifactId,
117
+ DEV_SNAPSHOT_VERSION,
118
+ )) as PlainObject<Entity>[];
119
+ snapshotEntities = entitiesJson as unknown as Entity[];
120
+ } catch {
121
+ this.snapshotNotAvailable = true;
122
+ this.compareState.complete();
123
+ return;
124
+ }
125
+
126
+ const snapshotCode = (yield graphManager.entitiesToPureCode(
127
+ snapshotEntities,
128
+ { pretty: true },
129
+ )) as string;
130
+ this.snapshotCode = snapshotCode;
131
+ this.compareState.complete();
132
+ } catch (error) {
133
+ assertErrorThrown(error);
134
+ this.editorStore.applicationStore.notificationService.notifyError(
135
+ `Error comparing with dev snapshot: ${error.message}`,
136
+ );
137
+ this.compareState.fail();
138
+ }
139
+ }
140
+
65
141
  *push(): GeneratorFn<void> {
66
142
  try {
67
143
  this.result = undefined;