@finos/legend-application-data-cube 0.3.5 → 0.3.7

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 (32) hide show
  1. package/lib/components/builder/LegendDataCubeCreator.d.ts.map +1 -1
  2. package/lib/components/builder/LegendDataCubeCreator.js +2 -5
  3. package/lib/components/builder/LegendDataCubeCreator.js.map +1 -1
  4. package/lib/components/builder/source/AdhocQueryDataCubeSourceBuilder.d.ts +8 -0
  5. package/lib/components/builder/source/AdhocQueryDataCubeSourceBuilder.d.ts.map +1 -1
  6. package/lib/components/builder/source/AdhocQueryDataCubeSourceBuilder.js +175 -2
  7. package/lib/components/builder/source/AdhocQueryDataCubeSourceBuilder.js.map +1 -1
  8. package/lib/index.css +1 -1
  9. package/lib/package.json +1 -1
  10. package/lib/stores/LegendDataCubeDataCubeEngine.d.ts +9 -4
  11. package/lib/stores/LegendDataCubeDataCubeEngine.d.ts.map +1 -1
  12. package/lib/stores/LegendDataCubeDataCubeEngine.js +34 -10
  13. package/lib/stores/LegendDataCubeDataCubeEngine.js.map +1 -1
  14. package/lib/stores/builder/LegendDataCubeCreatorState.d.ts.map +1 -1
  15. package/lib/stores/builder/LegendDataCubeCreatorState.js +1 -1
  16. package/lib/stores/builder/LegendDataCubeCreatorState.js.map +1 -1
  17. package/lib/stores/builder/source/AdHocCodeEditorState.d.ts +40 -0
  18. package/lib/stores/builder/source/AdHocCodeEditorState.d.ts.map +1 -0
  19. package/lib/stores/builder/source/AdHocCodeEditorState.js +147 -0
  20. package/lib/stores/builder/source/AdHocCodeEditorState.js.map +1 -0
  21. package/lib/stores/builder/source/AdhocQueryDataCubeSourceBuilderState.d.ts +38 -1
  22. package/lib/stores/builder/source/AdhocQueryDataCubeSourceBuilderState.d.ts.map +1 -1
  23. package/lib/stores/builder/source/AdhocQueryDataCubeSourceBuilderState.js +144 -4
  24. package/lib/stores/builder/source/AdhocQueryDataCubeSourceBuilderState.js.map +1 -1
  25. package/package.json +7 -7
  26. package/src/components/builder/LegendDataCubeCreator.tsx +2 -4
  27. package/src/components/builder/source/AdhocQueryDataCubeSourceBuilder.tsx +348 -4
  28. package/src/stores/LegendDataCubeDataCubeEngine.ts +38 -12
  29. package/src/stores/builder/LegendDataCubeCreatorState.tsx +1 -0
  30. package/src/stores/builder/source/AdHocCodeEditorState.tsx +189 -0
  31. package/src/stores/builder/source/AdhocQueryDataCubeSourceBuilderState.ts +221 -4
  32. package/tsconfig.json +1 -0
@@ -15,14 +15,358 @@
15
15
  */
16
16
 
17
17
  import { observer } from 'mobx-react-lite';
18
+ import { CustomSelectorInput, WarningIcon } from '@finos/legend-art';
19
+ import { useEffect, useState } from 'react';
20
+ import { flowResult } from 'mobx';
21
+ import { type LegendDataCubeBuilderStore } from '../../../stores/builder/LegendDataCubeBuilderStore.js';
22
+ import { LATEST_VERSION_ALIAS } from '@finos/legend-server-depot';
23
+ import {
24
+ ActionState,
25
+ assertErrorThrown,
26
+ compareSemVerVersions,
27
+ guaranteeNonNullable,
28
+ } from '@finos/legend-shared';
29
+ import {
30
+ CORE_PURE_PATH,
31
+ V1_Mapping,
32
+ V1_PackageableRuntime,
33
+ V1_deserializePackageableElement,
34
+ } from '@finos/legend-graph';
35
+ import type { Entity } from '@finos/legend-storage';
18
36
  import type { AdhocQueryDataCubeSourceBuilderState } from '../../../stores/builder/source/AdhocQueryDataCubeSourceBuilderState.js';
