@finos/legend-cube-builder 0.0.5 → 0.0.6

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 (245) hide show
  1. package/docs/adding-an-operation.md +4 -2
  2. package/docs/demo/README.md +1 -1
  3. package/docs/hosting.md +40 -9
  4. package/lib/__lib__/LegendCubeHelpText.d.ts.map +1 -1
  5. package/lib/__lib__/LegendCubeHelpText.js +3 -1
  6. package/lib/__lib__/LegendCubeHelpText.js.map +1 -1
  7. package/lib/__lib__/LegendCubeLabels.d.ts +34 -5
  8. package/lib/__lib__/LegendCubeLabels.d.ts.map +1 -1
  9. package/lib/__lib__/LegendCubeLabels.js +45 -8
  10. package/lib/__lib__/LegendCubeLabels.js.map +1 -1
  11. package/lib/__lib__/LegendCubeNavigation.d.ts +23 -0
  12. package/lib/__lib__/LegendCubeNavigation.d.ts.map +1 -0
  13. package/lib/__lib__/LegendCubeNavigation.js +33 -0
  14. package/lib/__lib__/LegendCubeNavigation.js.map +1 -0
  15. package/lib/__lib__/LegendCubeTesting.d.ts +3 -1
  16. package/lib/__lib__/LegendCubeTesting.d.ts.map +1 -1
  17. package/lib/__lib__/LegendCubeTesting.js +2 -0
  18. package/lib/__lib__/LegendCubeTesting.js.map +1 -1
  19. package/lib/__lib__/LegendCubeTour.d.ts +62 -0
  20. package/lib/__lib__/LegendCubeTour.d.ts.map +1 -0
  21. package/lib/__lib__/LegendCubeTour.js +149 -0
  22. package/lib/__lib__/LegendCubeTour.js.map +1 -0
  23. package/lib/__test-utils__/CubeExampleTestUtils.d.ts +34 -0
  24. package/lib/__test-utils__/CubeExampleTestUtils.d.ts.map +1 -0
  25. package/lib/__test-utils__/CubeExampleTestUtils.js +72 -0
  26. package/lib/__test-utils__/CubeExampleTestUtils.js.map +1 -0
  27. package/lib/__test-utils__/CubeVariantTestUtils.d.ts +16 -0
  28. package/lib/__test-utils__/CubeVariantTestUtils.d.ts.map +1 -0
  29. package/lib/__test-utils__/CubeVariantTestUtils.js +66 -0
  30. package/lib/__test-utils__/CubeVariantTestUtils.js.map +1 -0
  31. package/lib/components/CubeAddItems.d.ts.map +1 -1
  32. package/lib/components/CubeAddItems.js +2 -1
  33. package/lib/components/CubeAddItems.js.map +1 -1
  34. package/lib/components/CubeEditor.d.ts.map +1 -1
  35. package/lib/components/CubeEditor.js +11 -5
  36. package/lib/components/CubeEditor.js.map +1 -1
  37. package/lib/components/CubeFileMenu.d.ts.map +1 -1
  38. package/lib/components/CubeFileMenu.js +2 -1
  39. package/lib/components/CubeFileMenu.js.map +1 -1
  40. package/lib/components/CubeNodeIcon.d.ts.map +1 -1
  41. package/lib/components/CubeNodeIcon.js +2 -1
  42. package/lib/components/CubeNodeIcon.js.map +1 -1
  43. package/lib/components/canvas/CubeCanvas.d.ts.map +1 -1
  44. package/lib/components/canvas/CubeCanvas.js +3 -2
  45. package/lib/components/canvas/CubeCanvas.js.map +1 -1
  46. package/lib/components/canvas/CubeCanvasNode.d.ts.map +1 -1
  47. package/lib/components/canvas/CubeCanvasNode.js +4 -1
  48. package/lib/components/canvas/CubeCanvasNode.js.map +1 -1
  49. package/lib/components/dev-settings/CubeDevSettings.d.ts +2 -2
  50. package/lib/components/dev-settings/CubeDevSettings.d.ts.map +1 -1
  51. package/lib/components/dev-settings/CubeDevSettings.js +12 -8
  52. package/lib/components/dev-settings/CubeDevSettings.js.map +1 -1
  53. package/lib/components/editors/CubeColumnPicker.d.ts.map +1 -1
  54. package/lib/components/editors/CubeColumnPicker.js +3 -1
  55. package/lib/components/editors/CubeColumnPicker.js.map +1 -1
  56. package/lib/components/editors/CubeExplodeEditor.d.ts +26 -0
  57. package/lib/components/editors/CubeExplodeEditor.d.ts.map +1 -0
  58. package/lib/components/editors/CubeExplodeEditor.js +61 -0
  59. package/lib/components/editors/CubeExplodeEditor.js.map +1 -0
  60. package/lib/components/editors/CubeExpressionFilterEditor.d.ts.map +1 -1
  61. package/lib/components/editors/CubeExpressionFilterEditor.js +8 -5
  62. package/lib/components/editors/CubeExpressionFilterEditor.js.map +1 -1
  63. package/lib/components/editors/CubeExtendEditor.d.ts +0 -2
  64. package/lib/components/editors/CubeExtendEditor.d.ts.map +1 -1
  65. package/lib/components/editors/CubeExtendEditor.js +12 -11
  66. package/lib/components/editors/CubeExtendEditor.js.map +1 -1
  67. package/lib/components/editors/CubeJsonFieldsTree.d.ts +46 -0
  68. package/lib/components/editors/CubeJsonFieldsTree.d.ts.map +1 -0
  69. package/lib/components/editors/CubeJsonFieldsTree.js +93 -0
  70. package/lib/components/editors/CubeJsonFieldsTree.js.map +1 -0
  71. package/lib/components/editors/CubeNodeEditorRegistry.d.ts.map +1 -1
  72. package/lib/components/editors/CubeNodeEditorRegistry.js +3 -1
  73. package/lib/components/editors/CubeNodeEditorRegistry.js.map +1 -1
  74. package/lib/components/grid/CubeGridRegion.d.ts.map +1 -1
  75. package/lib/components/grid/CubeGridRegion.js +5 -4
  76. package/lib/components/grid/CubeGridRegion.js.map +1 -1
  77. package/lib/components/grid/CubeGridValues.d.ts +5 -3
  78. package/lib/components/grid/CubeGridValues.d.ts.map +1 -1
  79. package/lib/components/grid/CubeGridValues.js +8 -3
  80. package/lib/components/grid/CubeGridValues.js.map +1 -1
  81. package/lib/components/grid/CubeJsonExplorerDialog.d.ts +27 -0
  82. package/lib/components/grid/CubeJsonExplorerDialog.d.ts.map +1 -0
  83. package/lib/components/grid/CubeJsonExplorerDialog.js +102 -0
  84. package/lib/components/grid/CubeJsonExplorerDialog.js.map +1 -0
  85. package/lib/components/grid/CubeJsonViewerDialog.d.ts +27 -0
  86. package/lib/components/grid/CubeJsonViewerDialog.d.ts.map +1 -0
  87. package/lib/components/grid/CubeJsonViewerDialog.js +68 -0
  88. package/lib/components/grid/CubeJsonViewerDialog.js.map +1 -0
  89. package/lib/components/grid/CubeResultGrid.d.ts +15 -3
  90. package/lib/components/grid/CubeResultGrid.d.ts.map +1 -1
  91. package/lib/components/grid/CubeResultGrid.js +95 -3
  92. package/lib/components/grid/CubeResultGrid.js.map +1 -1
  93. package/lib/components/grid/CubeServerSideGrid.d.ts.map +1 -1
  94. package/lib/components/grid/CubeServerSideGrid.js +4 -2
  95. package/lib/components/grid/CubeServerSideGrid.js.map +1 -1
  96. package/lib/components/palette/CubePalette.d.ts.map +1 -1
  97. package/lib/components/palette/CubePalette.js +4 -1
  98. package/lib/components/palette/CubePalette.js.map +1 -1
  99. package/lib/components/source-picker/CubeIngestTab.d.ts +2 -2
  100. package/lib/components/source-picker/CubeIngestTab.d.ts.map +1 -1
  101. package/lib/components/source-picker/CubeIngestTab.js +19 -13
  102. package/lib/components/source-picker/CubeIngestTab.js.map +1 -1
  103. package/lib/components/tour/CubeTour.d.ts +36 -0
  104. package/lib/components/tour/CubeTour.d.ts.map +1 -0
  105. package/lib/components/tour/CubeTour.js +183 -0
  106. package/lib/components/tour/CubeTour.js.map +1 -0
  107. package/lib/index.css +1 -1
  108. package/lib/index.d.ts +1 -0
  109. package/lib/index.d.ts.map +1 -1
  110. package/lib/index.js +1 -0
  111. package/lib/index.js.map +1 -1
  112. package/lib/stores/CubeEditorState.d.ts +17 -1
  113. package/lib/stores/CubeEditorState.d.ts.map +1 -1
  114. package/lib/stores/CubeEditorState.js +35 -5
  115. package/lib/stores/CubeEditorState.js.map +1 -1
  116. package/lib/stores/CubeExamples.d.ts.map +1 -1
  117. package/lib/stores/CubeExamples.js +101 -4
  118. package/lib/stores/CubeExamples.js.map +1 -1
  119. package/lib/stores/CubeExamplesState.d.ts.map +1 -1
  120. package/lib/stores/CubeExamplesState.js +4 -1
  121. package/lib/stores/CubeExamplesState.js.map +1 -1
  122. package/lib/stores/CubeGridQuickActions.d.ts +7 -0
  123. package/lib/stores/CubeGridQuickActions.d.ts.map +1 -1
  124. package/lib/stores/CubeGridQuickActions.js +20 -11
  125. package/lib/stores/CubeGridQuickActions.js.map +1 -1
  126. package/lib/stores/CubePureText.d.ts +20 -0
  127. package/lib/stores/CubePureText.d.ts.map +1 -0
  128. package/lib/stores/CubePureText.js +24 -0
  129. package/lib/stores/CubePureText.js.map +1 -0
  130. package/lib/stores/CubeTourState.d.ts +49 -0
  131. package/lib/stores/CubeTourState.d.ts.map +1 -0
  132. package/lib/stores/CubeTourState.js +92 -0
  133. package/lib/stores/CubeTourState.js.map +1 -0
  134. package/lib/stores/LegendCubeHost.d.ts +88 -0
  135. package/lib/stores/LegendCubeHost.d.ts.map +1 -0
  136. package/lib/stores/LegendCubeHost.js +149 -0
  137. package/lib/stores/LegendCubeHost.js.map +1 -0
  138. package/lib/stores/editors/CubeExplodeDraft.d.ts +87 -0
  139. package/lib/stores/editors/CubeExplodeDraft.d.ts.map +1 -0
  140. package/lib/stores/editors/CubeExplodeDraft.js +221 -0
  141. package/lib/stores/editors/CubeExplodeDraft.js.map +1 -0
  142. package/lib/stores/editors/CubeExpressionFilterDraft.d.ts +3 -0
  143. package/lib/stores/editors/CubeExpressionFilterDraft.d.ts.map +1 -1
  144. package/lib/stores/editors/CubeExpressionFilterDraft.js +4 -0
  145. package/lib/stores/editors/CubeExpressionFilterDraft.js.map +1 -1
  146. package/lib/stores/editors/CubeExtendDraft.d.ts +9 -0
  147. package/lib/stores/editors/CubeExtendDraft.d.ts.map +1 -1
  148. package/lib/stores/editors/CubeExtendDraft.js +26 -0
  149. package/lib/stores/editors/CubeExtendDraft.js.map +1 -1
  150. package/lib/stores/editors/CubeNodeDraftRegistry.d.ts.map +1 -1
  151. package/lib/stores/editors/CubeNodeDraftRegistry.js +6 -1
  152. package/lib/stores/editors/CubeNodeDraftRegistry.js.map +1 -1
  153. package/lib/stores/fixtures/CubeSalesDuckDbModel.d.ts +1 -0
  154. package/lib/stores/fixtures/CubeSalesDuckDbModel.d.ts.map +1 -1
  155. package/lib/stores/fixtures/CubeSalesDuckDbModel.js +8 -2
  156. package/lib/stores/fixtures/CubeSalesDuckDbModel.js.map +1 -1
  157. package/lib/stores/source-picker/CubeIngestTabState.d.ts +48 -14
  158. package/lib/stores/source-picker/CubeIngestTabState.d.ts.map +1 -1
  159. package/lib/stores/source-picker/CubeIngestTabState.js +159 -62
  160. package/lib/stores/source-picker/CubeIngestTabState.js.map +1 -1
  161. package/lib/stores/variant/CubeJson.d.ts +48 -0
  162. package/lib/stores/variant/CubeJson.d.ts.map +1 -0
  163. package/lib/stores/variant/CubeJson.js +207 -0
  164. package/lib/stores/variant/CubeJson.js.map +1 -0
  165. package/lib/stores/variant/CubeJsonExplorerState.d.ts +88 -0
  166. package/lib/stores/variant/CubeJsonExplorerState.d.ts.map +1 -0
  167. package/lib/stores/variant/CubeJsonExplorerState.js +297 -0
  168. package/lib/stores/variant/CubeJsonExplorerState.js.map +1 -0
  169. package/lib/stores/variant/CubeJsonFieldsState.d.ts +83 -0
  170. package/lib/stores/variant/CubeJsonFieldsState.d.ts.map +1 -0
  171. package/lib/stores/variant/CubeJsonFieldsState.js +207 -0
  172. package/lib/stores/variant/CubeJsonFieldsState.js.map +1 -0
  173. package/lib/stores/variant/CubeJsonOffers.d.ts +76 -0
  174. package/lib/stores/variant/CubeJsonOffers.d.ts.map +1 -0
  175. package/lib/stores/variant/CubeJsonOffers.js +250 -0
  176. package/lib/stores/variant/CubeJsonOffers.js.map +1 -0
  177. package/lib/stores/variant/CubeJsonParse.d.ts +28 -0
  178. package/lib/stores/variant/CubeJsonParse.d.ts.map +1 -0
  179. package/lib/stores/variant/CubeJsonParse.js +53 -0
  180. package/lib/stores/variant/CubeJsonParse.js.map +1 -0
  181. package/lib/stores/variant/CubeJsonSampling.d.ts +41 -0
  182. package/lib/stores/variant/CubeJsonSampling.d.ts.map +1 -0
  183. package/lib/stores/variant/CubeJsonSampling.js +59 -0
  184. package/lib/stores/variant/CubeJsonSampling.js.map +1 -0
  185. package/lib/stores/variant/CubeJsonShape.d.ts +87 -0
  186. package/lib/stores/variant/CubeJsonShape.d.ts.map +1 -0
  187. package/lib/stores/variant/CubeJsonShape.js +209 -0
  188. package/lib/stores/variant/CubeJsonShape.js.map +1 -0
  189. package/lib/stores/variant/CubeJsonViewerState.d.ts +33 -0
  190. package/lib/stores/variant/CubeJsonViewerState.d.ts.map +1 -0
  191. package/lib/stores/variant/CubeJsonViewerState.js +53 -0
  192. package/lib/stores/variant/CubeJsonViewerState.js.map +1 -0
  193. package/package.json +2 -2
  194. package/src/__lib__/LegendCubeHelpText.ts +4 -0
  195. package/src/__lib__/LegendCubeLabels.ts +60 -8
  196. package/src/__lib__/LegendCubeNavigation.ts +48 -0
  197. package/src/__lib__/LegendCubeTesting.ts +2 -0
  198. package/src/__lib__/LegendCubeTour.ts +173 -0
  199. package/src/__test-utils__/CubeExampleTestUtils.ts +119 -0
  200. package/src/__test-utils__/CubeVariantTestUtils.ts +103 -0
  201. package/src/components/CubeAddItems.tsx +2 -0
  202. package/src/components/CubeEditor.tsx +15 -0
  203. package/src/components/CubeFileMenu.tsx +2 -0
  204. package/src/components/CubeNodeIcon.tsx +2 -0
  205. package/src/components/canvas/CubeCanvas.tsx +17 -1
  206. package/src/components/canvas/CubeCanvasNode.tsx +7 -0
  207. package/src/components/dev-settings/CubeDevSettings.tsx +17 -2
  208. package/src/components/editors/CubeColumnPicker.tsx +3 -0
  209. package/src/components/editors/CubeExplodeEditor.tsx +207 -0
  210. package/src/components/editors/CubeExpressionFilterEditor.tsx +25 -8
  211. package/src/components/editors/CubeExtendEditor.tsx +32 -19
  212. package/src/components/editors/CubeJsonFieldsTree.tsx +316 -0
  213. package/src/components/editors/CubeNodeEditorRegistry.ts +3 -0
  214. package/src/components/grid/CubeGridRegion.tsx +21 -3
  215. package/src/components/grid/CubeGridValues.ts +8 -3
  216. package/src/components/grid/CubeJsonExplorerDialog.tsx +304 -0
  217. package/src/components/grid/CubeJsonViewerDialog.tsx +190 -0
  218. package/src/components/grid/CubeResultGrid.tsx +144 -2
  219. package/src/components/grid/CubeServerSideGrid.tsx +13 -0
  220. package/src/components/palette/CubePalette.tsx +9 -0
  221. package/src/components/source-picker/CubeIngestTab.tsx +62 -30
  222. package/src/components/tour/CubeTour.tsx +317 -0
  223. package/src/index.ts +7 -0
  224. package/src/stores/CubeEditorState.ts +36 -5
  225. package/src/stores/CubeExamples.ts +120 -2
  226. package/src/stores/CubeExamplesState.ts +9 -1
  227. package/src/stores/CubeGridQuickActions.ts +23 -12
  228. package/src/stores/CubePureText.ts +28 -0
  229. package/src/stores/CubeTourState.ts +116 -0
  230. package/src/stores/LegendCubeHost.ts +243 -0
  231. package/src/stores/editors/CubeExplodeDraft.ts +272 -0
  232. package/src/stores/editors/CubeExpressionFilterDraft.ts +4 -0
  233. package/src/stores/editors/CubeExtendDraft.ts +29 -0
  234. package/src/stores/editors/CubeNodeDraftRegistry.ts +7 -0
  235. package/src/stores/fixtures/CubeSalesDuckDbModel.ts +8 -2
  236. package/src/stores/source-picker/CubeIngestTabState.ts +253 -102
  237. package/src/stores/variant/CubeJson.ts +229 -0
  238. package/src/stores/variant/CubeJsonExplorerState.ts +378 -0
  239. package/src/stores/variant/CubeJsonFieldsState.ts +252 -0
  240. package/src/stores/variant/CubeJsonOffers.ts +359 -0
  241. package/src/stores/variant/CubeJsonParse.ts +75 -0
  242. package/src/stores/variant/CubeJsonSampling.ts +90 -0
  243. package/src/stores/variant/CubeJsonShape.ts +279 -0
  244. package/src/stores/variant/CubeJsonViewerState.ts +59 -0
  245. package/tsconfig.json +22 -1
