@finos/legend-application-studio 28.21.51 → 28.21.53
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.d.ts.map +1 -1
- package/lib/components/editor/Editor.js +2 -1
- package/lib/components/editor/Editor.js.map +1 -1
- package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.js +14 -2
- package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.js.map +1 -1
- package/lib/components/editor/embedded-cube/EmbeddedCube.d.ts +23 -0
- package/lib/components/editor/embedded-cube/EmbeddedCube.d.ts.map +1 -0
- package/lib/components/editor/embedded-cube/EmbeddedCube.js +38 -0
- package/lib/components/editor/embedded-cube/EmbeddedCube.js.map +1 -0
- package/lib/components/editor/embedded-cube/EmbeddedCubeEditor.d.ts +35 -0
- package/lib/components/editor/embedded-cube/EmbeddedCubeEditor.d.ts.map +1 -0
- package/lib/components/editor/embedded-cube/EmbeddedCubeEditor.js +179 -0
- package/lib/components/editor/embedded-cube/EmbeddedCubeEditor.js.map +1 -0
- package/lib/components/extensions/Core_LegendStudioApplicationPlugin.d.ts.map +1 -1
- package/lib/components/extensions/Core_LegendStudioApplicationPlugin.js +4 -0
- package/lib/components/extensions/Core_LegendStudioApplicationPlugin.js.map +1 -1
- package/lib/index.css +1 -1
- package/lib/package.json +3 -1
- package/lib/stores/editor/EditorStore.d.ts +2 -0
- package/lib/stores/editor/EditorStore.d.ts.map +1 -1
- package/lib/stores/editor/EditorStore.js +6 -1
- package/lib/stores/editor/EditorStore.js.map +1 -1
- package/lib/stores/editor/embedded-cube/EmbeddedCubeState.d.ts +86 -0
- package/lib/stores/editor/embedded-cube/EmbeddedCubeState.d.ts.map +1 -0
- package/lib/stores/editor/embedded-cube/EmbeddedCubeState.js +206 -0
- package/lib/stores/editor/embedded-cube/EmbeddedCubeState.js.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeDataProductCatalog.d.ts +51 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeDataProductCatalog.d.ts.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeDataProductCatalog.js +148 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeDataProductCatalog.js.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeHost.d.ts +39 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeHost.d.ts.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeHost.js +60 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeHost.js.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeIngestCatalog.d.ts +57 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeIngestCatalog.d.ts.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeIngestCatalog.js +148 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeIngestCatalog.js.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeWorkspace.d.ts +58 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeWorkspace.d.ts.map +1 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeWorkspace.js +94 -0
- package/lib/stores/editor/embedded-cube/LegendStudioCubeWorkspace.js.map +1 -0
- package/lib/stores/graph-modifier/DSL_Ingest_GraphModifierHelper.d.ts +57 -0
- package/lib/stores/graph-modifier/DSL_Ingest_GraphModifierHelper.d.ts.map +1 -0
- package/lib/stores/graph-modifier/DSL_Ingest_GraphModifierHelper.js +151 -0
- package/lib/stores/graph-modifier/DSL_Ingest_GraphModifierHelper.js.map +1 -0
- package/package.json +13 -11
- package/src/components/editor/Editor.tsx +2 -0
- package/src/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.tsx +62 -0
- package/src/components/editor/embedded-cube/EmbeddedCube.tsx +57 -0
- package/src/components/editor/embedded-cube/EmbeddedCubeEditor.tsx +452 -0
- package/src/components/extensions/Core_LegendStudioApplicationPlugin.tsx +4 -0
- package/src/stores/editor/EditorStore.ts +6 -1
- package/src/stores/editor/embedded-cube/EmbeddedCubeState.ts +284 -0
- package/src/stores/editor/embedded-cube/LegendStudioCubeDataProductCatalog.ts +242 -0
- package/src/stores/editor/embedded-cube/LegendStudioCubeHost.ts +82 -0
- package/src/stores/editor/embedded-cube/LegendStudioCubeIngestCatalog.ts +235 -0
- package/src/stores/editor/embedded-cube/LegendStudioCubeWorkspace.ts +151 -0
- package/src/stores/graph-modifier/DSL_Ingest_GraphModifierHelper.ts +214 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026-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 {
|
|
18
|
+
MILESTONE_INGEST_COLUMNS,
|
|
19
|
+
type IngestDefinition,
|
|
20
|
+
} from '@finos/legend-graph';
|
|
21
|
+
import type { PlainObject } from '@finos/legend-shared';
|
|
22
|
+
import { action, computed, flowResult, makeObservable, observable } from 'mobx';
|
|
23
|
+
import {
|
|
24
|
+
getIngestDataSetNames,
|
|
25
|
+
getIngestMaterializedViews,
|
|
26
|
+
ingestDefinition_addMaterializedView,
|
|
27
|
+
ingestDefinition_updateMaterializedView,
|
|
28
|
+
type IngestMaterializedView,
|
|
29
|
+
type IngestMaterializedViewOptions,
|
|
30
|
+
} from '../../graph-modifier/DSL_Ingest_GraphModifierHelper.js';
|
|
31
|
+
import type { EditorStore } from '../EditorStore.js';
|
|
32
|
+
import { IngestDefinitionEditorState } from '../editor-state/element-editor-state/ingest/IngestDefinitionEditorState.js';
|
|
33
|
+
|
|
34
|
+
/** A data set's name: a letter or an underscore, then letters, digits and underscores */
|
|
35
|
+
const DATA_SET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The columns a batch-milestoned data set's write mode adds, `LAKE_IN_ID`,
|
|
39
|
+
* `LAKE_DIGEST` and the like, which a materialized view's primary key leaves
|
|
40
|
+
* out
|
|
41
|
+
*/
|
|
42
|
+
const MILESTONING_COLUMNS: readonly string[] = Object.values(
|
|
43
|
+
MILESTONE_INGEST_COLUMNS,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Legend Cube, opened full screen from an ingest definition's editor to build
|
|
48
|
+
* a materialized view of it from the project's ingest data sets (Create
|
|
49
|
+
* MatView). Studio's own shortcuts are off while it is open. Cube
|
|
50
|
+
* itself, and its host, are loaded with the dialog (`EmbeddedCube`), so
|
|
51
|
+
* nothing here imports them.
|
|
52
|
+
*/
|
|
53
|
+
export class EmbeddedCubeState {
|
|
54
|
+
readonly editorStore: EditorStore;
|
|
55
|
+
/** The definition the materialized view is added to; none while closed */
|
|
56
|
+
ingest: IngestDefinition | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* The materialized view opened to edit (Edit MatView, PLAN §11.18), by
|
|
59
|
+
* name: Cube opens on its lambda, and Save MatView replaces it; none for a
|
|
60
|
+
* new one
|
|
61
|
+
*/
|
|
62
|
+
matView: string | undefined;
|
|
63
|
+
|
|
64
|
+
constructor(editorStore: EditorStore) {
|
|
65
|
+
makeObservable(this, {
|
|
66
|
+
ingest: observable.ref,
|
|
67
|
+
matView: observable,
|
|
68
|
+
matViewDataSet: computed,
|
|
69
|
+
matViewLambda: computed,
|
|
70
|
+
open: action,
|
|
71
|
+
close: action,
|
|
72
|
+
addMaterializedView: action,
|
|
73
|
+
saveMaterializedView: action,
|
|
74
|
+
});
|
|
75
|
+
this.editorStore = editorStore;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get isOpen(): boolean {
|
|
79
|
+
return this.ingest !== undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Opens Cube on a new materialized view of the definition, or on one of its views to edit */
|
|
83
|
+
open(ingest: IngestDefinition, matView?: string): void {
|
|
84
|
+
this.ingest = ingest;
|
|
85
|
+
this.matView = matView;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
close(): void {
|
|
89
|
+
this.ingest = undefined;
|
|
90
|
+
this.matView = undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The materialized view opened to edit, as the definition holds it */
|
|
94
|
+
get matViewDataSet(): IngestMaterializedView | undefined {
|
|
95
|
+
const { ingest, matView } = this;
|
|
96
|
+
return ingest && matView !== undefined
|
|
97
|
+
? getIngestMaterializedViews(ingest).find((view) => view.name === matView)
|
|
98
|
+
: undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The lambda (protocol JSON) of the materialized view opened to edit */
|
|
102
|
+
get matViewLambda(): PlainObject | undefined {
|
|
103
|
+
return this.matViewDataSet?.lambda;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Why the name can't be the new materialized view's; none when it can */
|
|
107
|
+
getMaterializedViewNameProblem(name: string): string | undefined {
|
|
108
|
+
const trimmed = name.trim();
|
|
109
|
+
if (!trimmed) {
|
|
110
|
+
return 'Name the materialized view.';
|
|
111
|
+
}
|
|
112
|
+
if (!DATA_SET_NAME_PATTERN.test(trimmed)) {
|
|
113
|
+
return 'A data set name is letters, digits and underscores, and starts with a letter or an underscore.';
|
|
114
|
+
}
|
|
115
|
+
return this.ingest && getIngestDataSetNames(this.ingest).includes(trimmed)
|
|
116
|
+
? `The ingest definition already has a data set named ${trimmed}.`
|
|
117
|
+
: undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The classifications a materialized view's data can take: those a data
|
|
122
|
+
* product's access points offer; none when Studio's config gives none,
|
|
123
|
+
* and the classification is then typed
|
|
124
|
+
*/
|
|
125
|
+
get classifications(): string[] {
|
|
126
|
+
return (
|
|
127
|
+
this.editorStore.applicationStore.config.options.dataProductConfig
|
|
128
|
+
?.classifications ?? []
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Why the classification can't be the new materialized view's; none when it can */
|
|
133
|
+
getMaterializedViewClassificationProblem(
|
|
134
|
+
sensitivity: string,
|
|
135
|
+
): string | undefined {
|
|
136
|
+
const trimmed = sensitivity.trim();
|
|
137
|
+
if (!trimmed) {
|
|
138
|
+
return "Classify the materialized view's data.";
|
|
139
|
+
}
|
|
140
|
+
const { classifications } = this;
|
|
141
|
+
return classifications.length && !classifications.includes(trimmed)
|
|
142
|
+
? `${trimmed} isn't one of the data classifications.`
|
|
143
|
+
: undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The columns of the step that runs a materialized view's primary key can
|
|
148
|
+
* take: all but the milestoning columns a write mode adds
|
|
149
|
+
*/
|
|
150
|
+
getMaterializedViewKeyColumns(columns: readonly string[]): string[] {
|
|
151
|
+
return columns.filter((column) => !MILESTONING_COLUMNS.includes(column));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Adds the lambda to the ingest definition as a materialized view, with
|
|
156
|
+
* its primary key and the classification of its data, closes the cube and
|
|
157
|
+
* shows the definition with it
|
|
158
|
+
*/
|
|
159
|
+
addMaterializedView(
|
|
160
|
+
name: string,
|
|
161
|
+
lambda: PlainObject,
|
|
162
|
+
options: IngestMaterializedViewOptions,
|
|
163
|
+
): void {
|
|
164
|
+
const { ingest } = this;
|
|
165
|
+
if (
|
|
166
|
+
!ingest ||
|
|
167
|
+
this.getMaterializedViewNameProblem(name) !== undefined ||
|
|
168
|
+
this.getMaterializedViewClassificationProblem(options.sensitivity) !==
|
|
169
|
+
undefined
|
|
170
|
+
) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
ingestDefinition_addMaterializedView(ingest, name.trim(), lambda, {
|
|
174
|
+
primaryKey: options.primaryKey,
|
|
175
|
+
sensitivity: options.sensitivity.trim(),
|
|
176
|
+
});
|
|
177
|
+
this.closeOn(
|
|
178
|
+
ingest,
|
|
179
|
+
`Added materialized view ${name.trim()} to ${ingest.name}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Why Save MatView must ask again before it replaces the view opened to
|
|
185
|
+
* edit with the step that runs, given the step's columns (unknown, the key
|
|
186
|
+
* isn't checked): its data has no classification, as Studio wrote a view
|
|
187
|
+
* before it asked for one, or one that isn't among the classifications; or
|
|
188
|
+
* its primary key has a column the step no longer has, or a milestoning
|
|
189
|
+
* column. None when the view can be saved as it is
|
|
190
|
+
*/
|
|
191
|
+
getMaterializedViewSaveProblems(
|
|
192
|
+
columns: readonly string[] | undefined,
|
|
193
|
+
): string[] {
|
|
194
|
+
const view = this.matViewDataSet;
|
|
195
|
+
if (!view) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const problems: string[] = [];
|
|
199
|
+
const classificationProblem =
|
|
200
|
+
view.sensitivity === undefined
|
|
201
|
+
? `${view.name} has no classification of its data.`
|
|
202
|
+
: this.getMaterializedViewClassificationProblem(view.sensitivity);
|
|
203
|
+
if (classificationProblem !== undefined) {
|
|
204
|
+
problems.push(classificationProblem);
|
|
205
|
+
}
|
|
206
|
+
const milestoning = view.primaryKey.filter((column) =>
|
|
207
|
+
MILESTONING_COLUMNS.includes(column),
|
|
208
|
+
);
|
|
209
|
+
const gone = columns
|
|
210
|
+
? view.primaryKey.filter(
|
|
211
|
+
(column) =>
|
|
212
|
+
!columns.includes(column) && !milestoning.includes(column),
|
|
213
|
+
)
|
|
214
|
+
: [];
|
|
215
|
+
if (gone.length) {
|
|
216
|
+
problems.push(
|
|
217
|
+
gone.length === 1
|
|
218
|
+
? `Its primary key's ${gone.join(', ')} is no longer a column of the cube.`
|
|
219
|
+
: `Its primary key's ${gone.join(', ')} are no longer columns of the cube.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (milestoning.length) {
|
|
223
|
+
problems.push(
|
|
224
|
+
milestoning.length === 1
|
|
225
|
+
? `Its primary key's ${milestoning.join(', ')} is a milestoning column, which a key leaves out.`
|
|
226
|
+
: `Its primary key's ${milestoning.join(', ')} are milestoning columns, which a key leaves out.`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return problems;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Replaces the lambda of the materialized view opened to edit, in place
|
|
234
|
+
* (Save MatView), with the primary key and classification given, or its
|
|
235
|
+
* own; closes the cube and shows the definition with it. As a view added,
|
|
236
|
+
* a view saved has a classification of its data
|
|
237
|
+
*/
|
|
238
|
+
saveMaterializedView(
|
|
239
|
+
lambda: PlainObject,
|
|
240
|
+
options?: IngestMaterializedViewOptions,
|
|
241
|
+
): void {
|
|
242
|
+
const { ingest, matView, matViewDataSet } = this;
|
|
243
|
+
if (
|
|
244
|
+
!ingest ||
|
|
245
|
+
matView === undefined ||
|
|
246
|
+
!matViewDataSet ||
|
|
247
|
+
this.getMaterializedViewClassificationProblem(
|
|
248
|
+
(options ? options.sensitivity : matViewDataSet.sensitivity) ?? '',
|
|
249
|
+
) !== undefined
|
|
250
|
+
) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
ingestDefinition_updateMaterializedView(
|
|
254
|
+
ingest,
|
|
255
|
+
matView,
|
|
256
|
+
lambda,
|
|
257
|
+
options && {
|
|
258
|
+
primaryKey: options.primaryKey,
|
|
259
|
+
sensitivity: options.sensitivity.trim(),
|
|
260
|
+
},
|
|
261
|
+
);
|
|
262
|
+
this.closeOn(
|
|
263
|
+
ingest,
|
|
264
|
+
`Saved materialized view ${matView} of ${ingest.name}`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Closes the cube, then shows the definition's grammar with the change */
|
|
269
|
+
private closeOn(ingest: IngestDefinition, message: string): void {
|
|
270
|
+
this.close();
|
|
271
|
+
const editorState = this.editorStore.tabManagerState.tabs.find(
|
|
272
|
+
(tab): tab is IngestDefinitionEditorState =>
|
|
273
|
+
tab instanceof IngestDefinitionEditorState && tab.ingest === ingest,
|
|
274
|
+
);
|
|
275
|
+
if (editorState) {
|
|
276
|
+
flowResult(editorState.generateElementGrammar()).catch(
|
|
277
|
+
this.editorStore.applicationStore.alertUnhandledError,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
this.editorStore.applicationStore.notificationService.notifySuccess(
|
|
281
|
+
message,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026-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 {
|
|
18
|
+
dataProductAccessor,
|
|
19
|
+
lambda,
|
|
20
|
+
type IR,
|
|
21
|
+
type ModelContext,
|
|
22
|
+
type Schema,
|
|
23
|
+
} from '@finos/legend-cube';
|
|
24
|
+
import {
|
|
25
|
+
createCubeDataProductModel,
|
|
26
|
+
CubeAccessPoint,
|
|
27
|
+
CubeAccessPointGroup,
|
|
28
|
+
CubeDataProductCandidate,
|
|
29
|
+
CubeDataProductDescription,
|
|
30
|
+
CubeDataProductEnvironmentType,
|
|
31
|
+
CubeEngineError,
|
|
32
|
+
CubeEngineErrorKind,
|
|
33
|
+
type CubeAccessPointLocation,
|
|
34
|
+
type CubeDataProductCatalog,
|
|
35
|
+
type CubeDataProductProject,
|
|
36
|
+
type CubeEngine,
|
|
37
|
+
type NodeId,
|
|
38
|
+
} from '@finos/legend-cube-builder';
|
|
39
|
+
import { LakehouseAccessPoint, type DataProduct } from '@finos/legend-graph';
|
|
40
|
+
import type { EditorStore } from '../EditorStore.js';
|
|
41
|
+
import {
|
|
42
|
+
LEGEND_STUDIO_CUBE_WORKSPACE_VERSION,
|
|
43
|
+
type LegendStudioCubeWorkspace,
|
|
44
|
+
} from './LegendStudioCubeWorkspace.js';
|
|
45
|
+
|
|
46
|
+
// Legend Cube in Studio: a cube reads the data products of the project being
|
|
47
|
+
// edited, as the workspace holds them, never the platform's deployed ones.
|
|
48
|
+
// Their lakehouse access points are typed by Cube's engine on the workspace,
|
|
49
|
+
// as Studio types them for its own queries, and run on the project's
|
|
50
|
+
// lakehouse runtime.
|
|
51
|
+
|
|
52
|
+
/** The one class a Studio cube reads the project's data products as, so the tab offers no other */
|
|
53
|
+
const STUDIO_CUBE_DATA_PRODUCT_CLASS =
|
|
54
|
+
CubeDataProductEnvironmentType.PRODUCTION;
|
|
55
|
+
|
|
56
|
+
/** The deployment a workspace data product is picked from: none yet */
|
|
57
|
+
const NO_DEPLOYMENT_ID = '0';
|
|
58
|
+
|
|
59
|
+
export const LEGEND_STUDIO_CUBE_DATA_PRODUCT_MESSAGE = {
|
|
60
|
+
GONE: (dataProduct: string): string =>
|
|
61
|
+
`The project has no data product ${dataProduct} any more.`,
|
|
62
|
+
NOT_LAKEHOUSE: 'Cube reads lakehouse access points only',
|
|
63
|
+
NO_ACCESS_POINT: (accessPoint: string, dataProduct: string): string =>
|
|
64
|
+
`Data product ${dataProduct} has no lakehouse access point "${accessPoint}".`,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const keyOf = (group: string, accessPoint: string): string =>
|
|
68
|
+
`${group}\u0000${accessPoint}`;
|
|
69
|
+
|
|
70
|
+
/** The lambda that types an access point: `{| #P{dataProduct.accessPoint}#}` */
|
|
71
|
+
const typingLambdaOf = (dataProduct: string, accessPoint: string): IR =>
|
|
72
|
+
lambda([], [dataProductAccessor([dataProduct, accessPoint])]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The project's own data products, for the Data Product tab and for
|
|
76
|
+
* re-checking saved sources. Their access points are typed by the engine, a
|
|
77
|
+
* product's in one call, on the workspace
|
|
78
|
+
*/
|
|
79
|
+
export class LegendStudioCubeDataProductCatalog
|
|
80
|
+
implements CubeDataProductCatalog
|
|
81
|
+
{
|
|
82
|
+
readonly environmentTypes = [STUDIO_CUBE_DATA_PRODUCT_CLASS];
|
|
83
|
+
readonly editorStore: EditorStore;
|
|
84
|
+
readonly workspace: LegendStudioCubeWorkspace;
|
|
85
|
+
private readonly engine: CubeEngine;
|
|
86
|
+
|
|
87
|
+
constructor(
|
|
88
|
+
editorStore: EditorStore,
|
|
89
|
+
workspace: LegendStudioCubeWorkspace,
|
|
90
|
+
engine: CubeEngine,
|
|
91
|
+
) {
|
|
92
|
+
this.editorStore = editorStore;
|
|
93
|
+
this.workspace = workspace;
|
|
94
|
+
this.engine = engine;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The project's own data products, not its dependencies' */
|
|
98
|
+
private get dataProducts(): DataProduct[] {
|
|
99
|
+
return this.editorStore.graphManagerState.graph.ownDataProducts;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The model a cube of the workspace's data products names */
|
|
103
|
+
private get model(): ModelContext {
|
|
104
|
+
return createCubeDataProductModel({
|
|
105
|
+
...this.workspace.project,
|
|
106
|
+
versionId: LEGEND_STUDIO_CUBE_WORKSPACE_VERSION,
|
|
107
|
+
environmentType: STUDIO_CUBE_DATA_PRODUCT_CLASS,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private candidateOf(dataProduct: DataProduct): CubeDataProductCandidate {
|
|
112
|
+
return new CubeDataProductCandidate({
|
|
113
|
+
id: dataProduct.path,
|
|
114
|
+
deploymentId: NO_DEPLOYMENT_ID,
|
|
115
|
+
dataProductPath: dataProduct.path,
|
|
116
|
+
title: dataProduct.title ?? dataProduct.name,
|
|
117
|
+
description: dataProduct.description,
|
|
118
|
+
...this.workspace.project,
|
|
119
|
+
versionId: LEGEND_STUDIO_CUBE_WORKSPACE_VERSION,
|
|
120
|
+
environmentType: STUDIO_CUBE_DATA_PRODUCT_CLASS,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private dataProductOf(path: string): DataProduct {
|
|
125
|
+
const dataProduct = this.dataProducts.find((each) => each.path === path);
|
|
126
|
+
if (!dataProduct) {
|
|
127
|
+
throw new CubeEngineError(
|
|
128
|
+
CubeEngineErrorKind.COMPILE,
|
|
129
|
+
LEGEND_STUDIO_CUBE_DATA_PRODUCT_MESSAGE.GONE(path),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return dataProduct;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Every data product of the project; the tab matches the text itself */
|
|
136
|
+
async search(): Promise<readonly CubeDataProductCandidate[]> {
|
|
137
|
+
return this.dataProducts.map((each) => this.candidateOf(each));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A product's access point groups, its lakehouse access points typed on the workspace */
|
|
141
|
+
async describe(
|
|
142
|
+
candidate: CubeDataProductCandidate,
|
|
143
|
+
): Promise<CubeDataProductDescription> {
|
|
144
|
+
const dataProduct = this.dataProductOf(candidate.dataProductPath);
|
|
145
|
+
const lambdas = new Map<NodeId, IR>();
|
|
146
|
+
dataProduct.accessPointGroups.forEach((group) =>
|
|
147
|
+
group.accessPoints
|
|
148
|
+
.filter((point) => point instanceof LakehouseAccessPoint)
|
|
149
|
+
.forEach((point) =>
|
|
150
|
+
lambdas.set(
|
|
151
|
+
keyOf(group.id, point.id),
|
|
152
|
+
typingLambdaOf(dataProduct.path, point.id),
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
const schemas = lambdas.size
|
|
157
|
+
? await this.engine.typeLambdas(this.model, lambdas)
|
|
158
|
+
: new Map<NodeId, Schema | CubeEngineError>();
|
|
159
|
+
return new CubeDataProductDescription(
|
|
160
|
+
candidate,
|
|
161
|
+
dataProduct.accessPointGroups
|
|
162
|
+
.filter((group) => group.accessPoints.length > 0)
|
|
163
|
+
.map(
|
|
164
|
+
(group) =>
|
|
165
|
+
new CubeAccessPointGroup({
|
|
166
|
+
id: group.id,
|
|
167
|
+
title: group.title,
|
|
168
|
+
accessPoints: group.accessPoints.map((point) => {
|
|
169
|
+
const typed = schemas.get(keyOf(group.id, point.id));
|
|
170
|
+
return new CubeAccessPoint({
|
|
171
|
+
id: point.id,
|
|
172
|
+
title: point.title,
|
|
173
|
+
description: point.description,
|
|
174
|
+
schema: typed instanceof CubeEngineError ? undefined : typed,
|
|
175
|
+
disabledReason:
|
|
176
|
+
point instanceof LakehouseAccessPoint
|
|
177
|
+
? typed instanceof CubeEngineError
|
|
178
|
+
? typed.firstLine
|
|
179
|
+
: undefined
|
|
180
|
+
: LEGEND_STUDIO_CUBE_DATA_PRODUCT_MESSAGE.NOT_LAKEHOUSE,
|
|
181
|
+
});
|
|
182
|
+
}),
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The saved sources' schemas, typed on the workspace in one call */
|
|
189
|
+
async resolveSchemas(
|
|
190
|
+
project: CubeDataProductProject,
|
|
191
|
+
sources: ReadonlyMap<NodeId, CubeAccessPointLocation>,
|
|
192
|
+
): Promise<Map<NodeId, Schema | CubeEngineError>> {
|
|
193
|
+
const resolved = new Map<NodeId, Schema | CubeEngineError>();
|
|
194
|
+
const lambdas = new Map<NodeId, IR>();
|
|
195
|
+
sources.forEach((source, nodeId) => {
|
|
196
|
+
const point = this.dataProducts
|
|
197
|
+
.find((each) => each.path === source.dataProduct)
|
|
198
|
+
?.accessPointGroups.find(
|
|
199
|
+
(group) => group.id === source.accessPointGroup,
|
|
200
|
+
)
|
|
201
|
+
?.accessPoints.find((each) => each.id === source.accessPoint);
|
|
202
|
+
if (point instanceof LakehouseAccessPoint) {
|
|
203
|
+
lambdas.set(
|
|
204
|
+
nodeId,
|
|
205
|
+
typingLambdaOf(source.dataProduct, source.accessPoint),
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
resolved.set(
|
|
209
|
+
nodeId,
|
|
210
|
+
new CubeEngineError(
|
|
211
|
+
CubeEngineErrorKind.COMPILE,
|
|
212
|
+
LEGEND_STUDIO_CUBE_DATA_PRODUCT_MESSAGE.NO_ACCESS_POINT(
|
|
213
|
+
source.accessPoint,
|
|
214
|
+
source.dataProduct,
|
|
215
|
+
),
|
|
216
|
+
nodeId,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
if (lambdas.size) {
|
|
222
|
+
(
|
|
223
|
+
await this.engine.typeLambdas(
|
|
224
|
+
createCubeDataProductModel(project),
|
|
225
|
+
lambdas,
|
|
226
|
+
)
|
|
227
|
+
).forEach((schema, nodeId) => resolved.set(nodeId, schema));
|
|
228
|
+
}
|
|
229
|
+
// in the order the sources were given
|
|
230
|
+
return new Map(
|
|
231
|
+
[...sources.keys()].map((nodeId) => [
|
|
232
|
+
nodeId,
|
|
233
|
+
resolved.get(nodeId) as Schema | CubeEngineError,
|
|
234
|
+
]),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** No marketplace: a workspace data product has no page there yet */
|
|
239
|
+
getMarketplaceLink(): string | undefined {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026-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 {
|
|
18
|
+
buildCubeEngine,
|
|
19
|
+
LocalModelCatalog,
|
|
20
|
+
type CubeEngine,
|
|
21
|
+
type CubeHost,
|
|
22
|
+
} from '@finos/legend-cube-builder';
|
|
23
|
+
import type { LegendStudioApplicationStore } from '../../LegendStudioBaseStore.js';
|
|
24
|
+
import type { EditorStore } from '../EditorStore.js';
|
|
25
|
+
import { LegendStudioCubeDataProductCatalog } from './LegendStudioCubeDataProductCatalog.js';
|
|
26
|
+
import { LegendStudioCubeIngestCatalog } from './LegendStudioCubeIngestCatalog.js';
|
|
27
|
+
import { LegendStudioCubeWorkspace } from './LegendStudioCubeWorkspace.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Studio as the host of Legend Cube: Studio's engine and application store,
|
|
31
|
+
* and the project's own ingest definitions and data products as the only
|
|
32
|
+
* sources, typed and run on the workspace and the project's lakehouse
|
|
33
|
+
* runtime. No sample data, examples, databases or services. One per opening
|
|
34
|
+
* of the cube.
|
|
35
|
+
*/
|
|
36
|
+
export class LegendStudioCubeHost implements CubeHost {
|
|
37
|
+
readonly applicationStore: LegendStudioApplicationStore;
|
|
38
|
+
readonly engine: CubeEngine;
|
|
39
|
+
readonly modelCatalog: LocalModelCatalog;
|
|
40
|
+
readonly project: LegendStudioCubeWorkspace;
|
|
41
|
+
readonly ingestCatalog: LegendStudioCubeIngestCatalog;
|
|
42
|
+
readonly dataProductCatalog: LegendStudioCubeDataProductCatalog;
|
|
43
|
+
readonly offersSampleData = false;
|
|
44
|
+
|
|
45
|
+
constructor(editorStore: EditorStore) {
|
|
46
|
+
const { applicationStore } = editorStore;
|
|
47
|
+
const { config } = applicationStore;
|
|
48
|
+
this.applicationStore = applicationStore;
|
|
49
|
+
this.project = new LegendStudioCubeWorkspace(editorStore);
|
|
50
|
+
this.ingestCatalog = new LegendStudioCubeIngestCatalog(
|
|
51
|
+
editorStore,
|
|
52
|
+
this.project,
|
|
53
|
+
);
|
|
54
|
+
// the engine Studio's own editor uses, configured the same way; a run
|
|
55
|
+
// reads the workspace, and names the project's lakehouse environment
|
|
56
|
+
this.engine = buildCubeEngine(
|
|
57
|
+
{
|
|
58
|
+
baseUrl:
|
|
59
|
+
editorStore.editorConfig?.engineServerUrl ?? config.engineServerUrl,
|
|
60
|
+
queryBaseUrl:
|
|
61
|
+
editorStore.editorConfig?.engineQueryServerUrl ??
|
|
62
|
+
config.engineQueryServerUrl,
|
|
63
|
+
queryClientName: config.engineQueryClientName,
|
|
64
|
+
enableCompression: true,
|
|
65
|
+
useCookieAuthOnly: config.engineUseCookieAuthOnly,
|
|
66
|
+
},
|
|
67
|
+
applicationStore.tracerService,
|
|
68
|
+
{
|
|
69
|
+
ingestCatalog: this.ingestCatalog,
|
|
70
|
+
ingestDefinitionSource: this.ingestCatalog,
|
|
71
|
+
lakehouseEnvironment: this.project,
|
|
72
|
+
dataProductModelSource: this.project,
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
this.dataProductCatalog = new LegendStudioCubeDataProductCatalog(
|
|
76
|
+
editorStore,
|
|
77
|
+
this.project,
|
|
78
|
+
this.engine,
|
|
79
|
+
);
|
|
80
|
+
this.modelCatalog = new LocalModelCatalog(this.engine);
|
|
81
|
+
}
|
|
82
|
+
}
|