37
+ import {
38
+ buildProjectOption,
39
+ buildRuntimeOption,
40
+ buildVersionOption,
41
+ type ProjectOption,
42
+ type RuntimeOption,
43
+ type VersionOption,
44
+ } from './UserDefinedFunctionDataCubeSourceBuilder.js';
45
+ import { DataCubeCodeEditor } from '@finos/legend-data-cube';
46
+
47
+ export type MappingOption = {
48
+ label: string;
49
+ value: V1_Mapping;
50
+ };
51
+ export const buildMappingOption = (mapping: V1_Mapping): MappingOption => ({
52
+ label: mapping.name,
53
+ value: mapping,
54
+ });
19
55
 
20
56
  export const AdhocQueryDataCubeSourceBuilder = observer(
21
- (props: { sourceBuilder: AdhocQueryDataCubeSourceBuilderState }) => {
57
+ (props: {
58
+ sourceBuilder: AdhocQueryDataCubeSourceBuilderState;
59
+ store: LegendDataCubeBuilderStore;
60
+ }) => {
61
+ const { sourceBuilder, store } = props;
62
+ const [fetchSelectedProjectVersionsStatus] = useState(ActionState.create());
63
+ const [fetchSelectedVersionRuntimesStatus] = useState(ActionState.create());
64
+ const [fetchSelectedVersionMappingsStatus] = useState(ActionState.create());
65
+
66
+ //projects
67
+ const projectOptions = sourceBuilder.projects.map(buildProjectOption);
68
+ const selectedProjectOption = sourceBuilder.currentProject
69
+ ? buildProjectOption(sourceBuilder.currentProject)
70
+ : null;
71
+ const projectSelectorPlaceholder = sourceBuilder.loadProjectsState
72
+ .isInProgress
73
+ ? 'Loading projects'
74
+ : sourceBuilder.loadProjectsState.hasFailed
75
+ ? 'Error fetching projects'
76
+ : sourceBuilder.projects.length
77
+ ? 'Choose a project'
78
+ : 'No Projects available from depot';
79
+ const onProjectOptionChange = async (
80
+ option: ProjectOption | null,
81
+ ): Promise<void> => {
82
+ if (option?.value !== sourceBuilder.currentProject) {
83
+ sourceBuilder.setCurrentProject(option?.value);
84
+
85
+ // cascade
86
+ sourceBuilder.setCurrentVersionId(undefined);
87
+ sourceBuilder.setCurrentProjectVersions([]);
88
+ sourceBuilder.setCurrentRuntime(undefined);
89
+ sourceBuilder.setRuntimes([]);
90
+ sourceBuilder.setModelPointer(undefined);
91
+ sourceBuilder.setMappings([]);
92
+ sourceBuilder.setCurrentMapping(undefined);
93
+ sourceBuilder.codeEditorState.clearQuery();
94
+
95
+ try {
96
+ fetchSelectedProjectVersionsStatus.inProgress();
97
+ const versions = await store.depotServerClient.getVersions(
98
+ guaranteeNonNullable(option?.value.groupId),
99
+ guaranteeNonNullable(option?.value.artifactId),
100
+ true,
101
+ );
102
+ sourceBuilder.setCurrentProjectVersions(versions);
103
+ } catch (error) {
104
+ assertErrorThrown(error);
105
+ store.application.notificationService.notifyError(error);
106
+ } finally {
107
+ fetchSelectedProjectVersionsStatus.reset();
108
+ }
109
+ }
110
+ };
111
+
112
+ //versions
113
+ const versionOptions = [
114
+ LATEST_VERSION_ALIAS,
115
+ ...(sourceBuilder.currentProjectVersions ?? []),
116
+ ]
117
+ .toSorted((v1, v2) => compareSemVerVersions(v2, v1))
118
+ .map(buildVersionOption);
119
+ const selectedVersionOption = sourceBuilder.currentVersionId
120
+ ? buildVersionOption(sourceBuilder.currentVersionId)
121
+ : null;
122
+ const versionSelectorPlaceholder = !sourceBuilder.currentProject
123
+ ? 'No project selected'
124
+ : 'Choose a version';
125
+ const onVersionChange = async (
126
+ option: VersionOption | null,
127
+ ): Promise<void> => {
128
+ if (option?.value !== sourceBuilder.currentVersionId) {
129
+ sourceBuilder.setCurrentVersionId(option?.value);
130
+
131
+ //cascade
132
+ sourceBuilder.setCurrentRuntime(undefined);
133
+ sourceBuilder.setRuntimes([]);
134
+ sourceBuilder.setCurrentMapping(undefined);
135
+ sourceBuilder.setMappings([]);
136
+ sourceBuilder.codeEditorState.clearQuery();
137
+
138
+ //by this point, we should have all the necessary info to build the model context pointer
139
+ sourceBuilder.setModelPointer(
140
+ sourceBuilder.buildPureModelContextPointer(),
141
+ );
142
+
143
+ try {
144
+ const plugins =
145
+ store.application.pluginManager.getPureProtocolProcessorPlugins();
146
+
147
+ //runtimes
148
+ fetchSelectedVersionRuntimesStatus.inProgress();
149
+ const allFetchedRuntimes =
150
+ await store.depotServerClient.getVersionEntities(
151
+ guaranteeNonNullable(sourceBuilder.currentProject?.groupId),
152
+ guaranteeNonNullable(sourceBuilder.currentProject?.artifactId),
153
+ guaranteeNonNullable(option?.value),
154
+ CORE_PURE_PATH.RUNTIME,
155
+ );
156
+ const fetchedRuntimeOptions: V1_PackageableRuntime[] = [];
157
+ allFetchedRuntimes.forEach((runtime) => {
158
+ const runtimeEntity = runtime.entity as Entity;
159
+ const currentRuntimeElement = V1_deserializePackageableElement(
160
+ runtimeEntity.content,
161
+ plugins,
162
+ );
163
+ if (currentRuntimeElement instanceof V1_PackageableRuntime) {
164
+ fetchedRuntimeOptions.push(currentRuntimeElement);
165
+ }
166
+ });
167
+ sourceBuilder.setRuntimes(fetchedRuntimeOptions);
168
+ fetchSelectedVersionRuntimesStatus.reset();
169
+
170
+ //mappings
171
+ fetchSelectedVersionMappingsStatus.inProgress();
172
+ const allFetchedMappings =
173
+ await store.depotServerClient.getVersionEntities(
174
+ guaranteeNonNullable(sourceBuilder.currentProject?.groupId),
175
+ guaranteeNonNullable(sourceBuilder.currentProject?.artifactId),
176
+ guaranteeNonNullable(option?.value),
177
+ CORE_PURE_PATH.MAPPING,
178
+ );
179
+ const fetchedMappingOptions: V1_Mapping[] = [];
180
+ allFetchedMappings.forEach((mapping) => {
181
+ const mappingEntity = mapping.entity as Entity;
182
+ const currentMappingElement = V1_deserializePackageableElement(
183
+ mappingEntity.content,
184
+ plugins,
185
+ );
186
+ if (currentMappingElement instanceof V1_Mapping) {
187
+ fetchedMappingOptions.push(currentMappingElement);
188
+ }
189
+ });
190
+ sourceBuilder.setMappings(fetchedMappingOptions);
191
+ fetchSelectedVersionMappingsStatus.reset();
192
+ } catch (error) {
193
+ assertErrorThrown(error);
194
+ store.application.notificationService.notifyError(error);
195
+ }
196
+ }
197
+ };
198
+
199
+ //runtimes
200
+ const runtimeOptions = sourceBuilder.runtimes?.map(buildRuntimeOption);
201
+ const selectedRuntimeOption = sourceBuilder.currentRuntime
202
+ ? buildRuntimeOption(sourceBuilder.currentRuntime)
203
+ : null;
204
+ const runtimeSelectorPlaceholder = !sourceBuilder.currentVersionId
205
+ ? 'No version selected'
206
+ : 'Choose a runtime';
207
+ const onRuntimeChange = (option: RuntimeOption | null) => {
208
+ if (option === null) {
209
+ sourceBuilder.codeEditorState.clearQuery();
210
+ }
211
+ if (option?.value !== sourceBuilder.currentRuntime) {
212
+ sourceBuilder.setCurrentRuntime(option?.value);
213
+ }
214
+ };
215
+
216
+ //mappings
217
+ const mappingOptions = sourceBuilder.mappings?.map(buildMappingOption);
218
+ const selectedMappingOption = sourceBuilder.currentMapping
219
+ ? buildMappingOption(sourceBuilder.currentMapping)
220
+ : null;
221
+ const mappingSelectorPlaceholder = !sourceBuilder.currentVersionId
222
+ ? 'No version selected'
223
+ : 'Choose a mapping';
224
+ const onMappingChange = (option: MappingOption | null) => {
225
+ if (option?.value !== sourceBuilder.currentMapping) {
226
+ sourceBuilder.setCurrentMapping(option?.value);
227
+ }
228
+ };
229
+
230
+ const allowQueryEditing = (): boolean => {
231
+ return Boolean(
232
+ sourceBuilder.currentProject &&
233
+ sourceBuilder.currentVersionId &&
234
+ sourceBuilder.currentRuntime,
235
+ );
236
+ };
237
+
238
+ useEffect(() => {
239
+ flowResult(sourceBuilder.loadProjects()).catch(
240
+ store.application.alertUnhandledError,
241
+ );
242
+ }, [sourceBuilder, store.application]);
243
+
22
244
  return (
23
- <div className="flex h-full w-full p-2">
24
- <div className="flex h-6 items-center text-neutral-500">
25
- This is a work-in-progress.
245
+ <div className="flex h-full w-full">
246
+ <div className="m-3 flex w-full flex-col items-stretch gap-2 text-neutral-500">
247
+ <div className="query-setup__wizard__group">
248
+ <div className="query-setup__wizard__group__title">Project</div>
249
+ <CustomSelectorInput
250
+ className="query-setup__wizard__selector"
251
+ options={projectOptions}
252
+ disabled={
253
+ sourceBuilder.loadProjectsState.isInProgress ||
254
+ !projectOptions.length
255
+ }
256
+ isLoading={sourceBuilder.loadProjectsState.isInProgress}
257
+ onChange={(option: ProjectOption | null) => {
258
+ onProjectOptionChange(option).catch(
259
+ store.application.alertUnhandledError,
260
+ );
261
+ }}
262
+ value={selectedProjectOption}
263
+ placeholder={projectSelectorPlaceholder}
264
+ isClearable={true}
265
+ escapeClearsValue={true}
266
+ />
267
+ </div>
268
+
269
+ <div className="query-setup__wizard__group">
270
+ <div className="query-setup__wizard__group__title">Version</div>
271
+ <CustomSelectorInput
272
+ className="query-setup__wizard__selector"
273
+ options={versionOptions}
274
+ disabled={
275
+ !sourceBuilder.currentProject ||
276
+ fetchSelectedProjectVersionsStatus.isInProgress ||
277
+ !versionOptions.length
278
+ }
279
+ isLoading={fetchSelectedProjectVersionsStatus.isInProgress}
280
+ onChange={(option: VersionOption | null) => {
281
+ onVersionChange(option).catch(
282
+ store.application.alertUnhandledError,
283
+ );
284
+ }}
285
+ value={selectedVersionOption}
286
+ placeholder={versionSelectorPlaceholder}
287
+ isClearable={true}
288
+ escapeClearsValue={true}
289
+ />
290
+ </div>
291
+
292
+ <div className="query-setup__wizard__group">
293
+ <div className="query-setup__wizard__group__title">Runtime</div>
294
+ <CustomSelectorInput
295
+ className="query-setup__wizard__selector"
296
+ options={runtimeOptions}
297
+ disabled={
298
+ !sourceBuilder.currentVersionId ||
299
+ fetchSelectedVersionRuntimesStatus.isInProgress ||
300
+ !versionOptions.length
301
+ }
302
+ isLoading={fetchSelectedVersionRuntimesStatus.isInProgress}
303
+ onChange={(option: RuntimeOption | null) => {
304
+ onRuntimeChange(option);
305
+ }}
306
+ value={selectedRuntimeOption}
307
+ placeholder={runtimeSelectorPlaceholder}
308
+ isClearable={true}
309
+ escapeClearsValue={true}
310
+ />
311
+ </div>
312
+
313
+ <div className="query-setup__wizard__group">
314
+ <div className="query-setup__wizard__group__title">
315
+ Mapping (optional)
316
+ </div>
317
+ <CustomSelectorInput
318
+ className="query-setup__wizard__selector"
319
+ options={mappingOptions}
320
+ disabled={
321
+ !sourceBuilder.currentVersionId ||
322
+ fetchSelectedVersionMappingsStatus.isInProgress ||
323
+ !versionOptions.length
324
+ }
325
+ isLoading={fetchSelectedVersionMappingsStatus.isInProgress}
326
+ onChange={(option: MappingOption | null) => {
327
+ onMappingChange(option);
328
+ }}
329
+ value={selectedMappingOption}
330
+ placeholder={mappingSelectorPlaceholder}
331
+ isClearable={true}
332
+ escapeClearsValue={true}
333
+ />
334
+ </div>
335
+
336
+ <div className="query-setup__wizard__group">
337
+ <div className="query-setup__wizard__group__title">Query</div>
338
+
339
+ <div
340
+ className="mt-2 h-40 w-full"
341
+ style={{
342
+ border: '2px solid #e5e7eb',
343
+ padding: '5px',
344
+ borderRadius: '5px',
345
+ position: 'relative',
346
+ backgroundColor: allowQueryEditing() ? '#ffffff' : '#f2f2f2',
347
+ }}
348
+ >
349
+ {allowQueryEditing() ? (
350
+ <DataCubeCodeEditor state={sourceBuilder.codeEditorState} />
351
+ ) : (
352
+ <div>No runtime selected </div>
353
+ )}
354
+ </div>
355
+ {sourceBuilder.codeEditorState.compilationError ? (
356
+ <div
357
+ className="flex h-full w-full flex-shrink-0 text-sm"
358
+ style={{ color: 'red' }}
359
+ >
360
+ <WarningIcon />
361
+ Compilation Error: Ensure TDS is returned and all class names
362
+ are correct
363
+ </div>
364
+ ) : sourceBuilder.queryCompileState.isInProgress ? (
365
+ <div className="flex h-full w-full flex-shrink-0 text-sm">
366
+ Compiling query...
367
+ </div>
368
+ ) : null}
369
+ </div>
26
370
  </div>
27
371
  </div>
28
372
  );
@@ -88,7 +88,6 @@ import {
88
88
  import {
89
89
  _elementPtr,
90
90
  DataCubeEngine,
91
- type DataCubeSource,
92
91
  type CompletionItem,
93
92
  _function,
94
93
  DataCubeFunction,
@@ -103,6 +102,7 @@ import {
103
102
  DataCubeExecutionError,
104
103
  RawUserDefinedFunctionDataCubeSource,
105
104
  ADHOC_FUNCTION_DATA_CUBE_SOURCE_TYPE,
105
+ type DataCubeSource,
106
106
  UserDefinedFunctionDataCubeSource,
107
107
  DataCubeQueryFilterOperator,
108
108
  } from '@finos/legend-data-cube';
@@ -177,6 +177,9 @@ export class LegendDataCubeDataCubeEngine extends DataCubeEngine {
177
177
  const rawSource =
178
178
  RawAdhocQueryDataCubeSource.serialization.fromJson(value);
179
179
  const source = new AdhocQueryDataCubeSource();
180
+ if (rawSource.mapping) {
181
+ source.mapping = rawSource.mapping;
182
+ }
180
183
  source.runtime = rawSource.runtime;
181
184
  source.model = rawSource.model;
182
185
  source.query = await this.parseValueSpecification(
@@ -406,7 +409,10 @@ export class LegendDataCubeDataCubeEngine extends DataCubeEngine {
406
409
  ),
407
410
  ),
408
411
  );
409
- source.query = at(source.lambda.body, 0);
412
+ // If the lambda has multiple expressions, the source query should only be the final
413
+ // expression of the lambda. All previous expressions should be left untouched and will
414
+ // be prepended to the transformed query when it is executed.
415
+ source.query = at(source.lambda.body, source.lambda.body.length - 1);
410
416
  try {
411
417
  source.columns = (
412
418
  await this._getLambdaRelationType(
@@ -527,35 +533,43 @@ export class LegendDataCubeDataCubeEngine extends DataCubeEngine {
527
533
  override async getQueryTypeahead(
528
534
  code: string,
529
535
  baseQuery: V1_Lambda,
530
- source: DataCubeSource,
536
+ context: DataCubeSource | PlainObject,
531
537
  ) {
532
538
  const baseQueryCode = await this.getValueSpecificationCode(baseQuery);
533
539
  let codeBlock = baseQueryCode + code;
534
540
  codeBlock = codeBlock.startsWith(LAMBDA_PIPE)
535
541
  ? codeBlock.substring(LAMBDA_PIPE.length)
536
542
  : codeBlock;
537
- if (source instanceof AdhocQueryDataCubeSource) {
543
+ if (context instanceof AdhocQueryDataCubeSource) {
538
544
  return (
539
545
  await this._engineServerClient.completeCode({
540
546
  codeBlock,
541
- model: source.model,
547
+ model: context.model,
542
548
  })
543
549
  ).completions as CompletionItem[];
544
- } else if (source instanceof UserDefinedFunctionDataCubeSource) {
550
+ } else if (context instanceof UserDefinedFunctionDataCubeSource) {
545
551
  return (
546
552
  await this._engineServerClient.completeCode({
547
553
  codeBlock,
548
- model: source.model,
554
+ model: context.model,
549
555
  })
550
556
  ).completions as CompletionItem[];
551
- } else if (source instanceof LegendQueryDataCubeSource) {
557
+ } else if (context instanceof LegendQueryDataCubeSource) {
558
+ return (
559
+ await this._engineServerClient.completeCode({
560
+ codeBlock,
561
+ model: context.model,
562
+ })
563
+ ).completions as CompletionItem[];
564
+ } else if (Object.getPrototypeOf(context) === Object.prototype) {
552
565
  return (
553
566
  await this._engineServerClient.completeCode({
554
567
  codeBlock,
555
- model: source.model,
568
+ model: context,
556
569
  })
557
570
  ).completions as CompletionItem[];
558
571
  }
572
+
559
573
  throw new UnsupportedOperationError(
560
574
  `Can't get code completion for lambda with unsupported source`,
561
575
  );
@@ -642,7 +656,16 @@ export class LegendDataCubeDataCubeEngine extends DataCubeEngine {
642
656
  result = await this._runQuery(query, source.model, undefined, options);
643
657
  } else if (source instanceof LegendQueryDataCubeSource) {
644
658
  query.parameters = source.lambda.parameters;
645
- query.body = [...source.letParameterValueSpec, ...query.body];
659
+ // If the source lambda has multiple expressions, we should prepend all but the
660
+ // last expression to the transformed query body (which came from the final
661
+ // expression of the source lambda).
662
+ query.body = [
663
+ ...source.letParameterValueSpec,
664
+ ...(source.lambda.body.length > 1
665
+ ? source.lambda.body.slice(0, -1)
666
+ : []),
667
+ ...query.body,
668
+ ];
646
669
  result = await this._runQuery(
647
670
  query,
648
671
  source.model,
@@ -741,7 +764,10 @@ export class LegendDataCubeDataCubeEngine extends DataCubeEngine {
741
764
  if (source instanceof AdhocQueryDataCubeSource) {
742
765
  return _function(
743
766
  DataCubeFunction.FROM,
744
- [_elementPtr(source.runtime)].filter(isNonNullable),
767
+ [
768
+ source.mapping ? _elementPtr(source.mapping) : undefined,
769
+ _elementPtr(source.runtime),
770
+ ].filter(isNonNullable),
745
771
  );
746
772
  } else if (source instanceof UserDefinedFunctionDataCubeSource) {
747
773
  return _function(
@@ -879,7 +905,7 @@ export class LegendDataCubeDataCubeEngine extends DataCubeEngine {
879
905
  );
880
906
  }
881
907
 
882
- private async _getLambdaRelationType(
908
+ async _getLambdaRelationType(
883
909
  lambda: PlainObject<V1_Lambda>,
884
910
  model: PlainObject<V1_PureModelContext>,
885
911
  ) {
@@ -107,6 +107,7 @@ export class LegendDataCubeCreatorState {
107
107
  this._application,
108
108
  this._engine,
109
109
  this._alertService,
110
+ this._store,
110
111
  );
111
112
  case LegendDataCubeSourceBuilderType.LOCAL_FILE:
112
113
  return new LocalFileDataCubeSourceBuilderState(
@@ -0,0 +1,189 @@
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
+
17
+ import { clearMarkers, setErrorMarkers } from '@finos/legend-code-editor';
18
+ import {
19
+ DataCubeCodeEditorState,
20
+ type DataCubeEngine,
21
+ } from '@finos/legend-data-cube';
22
+ import { EngineError, type V1_Lambda } from '@finos/legend-graph';
23
+ import {
24
+ uuid,
25
+ ActionState,
26
+ type PlainObject,
27
+ assertErrorThrown,
28
+ } from '@finos/legend-shared';
29
+ import { action, makeObservable, observable, runInAction } from 'mobx';
30
+
31
+ export class AdHocCodeEditorState extends DataCubeCodeEditorState {
32
+ protected override readonly uuid = uuid();
33
+ codeSuffix: string;
34
+
35
+ readonly validationState = ActionState.create();
36
+ compilationError?: Error | undefined;
37
+ override currentlyEditing = false;
38
+
39
+ alertHandler: (error: Error) => void;
40
+ override model: PlainObject | undefined;
41
+ compileQueryCheck: () => Promise<boolean | undefined>;
42
+ queryLambda: () => V1_Lambda;
43
+
44
+ constructor(
45
+ alertHandler: (error: Error) => void,
46
+ model: PlainObject | undefined,
47
+ compileQueryCheck: () => Promise<boolean | undefined>,
48
+ queryLambda: () => V1_Lambda,
49
+ engine: DataCubeEngine,
50
+ ) {
51
+ super(engine);
52
+ makeObservable(this, {
53
+ code: observable,
54
+
55
+ editor: observable.ref,
56
+ setEditor: action,
57
+
58
+ codeError: observable.ref,
59
+ compilationError: observable,
60
+ showError: action,
61
+ clearError: action,
62
+
63
+ returnType: observable,
64
+ setReturnType: action,
65
+ });
66
+
67
+ this.codePrefix = '';
68
+ this.codeSuffix = '';
69
+
70
+ this.alertHandler = alertHandler;
71
+ this.model = model;
72
+ this.compileQueryCheck = compileQueryCheck;
73
+ this.queryLambda = queryLambda;
74
+ this.engine = engine;
75
+ }
76
+
77
+ compileFunction = async (): Promise<boolean | undefined> => {
78
+ if (this.code.length !== 0) {
79
+ try {
80
+ this.currentlyEditing = false;
81
+ return await this.compileQueryCheck();
82
+ } catch (error) {
83
+ assertErrorThrown(error);
84
+ if (error instanceof EngineError) {
85
+ this.validationState.fail();
86
+ // correct the source information since we added prefix to the code
87
+ // and reveal error in the editor
88
+ if (error.sourceInformation) {
89
+ error.sourceInformation.startColumn -=
90
+ error.sourceInformation.startLine === 1
91
+ ? this.codePrefix.length
92
+ : 0;
93
+ error.sourceInformation.endColumn -=
94
+ error.sourceInformation.endLine === 1
95
+ ? this.codePrefix.length
96
+ : 0;
97
+ const fullRange = this.editorModel.getFullModelRange();
98
+ if (
99
+ error.sourceInformation.startLine < 1 ||
100
+ (error.sourceInformation.startLine === 1 &&
101
+ error.sourceInformation.startColumn < 1) ||
102
+ error.sourceInformation.endLine > fullRange.endLineNumber ||
103
+ (error.sourceInformation.endLine === fullRange.endLineNumber &&
104
+ error.sourceInformation.endColumn > fullRange.endColumn)
105
+ ) {
106
+ error.sourceInformation.startColumn = fullRange.startColumn;
107
+ error.sourceInformation.startLine = fullRange.startLineNumber;
108
+ error.sourceInformation.endColumn = fullRange.endColumn;
109
+ error.sourceInformation.endLine = fullRange.endLineNumber;
110
+ }
111
+ }
112
+ this.showError(error);
113
+ return undefined;
114
+ } else if (error instanceof Error) {
115
+ this.setCompilationError(error);
116
+ }
117
+ this.alertHandler(error);
118
+ }
119
+ }
120
+ return undefined;
121
+ };
122
+
123
+ override get hasErrors(): boolean {
124
+ return Boolean(this.codeError ?? this.compilationError);
125
+ }
126
+
127
+ isValid(): boolean {
128
+ return Boolean(
129
+ !this.hasErrors &&
130
+ this.code.length !== 0 &&
131
+ !this.currentlyEditing &&
132
+ !this.validationState.isInProgress,
133
+ );
134
+ }
135
+
136
+ setCompilationError(val: Error | undefined) {
137
+ runInAction(() => {
138
+ this.compilationError = val;
139
+ });
140
+ }
141
+
142
+ setModel(val: PlainObject | undefined) {
143
+ this.model = val;
144
+ }
145
+
146
+ clearQuery() {
147
+ this.clearError();
148
+ this.editor?.getModel()?.setValue('');
149
+ }
150
+
151
+ override clearError() {
152
+ this.codeError = undefined;
153
+ this.compilationError = undefined;
154
+ clearMarkers(this.uuid);
155
+ }
156
+
157
+ showError(error: EngineError) {
158
+ this.codeError = error;
159
+ if (error.sourceInformation) {
160
+ setErrorMarkers(
161
+ this.editorModel,
162
+ [
163
+ {
164
+ message: error.message,
165
+ startLineNumber: error.sourceInformation.startLine,
166
+ startColumn: error.sourceInformation.startColumn,
167
+ endLineNumber: error.sourceInformation.endLine,
168
+ endColumn: error.sourceInformation.endColumn,
169
+ },
170
+ ],
171
+ this.uuid,
172
+ );
173
+ }
174
+ }
175
+
176
+ async getReturnType() {
177
+ this.validationState.inProgress();
178
+
179
+ // properly reset the error state before revalidating
180
+ this.clearError();
181
+ this.setReturnType(undefined);
182
+
183
+ await this.compileFunction();
184
+
185
+ this.validationState.complete();
186
+
187
+ return undefined;
188
+ }
189
+ }