@@ -0,0 +1,252 @@
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
+ getVariantDatabaseProblem,
19
+ type Schema,
20
+ type SchemaColumn,
21
+ } from '@finos/legend-cube';
22
+ import { assertErrorThrown, type GeneratorFn } from '@finos/legend-shared';
23
+ import {
24
+ action,
25
+ computed,
26
+ flow,
27
+ flowResult,
28
+ makeObservable,
29
+ observable,
30
+ } from 'mobx';
31
+ import { CubeEngineError } from '../../graph-manager/CubeEngine.js';
32
+ import type { CubeEditorState } from '../CubeEditorState.js';
33
+ import { CubeQueryRunAborted } from '../CubeQueryRun.js';
34
+ import {
35
+ type CubeJsonField,
36
+ filterCubeJsonFields,
37
+ getCubeJsonFields,
38
+ } from './CubeJsonOffers.js';
39
+ import { readCubeJsonSample } from './CubeJsonSampling.js';
40
+ import type { CubeJsonSample } from './CubeJsonShape.js';
41
+
42
+ /** A field's path as one key */
43
+ const pathKey = (path: readonly string[]): string => JSON.stringify(path);
44
+
45
+ /**
46
+ * A Variant input column's fields in an expression editor, Extend's or
47
+ * Filter by Expression's (PLAN §11.20, V.9): opened from the column's chip,
48
+ * read from a sample of the column just before the node being edited, so no
49
+ * run of the cube is needed and stale rows don't matter. The sample is read
50
+ * as Explore JSON reads one (listed in Runs, and it can be stopped); a field
51
+ * clicked writes its expression into the editor's.
52
+ */
53
+ export class CubeJsonFieldsState {
54
+ readonly editorState: CubeEditorState;
55
+ /** The node being edited, whose input is read */
56
+ readonly nodeId: string;
57
+
58
+ /** The column whose fields are open; `undefined` while closed */
59
+ column: SchemaColumn | undefined;
60
+ /** What was read, and of which column */
61
+ sample:
62
+ | { readonly column: string; readonly sample: CubeJsonSample }
63
+ | undefined;
64
+ isReading = false;
65
+ /** Why the column couldn't be read */
66
+ error: string | undefined;
67
+ search = '';
68
+ /** The fields folded, by path; every field starts open */
69
+ readonly folded = observable.set<string>();
70
+ /** The field whose other forms show, by path */
71
+ formsOf: string | undefined;
72
+
73
+ private controller: AbortController | undefined;
74
+
75
+ constructor(editorState: CubeEditorState, nodeId: string) {
76
+ makeObservable(this, {
77
+ column: observable.ref,
78
+ sample: observable.ref,
79
+ isReading: observable,
80
+ error: observable,
81
+ search: observable,
82
+ formsOf: observable,
83
+ input: computed,
84
+ databaseProblem: computed,
85
+ fields: computed,
86
+ shownFields: computed,
87
+ toggle: flow,
88
+ read: flow,
89
+ stop: action,
90
+ close: action,
91
+ setSearch: action,
92
+ toggleFolded: action,
93
+ toggleForms: action,
94
+ });
95
+ this.editorState = editorState;
96
+ this.nodeId = nodeId;
97
+ }
98
+
99
+ /** The node's input: its id and schema, once known */
100
+ get input(): { id: string; schema: Schema } | undefined {
101
+ const [id] = this.editorState.document.query.getInputIds(this.nodeId);
102
+ const schema =
103
+ id === undefined ? undefined : this.editorState.analysis.schemas.get(id);
104
+ return id === undefined || !schema ? undefined : { id, schema };
105
+ }
106
+
107
+ /**
108
+ * Why the fields can't be read: the database the node's input runs on has
109
+ * no Variant functions; `undefined` when they can, or it isn't known yet
110
+ */
111
+ get databaseProblem(): string | undefined {
112
+ const { editorState, input } = this;
113
+ const { context, query } = editorState.document;
114
+ return input === undefined || context?.runtime === undefined
115
+ ? undefined
116
+ : getVariantDatabaseProblem(
117
+ editorState.getRunDatabaseType(
118
+ query,
119
+ input.id,
120
+ context.model,
121
+ context.runtime,
122
+ ),
123
+ );
124
+ }
125
+
126
+ /** The open column's fields, from what was read of it */
127
+ get fields(): CubeJsonField | undefined {
128
+ const { column, sample } = this;
129
+ return column && sample?.column === column.name
130
+ ? getCubeJsonFields(column.name, sample.sample)
131
+ : undefined;
132
+ }
133
+
134
+ /** The fields matching the search, with the fields they are in */
135
+ get shownFields(): CubeJsonField | undefined {
136
+ return this.fields && filterCubeJsonFields(this.fields, this.search);
137
+ }
138
+
139
+ isOpen(column: string): boolean {
140
+ return this.column?.name === column;
141
+ }
142
+
143
+ isFolded(path: readonly string[]): boolean {
144
+ return this.folded.has(pathKey(path));
145
+ }
146
+
147
+ areFormsShown(path: readonly string[]): boolean {
148
+ return this.formsOf === pathKey(path);
149
+ }
150
+
151
+ /**
152
+ * Opens a column's fields, reading its first values unless they were read
153
+ * already, or closes them if open. The model's databases are loaded first:
154
+ * a database without JSON functions is refused, with the reason.
155
+ */
156
+ *toggle(column: SchemaColumn): GeneratorFn<void> {
157
+ if (this.isOpen(column.name)) {
158
+ this.close();
159
+ return;
160
+ }
161
+ this.stop();
162
+ this.column = column;
163
+ this.error = undefined;
164
+ this.search = '';
165
+ this.formsOf = undefined;
166
+ if (this.sample?.column === column.name) {
167
+ return;
168
+ }
169
+ const { context } = this.editorState.document;
170
+ if (context) {
171
+ yield flowResult(this.editorState.loadModelOutline(context.model));
172
+ }
173
+ const problem = this.databaseProblem;
174
+ if (problem) {
175
+ this.error = problem;
176
+ return;
177
+ }
178
+ yield flowResult(this.read(false));
179
+ }
180
+
181
+ /** Reads the open column's first values, or every row, before the node */
182
+ *read(everyRow: boolean): GeneratorFn<void> {
183
+ const { editorState, column, input } = this;
184
+ if (!column || !input || this.isReading) {
185
+ return;
186
+ }
187
+ const controller = new AbortController();
188
+ this.controller = controller;
189
+ this.isReading = true;
190
+ this.error = undefined;
191
+ try {
192
+ const sample = (yield readCubeJsonSample(editorState, {
193
+ document: editorState.document,
194
+ afterId: input.id,
195
+ column,
196
+ everyRow,
197
+ abortController: controller,
198
+ })) as CubeJsonSample | undefined;
199
+ if (this.controller === controller && sample) {
200
+ this.sample = { column: column.name, sample };
201
+ }
202
+ } catch (error) {
203
+ if (
204
+ this.controller !== controller ||
205
+ error instanceof CubeQueryRunAborted
206
+ ) {
207
+ return;
208
+ }
209
+ assertErrorThrown(error);
210
+ this.error =
211
+ error instanceof CubeEngineError ? error.firstLine : error.message;
212
+ } finally {
213
+ if (this.controller === controller) {
214
+ this.controller = undefined;
215
+ this.isReading = false;
216
+ }
217
+ }
218
+ }
219
+
220
+ /** Stops a read in flight, keeping what was read before it */
221
+ stop(): void {
222
+ this.controller?.abort();
223
+ this.controller = undefined;
224
+ this.isReading = false;
225
+ }
226
+
227
+ /** Closes the fields, keeping what was read for when they open again */
228
+ close(): void {
229
+ this.stop();
230
+ this.column = undefined;
231
+ this.error = undefined;
232
+ this.formsOf = undefined;
233
+ }
234
+
235
+ setSearch(search: string): void {
236
+ this.search = search;
237
+ }
238
+
239
+ toggleFolded(path: readonly string[]): void {
240
+ const key = pathKey(path);
241
+ if (this.folded.has(key)) {
242
+ this.folded.delete(key);
243
+ } else {
244
+ this.folded.add(key);
245
+ }
246
+ }
247
+
248
+ toggleForms(path: readonly string[]): void {
249
+ const key = pathKey(path);
250
+ this.formsOf = this.formsOf === key ? undefined : key;
251
+ }
252
+ }
@@ -0,0 +1,359 @@
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 { foldColumnName } from '@finos/legend-cube';
18
+ import { columnAccessOf, pureStringLiteral } from '../CubePureText.js';
19
+ import {
20
+ CUBE_JSON_PURE_TYPE,
21
+ CubeJsonScalarKind,
22
+ type CubeJsonSample,
23
+ type CubeJsonShape,
24
+ describeCubeJsonShape,
25
+ getCubeJsonScalarType,
26
+ getCubeJsonTopValues,
27
+ isCubeJsonScalarShape,
28
+ } from './CubeJsonShape.js';
29
+
30
+ // The fields of a Variant column's documents, and the columns each can become
31
+ // (PLAN §11.20), after legend-lite's Data Cube: picking one writes the Pure
32
+ // text of an Extend column, which the engine parses and types like any typed
33
+ // there. Only forms run on legend-engine are offered: `contains` goes through
34
+ // `toMany(@Variant)->map(…to(@T))`, since `toMany(@String)->contains` writes
35
+ // invalid SQL on DuckDB; no `exists` (an engine error); no tuple explode
36
+ // (`toVariant(newMap(…))`, nested maps excluded on Snowflake and DuckDB).
37
+
38
+ /** The Variant type, as an expression names it */
39
+ const VARIANT = 'Variant';
40
+
41
+ /** How many of an array's top values get a "contains" offer */
42
+ const CONTAINS_OFFERS = 8;
43
+
44
+ /** One column a field can become */
45
+ export interface CubeJsonOffer {
46
+ /** Unique among the field's offers, e.g. `value`, `count`, `contains:FR` */
47
+ readonly key: string;
48
+ /** What the dialog shows, e.g. `the value, as Integer` */
49
+ readonly label: string;
50
+ /** The column's default name, e.g. `payload_user_tier` */
51
+ readonly name: string;
52
+ /** The Pure type it should take, `Variant` for JSON: a suggestion, the engine types the column */
53
+ readonly type: string;
54
+ /** An Extend column's text, `x | …`; `undefined` for an explode */
55
+ readonly code: string | undefined;
56
+ /** One row per element of the array at this path: an Explode Array node */
57
+ readonly explode?:
58
+ | { readonly column: string; readonly path: readonly string[] }
59
+ | undefined;
60
+ }
61
+
62
+ /** A position in the documents, and what it can become */
63
+ export interface CubeJsonField {
64
+ /** The keys from the column to it; empty for the column itself */
65
+ readonly path: readonly string[];
66
+ /** What it held, e.g. `text`, `list of object` */
67
+ readonly description: string;
68
+ /** Its share, 0 to 1, of the places it could be: the rows read, or its object's occurrences */
69
+ readonly presence: number;
70
+ /** Its most frequent values, for a scalar */
71
+ readonly topValues: readonly string[];
72
+ readonly offers: readonly CubeJsonOffer[];
73
+ readonly children: readonly CubeJsonField[];
74
+ }
75
+
76
+ /** A column name from parts: joined by `_`, letters, digits and `_` only */
77
+ const nameOf = (parts: readonly string[]): string =>
78
+ parts
79
+ .join('_')
80
+ .replace(/[^A-Za-z0-9_]+/gu, '_')
81
+ .replace(/_+/gu, '_')
82
+ .replace(/^_|_$/gu, '') || 'value';
83
+
84
+ /** The default name of an array's element, as Explode Array makes it: its last key's, or its column's, then `_element` */
85
+ export const getCubeJsonElementName = (
86
+ column: string,
87
+ path: readonly string[],
88
+ ): string => nameOf([path[path.length - 1] ?? column, 'element']);
89
+
90
+ /** `$x.c->get('a')->get('b')` */
91
+ const pathOf = (column: string, path: readonly string[]): string =>
92
+ path.reduce(
93
+ (expression, key) => `${expression}->get(${pureStringLiteral(key)})`,
94
+ columnAccessOf(column),
95
+ );
96
+
97
+ /** A function of the row */
98
+ const ofRow = (body: string): string => `x | ${body}`;
99
+
100
+ /**
101
+ * A seen value as a Pure literal of the kind it is read as: a number in a
102
+ * Float list with a point (`2.0`); `undefined` for a number Pure can't write
103
+ * as a literal (an exponent)
104
+ */
105
+ const literalOf = (
106
+ kind: CubeJsonScalarKind,
107
+ text: string,
108
+ ): string | undefined =>
109
+ kind === CubeJsonScalarKind.INTEGER || kind === CubeJsonScalarKind.FLOAT
110
+ ? !/^-?\d+(?:\.\d+)?$/u.test(text)
111
+ ? undefined
112
+ : kind === CubeJsonScalarKind.FLOAT && !text.includes('.')
113
+ ? `${text}.0`
114
+ : text
115
+ : kind === CubeJsonScalarKind.BOOLEAN
116
+ ? String(text === 'true')
117
+ : pureStringLiteral(text);
118
+
119
+ /** The kinds "contains" compares as typed literals: text, numbers and booleans */
120
+ const COMPARABLE_KINDS = new Set([
121
+ CubeJsonScalarKind.TEXT,
122
+ CubeJsonScalarKind.INTEGER,
123
+ CubeJsonScalarKind.FLOAT,
124
+ CubeJsonScalarKind.BOOLEAN,
125
+ ]);
126
+
127
+ const arrayOffers = (
128
+ column: string,
129
+ path: readonly string[],
130
+ shape: CubeJsonShape,
131
+ ): CubeJsonOffer[] => {
132
+ const array = pathOf(column, path);
133
+ const names = [column, ...path];
134
+ const many = `${array}->toMany(@${VARIANT})`;
135
+ const offers: CubeJsonOffer[] = [
136
+ {
137
+ key: 'explode',
138
+ label: 'one row per element',
139
+ name: getCubeJsonElementName(column, path),
140
+ type: VARIANT,
141
+ code: undefined,
142
+ explode: { column, path },
143
+ },
144
+ {
145
+ key: 'count',
146
+ label: 'number of elements',
147
+ name: nameOf([...names, 'count']),
148
+ type: 'Integer',
149
+ code: ofRow(`${many}->size()`),
150
+ },
151
+ ];
152
+ const { element } = shape;
153
+ if (!element) {
154
+ return offers;
155
+ }
156
+ const elementKind = getCubeJsonScalarType(element);
157
+ if (elementKind && isCubeJsonScalarShape(element)) {
158
+ offers.push({
159
+ key: 'list',
160
+ label: 'all values, as text',
161
+ name: nameOf([...names, 'list']),
162
+ type: 'String',
163
+ code: ofRow(`${many}->map(e | $e->to(@String))->joinStrings(', ')`),
164
+ });
165
+ if (COMPARABLE_KINDS.has(elementKind)) {
166
+ const type = CUBE_JSON_PURE_TYPE[elementKind];
167
+ getCubeJsonTopValues(element, CONTAINS_OFFERS).forEach((value) => {
168
+ const literal = literalOf(elementKind, value);
169
+ if (literal !== undefined) {
170
+ offers.push({
171
+ key: `contains:${value}`,
172
+ label: `contains ${value}`,
173
+ name: nameOf([...names, 'has', value]),
174
+ type: 'Boolean',
175
+ code: ofRow(
176
+ `${many}->map(e | $e->to(@${type}))->contains(${literal})`,
177
+ ),
178
+ });
179
+ }
180
+ });
181
+ }
182
+ }
183
+ if (element.objects > 0) {
184
+ offers.push({
185
+ key: 'first',
186
+ label: 'first element, as JSON',
187
+ name: nameOf([...names, 'first']),
188
+ type: VARIANT,
189
+ code: ofRow(`${array}->get(0)`),
190
+ });
191
+ [...element.fields.entries()]
192
+ .sort(([left], [right]) => left.localeCompare(right))
193
+ .forEach(([key, field]) => {
194
+ const kind = getCubeJsonScalarType(field);
195
+ if (!kind || !isCubeJsonScalarShape(field)) {
196
+ return;
197
+ }
198
+ const type = CUBE_JSON_PURE_TYPE[kind];
199
+ offers.push({
200
+ key: `first:${key}`,
201
+ label: `first element's ${key}`,
202
+ name: nameOf([...names, 'first', key]),
203
+ type,
204
+ code: ofRow(
205
+ `${array}->get(0)->get(${pureStringLiteral(key)})->to(@${type})`,
206
+ ),
207
+ });
208
+ if (
209
+ kind === CubeJsonScalarKind.INTEGER ||
210
+ kind === CubeJsonScalarKind.FLOAT
211
+ ) {
212
+ offers.push({
213
+ key: `total:${key}`,
214
+ label: `total of ${key}`,
215
+ name: nameOf([...names, key, 'total']),
216
+ type,
217
+ code: ofRow(
218
+ `${many}->map(e | $e->get(${pureStringLiteral(key)})->to(@${type}))->sum()`,
219
+ ),
220
+ });
221
+ }
222
+ });
223
+ }
224
+ return offers;
225
+ };
226
+
227
+ const offersOf = (
228
+ column: string,
229
+ path: readonly string[],
230
+ shape: CubeJsonShape,
231
+ ): CubeJsonOffer[] => {
232
+ const names = [column, ...path];
233
+ const offers: CubeJsonOffer[] = [];
234
+ const kind = getCubeJsonScalarType(shape);
235
+ if (kind && isCubeJsonScalarShape(shape)) {
236
+ const type = CUBE_JSON_PURE_TYPE[kind];
237
+ offers.push({
238
+ key: 'value',
239
+ label: `the value, as ${type}`,
240
+ name: nameOf(path.length ? names : [...names, 'value']),
241
+ type,
242
+ code: ofRow(`${pathOf(column, path)}->to(@${type})`),
243
+ });
244
+ }
245
+ // a nested object or array, as a Variant column of its own, to explore again
246
+ if (path.length > 0 && (shape.objects > 0 || shape.arrays > 0)) {
247
+ offers.push({
248
+ key: 'json',
249
+ label: 'as JSON',
250
+ name: nameOf(names),
251
+ type: VARIANT,
252
+ code: ofRow(pathOf(column, path)),
253
+ });
254
+ }
255
+ if (shape.objects > 0) {
256
+ offers.push({
257
+ key: 'keys',
258
+ label: 'its keys, as text',
259
+ name: nameOf([...names, 'keys']),
260
+ type: 'String',
261
+ code: ofRow(
262
+ `${pathOf(column, path)}->to(@Map<String, ${VARIANT}>)->toOne()->keys()->joinStrings(', ')`,
263
+ ),
264
+ });
265
+ }
266
+ if (shape.arrays > 0) {
267
+ offers.push(...arrayOffers(column, path, shape));
268
+ }
269
+ return offers;
270
+ };
271
+
272
+ /**
273
+ * A column's fields, from a sample of its documents: the column itself, its
274
+ * object's fields, theirs, and so on, each with what it held, how often it was
275
+ * there and what it can become. An array's elements' fields are reached
276
+ * through the array's offers (first element's, total of), not as fields.
277
+ */
278
+ export const getCubeJsonFields = (
279
+ column: string,
280
+ sample: CubeJsonSample,
281
+ ): CubeJsonField => {
282
+ const build = (
283
+ shape: CubeJsonShape,
284
+ path: readonly string[],
285
+ places: number,
286
+ ): CubeJsonField => ({
287
+ path,
288
+ description: describeCubeJsonShape(shape),
289
+ presence: Math.min(1, (shape.present - shape.nulls) / Math.max(1, places)),
290
+ topValues: isCubeJsonScalarShape(shape) ? getCubeJsonTopValues(shape) : [],
291
+ offers: offersOf(column, path, shape),
292
+ children:
293
+ shape.objects > 0
294
+ ? [...shape.fields.entries()]
295
+ .sort(([left], [right]) => left.localeCompare(right))
296
+ .map(([key, field]) => build(field, [...path, key], shape.objects))
297
+ : [],
298
+ });
299
+ return build(sample.shape, [], sample.rows);
300
+ };
301
+
302
+ /** A name no taken column has, compared folded: `name`, else `name_2`, `name_3`, … */
303
+ export const getFreeCubeColumnName = (
304
+ name: string,
305
+ taken: readonly string[],
306
+ ): string => {
307
+ const folded = new Set(taken.map(foldColumnName));
308
+ if (!folded.has(foldColumnName(name))) {
309
+ return name;
310
+ }
311
+ let index = 2;
312
+ while (folded.has(foldColumnName(`${name}_${index}`))) {
313
+ index += 1;
314
+ }
315
+ return `${name}_${index}`;
316
+ };
317
+
318
+ /** An offer's expression of the row, as an editor writes it: its text without `x | ` */
319
+ export const getCubeJsonOfferExpression = (
320
+ offer: CubeJsonOffer,
321
+ ): string | undefined => offer.code?.replace(/^x \| /u, '');
322
+
323
+ /**
324
+ * What clicking a field in an expression editor writes (PLAN §11.20, V.9):
325
+ * a value as its suggested type, anything else as JSON (the column itself
326
+ * for its root); and the column "+ column" would add, but for the root,
327
+ * which is the column already
328
+ */
329
+ export const getCubeJsonFieldDefault = (
330
+ column: string,
331
+ field: CubeJsonField,
332
+ ): { expression: string; column?: { name: string; code: string } } => {
333
+ const offer = field.offers.find(
334
+ ({ key }) => key === 'value' || key === 'json',
335
+ );
336
+ const expression =
337
+ (offer && getCubeJsonOfferExpression(offer)) ?? pathOf(column, field.path);
338
+ return offer?.code && field.path.length
339
+ ? { expression, column: { name: offer.name, code: offer.code } }
340
+ : { expression };
341
+ };
342
+
343
+ /** The fields whose keys match a search, with the fields they are in; all without a search */
344
+ export const filterCubeJsonFields = (
345
+ field: CubeJsonField,
346
+ search: string,
347
+ ): CubeJsonField | undefined => {
348
+ const needle = search.trim().toLowerCase();
349
+ if (!needle) {
350
+ return field;
351
+ }
352
+ const children = field.children
353
+ .map((child) => filterCubeJsonFields(child, search))
354
+ .filter((child): child is CubeJsonField => child !== undefined);
355
+ return children.length ||
356
+ field.path.some((key) => key.toLowerCase().includes(needle))
357
+ ? { ...field, children }
358
+ : undefined;
359
+ };
@@ -0,0 +1,75 @@
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 { Extend } from '@finos/legend-cube';
18
+ import type { CubeEditorState } from '../CubeEditorState.js';
19
+ import { columnAccessOf } from '../CubePureText.js';
20
+ import { readCubeJson } from './CubeJson.js';
21
+ import { getFreeCubeColumnName } from './CubeJsonOffers.js';
22
+
23
+ // Parse as JSON (PLAN §11.20): a text column holding JSON documents read as a
24
+ // Variant column, `fromJson()`, so Explore JSON and Explode Array work on it.
25
+ // JSON stored as text is common, and it is the only way to a Variant column on
26
+ // a local source: H2 has none, and a direct connection hides a JSON column.
27
+
28
+ /** The column a text column's documents become: `<name>_json` */
29
+ export const getCubeJsonParseName = (column: string): string =>
30
+ `${column}_json`;
31
+
32
+ /** The Extend column's text: the text read as a document (`fromJson` takes one value) */
33
+ export const getCubeJsonParseCode = (column: string): string =>
34
+ `x | ${columnAccessOf(column)}->toOne()->fromJson()`;
35
+
36
+ /** Whether a text cell holds a JSON object or array, which Parse as JSON is offered on */
37
+ export const isCubeJsonDocumentText = (text: string): boolean => {
38
+ const node = readCubeJson(text);
39
+ return node?.kind === 'object' || node?.kind === 'array';
40
+ };
41
+
42
+ /**
43
+ * Adds Parse as JSON's column after the node that ran, as one undo step, once
44
+ * the engine parses its text; nothing if the cube changed meanwhile
45
+ */
46
+ export const addCubeJsonParseColumn = async (
47
+ editorState: CubeEditorState,
48
+ column: string,
49
+ ): Promise<void> => {
50
+ const { document } = editorState;
51
+ const { query } = document;
52
+ const afterId = query.selected;
53
+ if (afterId === undefined) {
54
+ return;
55
+ }
56
+ const extendId = query.generateId(Extend.TYPE);
57
+ const code = getCubeJsonParseCode(column);
58
+ const name = getFreeCubeColumnName(
59
+ getCubeJsonParseName(column),
60
+ (editorState.analysis.schemas.get(afterId)?.columns ?? []).map(
61
+ ({ name: taken }) => taken,
62
+ ),
63
+ );
64
+ const { lambda } = await editorState.host.engine.parseExpression(
65
+ code,
66
+ `${extendId}:0`,
67
+ );
68
+ // as the menu found it: nothing changed while the engine parsed
69
+ if (editorState.document === document) {
70
+ editorState.addConfiguredNode(
71
+ new Extend(extendId, [{ name, code, lambda }]),
72
+ afterId,
73
+ );
74
+ }
75
+ };