@finos/legend-data-cube 0.3.118 → 0.3.119

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 (60) hide show
  1. package/lib/components/view/filter/DataCubeFilterEditor.d.ts.map +1 -1
  2. package/lib/components/view/filter/DataCubeFilterEditor.js +73 -36
  3. package/lib/components/view/filter/DataCubeFilterEditor.js.map +1 -1
  4. package/lib/components/view/filter/DataCubeFilterEditorUtils.d.ts +65 -0
  5. package/lib/components/view/filter/DataCubeFilterEditorUtils.d.ts.map +1 -0
  6. package/lib/components/view/filter/DataCubeFilterEditorUtils.js +137 -0
  7. package/lib/components/view/filter/DataCubeFilterEditorUtils.js.map +1 -0
  8. package/lib/index.css +2 -2
  9. package/lib/index.css.map +1 -1
  10. package/lib/package.json +1 -1
  11. package/lib/stores/core/DataCubeQueryBuilderUtils.d.ts.map +1 -1
  12. package/lib/stores/core/DataCubeQueryBuilderUtils.js +7 -0
  13. package/lib/stores/core/DataCubeQueryBuilderUtils.js.map +1 -1
  14. package/lib/stores/core/DataCubeQueryEngine.d.ts +26 -1
  15. package/lib/stores/core/DataCubeQueryEngine.d.ts.map +1 -1
  16. package/lib/stores/core/DataCubeQueryEngine.js +42 -0
  17. package/lib/stores/core/DataCubeQueryEngine.js.map +1 -1
  18. package/lib/stores/core/DataCubeSnapshotBuilderUtils.d.ts.map +1 -1
  19. package/lib/stores/core/DataCubeSnapshotBuilderUtils.js +12 -0
  20. package/lib/stores/core/DataCubeSnapshotBuilderUtils.js.map +1 -1
  21. package/lib/stores/core/filter/DataCubeQueryFilterOperation.d.ts +1 -1
  22. package/lib/stores/core/filter/DataCubeQueryFilterOperation.d.ts.map +1 -1
  23. package/lib/stores/core/filter/DataCubeQueryFilterOperation.js +9 -0
  24. package/lib/stores/core/filter/DataCubeQueryFilterOperation.js.map +1 -1
  25. package/lib/stores/core/filter/DataCubeQueryFilterOperation__Equal.d.ts.map +1 -1
  26. package/lib/stores/core/filter/DataCubeQueryFilterOperation__Equal.js +7 -1
  27. package/lib/stores/core/filter/DataCubeQueryFilterOperation__Equal.js.map +1 -1
  28. package/lib/stores/core/filter/DataCubeQueryFilterOperation__GreaterThan.d.ts.map +1 -1
  29. package/lib/stores/core/filter/DataCubeQueryFilterOperation__GreaterThan.js +7 -1
  30. package/lib/stores/core/filter/DataCubeQueryFilterOperation__GreaterThan.js.map +1 -1
  31. package/lib/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqual.d.ts.map +1 -1
  32. package/lib/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqual.js +7 -1
  33. package/lib/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqual.js.map +1 -1
  34. package/lib/stores/core/filter/DataCubeQueryFilterOperation__LessThan.d.ts.map +1 -1
  35. package/lib/stores/core/filter/DataCubeQueryFilterOperation__LessThan.js +7 -1
  36. package/lib/stores/core/filter/DataCubeQueryFilterOperation__LessThan.js.map +1 -1
  37. package/lib/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqual.d.ts.map +1 -1
  38. package/lib/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqual.js +7 -1
  39. package/lib/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqual.js.map +1 -1
  40. package/lib/stores/core/filter/DataCubeQueryFilterOperation__NotEqual.d.ts.map +1 -1
  41. package/lib/stores/core/filter/DataCubeQueryFilterOperation__NotEqual.js +7 -1
  42. package/lib/stores/core/filter/DataCubeQueryFilterOperation__NotEqual.js.map +1 -1
  43. package/lib/stores/view/filter/DataCubeFilterEditorState.d.ts.map +1 -1
  44. package/lib/stores/view/filter/DataCubeFilterEditorState.js +3 -1
  45. package/lib/stores/view/filter/DataCubeFilterEditorState.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/components/view/filter/DataCubeFilterEditor.tsx +150 -88
  48. package/src/components/view/filter/DataCubeFilterEditorUtils.ts +165 -0
  49. package/src/stores/core/DataCubeQueryBuilderUtils.ts +7 -0
  50. package/src/stores/core/DataCubeQueryEngine.ts +65 -0
  51. package/src/stores/core/DataCubeSnapshotBuilderUtils.ts +14 -0
  52. package/src/stores/core/filter/DataCubeQueryFilterOperation.ts +15 -1
  53. package/src/stores/core/filter/DataCubeQueryFilterOperation__Equal.tsx +7 -0
  54. package/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThan.tsx +7 -0
  55. package/src/stores/core/filter/DataCubeQueryFilterOperation__GreaterThanOrEqual.tsx +7 -0
  56. package/src/stores/core/filter/DataCubeQueryFilterOperation__LessThan.tsx +7 -0
  57. package/src/stores/core/filter/DataCubeQueryFilterOperation__LessThanOrEqual.tsx +7 -0
  58. package/src/stores/core/filter/DataCubeQueryFilterOperation__NotEqual.tsx +7 -0
  59. package/src/stores/view/filter/DataCubeFilterEditorState.tsx +3 -1
  60. package/tsconfig.json +1 -0
@@ -18,9 +18,6 @@ import {
18
18
  BasePopover,
19
19
  cn,
20
20
  DataCubeIcon,
21
- DateCalendar,
22
- DatePicker,
23
- DatePickerField,
24
21
  useDropdownMenu,
25
22
  useForkRef,
26
23
  } from '@finos/legend-art';
@@ -29,8 +26,18 @@ import {
29
26
  DataCubeOperationAdvancedValueType,
30
27
  DataCubeQueryFilterGroupOperator,
31
28
  getDataType,
29
+ isCurrentMomentValue,
30
+ isDateTimeType,
32
31
  type DataCubeOperationValue,
33
32
  } from '../../../stores/core/DataCubeQueryEngine.js';
33
+ import {
34
+ buildDateValue,
35
+ DATE_VALUE_MODE_DESCRIPTION,
36
+ DATE_VALUE_MODE_LABEL,
37
+ getDateValueFormats,
38
+ getDateValueMode,
39
+ getDateValueModes,
40
+ } from './DataCubeFilterEditorUtils.js';
34
41
  import {
35
42
  DataCubeFilterEditorConditionGroupTreeNode,
36
43
  DataCubeFilterEditorConditionTreeNode,
@@ -52,11 +59,7 @@ import {
52
59
  useState,
53
60
  useMemo,
54
61
  } from 'react';
55
- import {
56
- DATE_FORMAT,
57
- PRECISE_PRIMITIVE_TYPE,
58
- PRIMITIVE_TYPE,
59
- } from '@finos/legend-graph';
62
+ import { PRECISE_PRIMITIVE_TYPE, PRIMITIVE_TYPE } from '@finos/legend-graph';
60
63
 
61
64
  import {
62
65
  formatDate,
@@ -228,93 +231,131 @@ const DataCubeEditorFilterConditionNodeNumberValueEditor = observer(
228
231
  }),
229
232
  );
230
233
 
231
- // NOTE: this has to be declared here instead of defined inline in slot configuration of DatePicker
232
- // else, with each re-render, a new function will be created and the ref might be lost
233
- const CustomDateFieldPicker = forwardRef<HTMLInputElement>(
234
- function CustomDateFieldPicker(p, r) {
235
- return (
236
- <DatePickerField
237
- {...p}
238
- ref={r}
239
- className="h-5 w-full flex-shrink-0 border border-neutral-400 px-1 pr-5 text-sm disabled:border-neutral-300 disabled:bg-neutral-50 disabled:text-neutral-300"
240
- />
241
- );
242
- },
243
- );
234
+ const DATE_INPUT_CLASSNAME =
235
+ 'h-5 w-full flex-shrink-0 border border-neutral-400 px-1 text-sm disabled:border-neutral-300 disabled:bg-neutral-50 disabled:text-neutral-300';
244
236
 
245
- const CustomDateFieldPickerOpenCalendarButton = observer(
246
- (props: { value: string; updateValue: (value: string) => void }) => {
247
- const { value, updateValue } = props;
248
- const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
237
+ /**
238
+ * Editor for an absolute date value, picked with the control of the browser,
239
+ * like the query builder does. A value which carries a time of day is edited
240
+ * down to the second, and is written back with the time zone of the browser.
241
+ */
242
+ const DataCubeEditorFilterConditionNodeDatePicker = observer(
243
+ forwardRef<
244
+ HTMLInputElement,
245
+ {
246
+ value: string;
247
+ updateValue: (value: string) => void;
248
+ valueType: string;
249
+ }
250
+ >(function DataCubeEditorFilterConditionNodeDatePicker(props, ref) {
251
+ const { value, updateValue, valueType } = props;
252
+ const hasTime = isDateTimeType(valueType);
253
+ const { valueFormat, displayFormat } = getDateValueFormats(valueType);
254
+ const moment = parseISO(value);
249
255
 
250
256
  return (
251
- <>
252
- <button
253
- className="absolute right-0 top-0 flex h-5 w-5 items-center justify-center"
254
- title="Open Date Picker..."
255
- onClick={(event) => {
256
- setAnchorEl(event.currentTarget);
257
- }}
258
- tabIndex={-1}
259
- >
260
- <DataCubeIcon.Calendar className="text-lg text-neutral-500 hover:text-neutral-600" />
261
- </button>
262
- <BasePopover
263
- open={Boolean(anchorEl)}
264
- anchorEl={anchorEl}
265
- anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
266
- transformOrigin={{ vertical: 'top', horizontal: 'center' }}
267
- onClose={() => setAnchorEl(null)}
268
- classes={{
269
- root: 'data-cube-editor-date-calendar mt-0.5',
270
- paper: 'shadow-md rounded-none border border-neutral-300',
271
- }}
272
- >
273
- <DateCalendar
274
- autoFocus={true}
275
- value={parseISO(value)}
276
- onChange={(newValue: Date) => {
277
- updateValue(formatDate(newValue, DATE_FORMAT));
278
- setAnchorEl(null);
279
- }}
280
- />
281
- </BasePopover>
282
- </>
257
+ <input
258
+ ref={ref}
259
+ className={DATE_INPUT_CLASSNAME}
260
+ // NOTE: despite its name, `datetime-local` registers a moment in the
261
+ // time zone of the browser
262
+ // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#setting_timezones
263
+ type={hasTime ? 'datetime-local' : 'date'}
264
+ // NOTE: configure the step so the seconds can be picked
265
+ // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#step
266
+ step={hasTime ? '1' : undefined}
267
+ spellCheck={false}
268
+ value={
269
+ !Number.isNaN(moment.getTime())
270
+ ? formatDate(moment, displayFormat)
271
+ : ''
272
+ }
273
+ onChange={(event) => {
274
+ const newMoment = parseISO(event.target.value);
275
+ // NOTE: the control reports an empty value while the user is still
276
+ // filling it in
277
+ if (!Number.isNaN(newMoment.getTime())) {
278
+ updateValue(formatDate(newMoment, valueFormat));
279
+ }
280
+ }}
281
+ />
283
282
  );
284
- },
283
+ }),
285
284
  );
286
285
 
286
+ /**
287
+ * Date value editor for scalar filter conditions. For columns which carry a
288
+ * date, it lets the user pick how the value is specified: an absolute date, an
289
+ * absolute date and time, `today()` or `now()`.
290
+ */
287
291
  const DataCubeEditorFilterConditionNodeDateValueEditor = observer(
288
292
  forwardRef<
289
293
  HTMLInputElement,
290
294
  {
291
- value: string;
292
- updateValue: (value: string) => void;
295
+ node: DataCubeFilterEditorConditionTreeNode;
293
296
  }
294
- >(function DataCubeEditorFilterConditionNodeValueEditor(props, ref) {
295
- const { value, updateValue } = props;
297
+ >(function DataCubeEditorFilterConditionNodeDateValueEditor(props, ref) {
298
+ const { node } = props;
299
+ const value = node.value;
300
+ const modes = getDateValueModes(node.column.type);
301
+ const mode = getDateValueMode(value);
302
+ const [
303
+ openModeDropdown,
304
+ closeModeDropdown,
305
+ modeDropdownProps,
306
+ modeDropdownPropsOpen,
307
+ ] = useDropdownMenu();
296
308
 
297
- return (
298
- <DatePicker
309
+ const datePicker = (
310
+ <DataCubeEditorFilterConditionNodeDatePicker
299
311
  ref={ref}
300
- value={parseISO(value)}
301
- format={DATE_FORMAT}
302
- slots={{
303
- field: CustomDateFieldPicker,
304
- openPickerButton: () => (
305
- <CustomDateFieldPickerOpenCalendarButton
306
- value={value}
307
- updateValue={updateValue}
308
- />
309
- ),
310
- }}
311
- onChange={(newValue: Date | null) => {
312
- if (newValue) {
313
- updateValue(formatDate(newValue, DATE_FORMAT));
314
- }
315
- }}
312
+ value={value.value as string}
313
+ updateValue={(val) => node.updateValue(val)}
314
+ valueType={value.type}
316
315
  />
317
316
  );
317
+
318
+ // For columns which carry no date (e.g. a time-of-day column), only the
319
+ // date picker is shown, none of the modes apply.
320
+ if (!modes.length) {
321
+ return datePicker;
322
+ }
323
+
324
+ return (
325
+ <div className="flex h-full w-full items-center">
326
+ <FormDropdownMenuTrigger
327
+ className="relative mr-1 w-24 flex-shrink-0"
328
+ onClick={openModeDropdown}
329
+ open={modeDropdownPropsOpen}
330
+ title={DATE_VALUE_MODE_DESCRIPTION[mode]}
331
+ >
332
+ {DATE_VALUE_MODE_LABEL[mode]}
333
+ </FormDropdownMenuTrigger>
334
+ <FormDropdownMenu className="w-24" {...modeDropdownProps}>
335
+ {modes.map((_mode) => (
336
+ <FormDropdownMenuItem
337
+ key={_mode}
338
+ onClick={() => {
339
+ if (_mode !== mode) {
340
+ node.setValue(buildDateValue(_mode, value));
341
+ }
342
+ closeModeDropdown();
343
+ }}
344
+ autoFocus={_mode === mode}
345
+ title={DATE_VALUE_MODE_DESCRIPTION[_mode]}
346
+ >
347
+ {DATE_VALUE_MODE_LABEL[_mode]}
348
+ </FormDropdownMenuItem>
349
+ ))}
350
+ </FormDropdownMenu>
351
+ {/* NOTE: the date control sizes itself against its container, so it
352
+ needs one which is only as wide as what the mode selector leaves, else
353
+ it overflows the value editor. */}
354
+ {!isCurrentMomentValue(value) && (
355
+ <div className="min-w-0 flex-1">{datePicker}</div>
356
+ )}
357
+ </div>
358
+ );
318
359
  }),
319
360
  );
320
361
 
@@ -454,12 +495,13 @@ const DataCubeEditorFilterConditionNodeListValueEditor = observer(
454
495
  case PRECISE_PRIMITIVE_TYPE.STRICTTIME:
455
496
  case PRECISE_PRIMITIVE_TYPE.TIMESTAMP: {
456
497
  // try parse ISO, else use formatted now
498
+ const { valueFormat } = getDateValueFormats(firstType);
457
499
  let dateVal = trimmed;
458
500
  try {
459
501
  const d = parseISO(trimmed);
460
- dateVal = formatDate(d, DATE_FORMAT);
502
+ dateVal = formatDate(d, valueFormat);
461
503
  } catch {
462
- dateVal = formatDate(new Date(), DATE_FORMAT);
504
+ dateVal = formatDate(new Date(), valueFormat);
463
505
  }
464
506
  newItem = { type: firstType, value: dateVal };
465
507
  break;
@@ -621,10 +663,14 @@ const DataCubeEditorFilterConditionNodeListValueEditor = observer(
621
663
  case PRECISE_PRIMITIVE_TYPE.STRICTTIME:
622
664
  case PRECISE_PRIMITIVE_TYPE.TIMESTAMP:
623
665
  return (
624
- <DataCubeEditorFilterConditionNodeDateValueEditor
666
+ <DataCubeEditorFilterConditionNodeDatePicker
667
+ valueType={it.type}
625
668
  value={String(
626
669
  it.value ??
627
- formatDate(new Date(), DATE_FORMAT),
670
+ formatDate(
671
+ new Date(),
672
+ getDateValueFormats(it.type).valueFormat,
673
+ ),
628
674
  )}
629
675
  updateValue={(v: string) => {
630
676
  const next = localItems.slice();
@@ -719,12 +765,13 @@ const DataCubeEditorFilterConditionNodeValueEditor = observer(
719
765
  forwardRef<
720
766
  HTMLElement,
721
767
  {
768
+ node: DataCubeFilterEditorConditionTreeNode;
722
769
  value: DataCubeOperationValue;
723
770
  updateValue: (value: unknown) => void;
724
771
  view: DataCubeViewState;
725
772
  }
726
773
  >(function DataCubeEditorFilterConditionNodeValueEditor(props, ref) {
727
- const { value, updateValue, view } = props;
774
+ const { node, value, updateValue, view } = props;
728
775
  // WIP: support column
729
776
  switch (value.type) {
730
777
  case PRIMITIVE_TYPE.STRING:
@@ -774,11 +821,12 @@ const DataCubeEditorFilterConditionNodeValueEditor = observer(
774
821
  case PRECISE_PRIMITIVE_TYPE.DATETIME:
775
822
  case PRECISE_PRIMITIVE_TYPE.STRICTTIME:
776
823
  case PRECISE_PRIMITIVE_TYPE.TIMESTAMP:
824
+ case DataCubeOperationAdvancedValueType.TODAY:
825
+ case DataCubeOperationAdvancedValueType.NOW:
777
826
  return (
778
827
  <DataCubeEditorFilterConditionNodeDateValueEditor
779
828
  ref={ref as React.RefObject<HTMLInputElement>}
780
- value={value.value as string}
781
- updateValue={(val) => updateValue(val)}
829
+ node={node}
782
830
  />
783
831
  );
784
832
  case DataCubeOperationAdvancedValueType.COLUMN:
@@ -909,6 +957,12 @@ const DataCubeEditorFilterConditionNodeDisplay = observer(
909
957
  () => valueEditorRef.current?.focus(),
910
958
  [],
911
959
  );
960
+ // NOTE: the value editor for columns which carry a date is wider to
961
+ // accommodate the selector for switching between a specific date and the
962
+ // `today()`/`now()` function.
963
+ const hasDateValueModeSelector = Boolean(
964
+ getDateValueModes(node.column.type).length,
965
+ );
912
966
 
913
967
  return (
914
968
  <div className="group flex h-6 items-center">
@@ -1034,9 +1088,17 @@ const DataCubeEditorFilterConditionNodeDisplay = observer(
1034
1088
  </FormDropdownMenuItem>
1035
1089
  ))}
1036
1090
  </FormDropdownMenu>
1037
- <div className="relative w-32 flex-shrink-0">
1091
+ <div
1092
+ className={cn('relative flex-shrink-0', {
1093
+ // NOTE: wide enough for a date-time value, which the user can
1094
+ // switch to whenever the mode selector is shown
1095
+ 'w-80': hasDateValueModeSelector,
1096
+ 'w-32': !hasDateValueModeSelector,
1097
+ })}
1098
+ >
1038
1099
  <DataCubeEditorFilterConditionNodeValueEditor
1039
1100
  ref={valueEditorRef}
1101
+ node={node}
1040
1102
  value={node.value}
1041
1103
  updateValue={(val) => node.updateValue(val)}
1042
1104
  view={view}
@@ -0,0 +1,165 @@
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 {
18
+ DATE_FORMAT,
19
+ DATE_TIME_FORMAT,
20
+ PRIMITIVE_TYPE,
21
+ } from '@finos/legend-graph';
22
+ import { formatDate, parseISO } from '@finos/legend-shared';
23
+ import {
24
+ DataCubeOperationAdvancedValueType,
25
+ getCurrentMomentValueType,
26
+ isDateTimeType,
27
+ type DataCubeOperationValue,
28
+ } from '../../../stores/core/DataCubeQueryEngine.js';
29
+
30
+ /**
31
+ * The ways a date filter value can be specified: as an absolute moment - a
32
+ * date, or a date and a time of day - or as a function resolved when the query
33
+ * runs.
34
+ */
35
+ export enum DataCubeDateValueMode {
36
+ ABSOLUTE_DATE = 'ABSOLUTE_DATE',
37
+ ABSOLUTE_DATE_TIME = 'ABSOLUTE_DATE_TIME',
38
+ TODAY = 'TODAY',
39
+ NOW = 'NOW',
40
+ }
41
+
42
+ export const DATE_VALUE_MODE_LABEL: Record<DataCubeDateValueMode, string> = {
43
+ [DataCubeDateValueMode.ABSOLUTE_DATE]: 'Date',
44
+ [DataCubeDateValueMode.ABSOLUTE_DATE_TIME]: 'Date Time',
45
+ [DataCubeDateValueMode.TODAY]: 'Today',
46
+ [DataCubeDateValueMode.NOW]: 'Now',
47
+ };
48
+
49
+ // NOTE: the wording mirrors the date picker of the query builder, where a
50
+ // value specified by the user is an "absolute" one, as opposed to one which
51
+ // resolves relative to the moment the query runs.
52
+ export const DATE_VALUE_MODE_DESCRIPTION: Record<
53
+ DataCubeDateValueMode,
54
+ string
55
+ > = {
56
+ [DataCubeDateValueMode.ABSOLUTE_DATE]: 'Absolute date',
57
+ [DataCubeDateValueMode.ABSOLUTE_DATE_TIME]: 'Absolute date and time',
58
+ [DataCubeDateValueMode.NOW]: 'now()',
59
+ [DataCubeDateValueMode.TODAY]: 'today()',
60
+ };
61
+
62
+ // The format a `datetime-local` input takes: down to the second, and without
63
+ // the time zone, which is not part of what the user picks - it is taken from
64
+ // the browser when the value is written back out.
65
+ const DATE_TIME_INPUT_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
66
+
67
+ /**
68
+ * Returns the format the specified date value is stored in, and the format the
69
+ * editor displays it in. These differ for a date-time, whose value - see
70
+ * `_defaultPrimitiveTypeValue()` - carries the time zone, which is more than
71
+ * the editor shows.
72
+ */
73
+ export function getDateValueFormats(valueType: string): {
74
+ valueFormat: string;
75
+ displayFormat: string;
76
+ } {
77
+ return isDateTimeType(valueType)
78
+ ? { valueFormat: DATE_TIME_FORMAT, displayFormat: DATE_TIME_INPUT_FORMAT }
79
+ : { valueFormat: DATE_FORMAT, displayFormat: DATE_FORMAT };
80
+ }
81
+
82
+ export function getDateValueMode(
83
+ value: DataCubeOperationValue,
84
+ ): DataCubeDateValueMode {
85
+ switch (value.type) {
86
+ case DataCubeOperationAdvancedValueType.TODAY:
87
+ return DataCubeDateValueMode.TODAY;
88
+ case DataCubeOperationAdvancedValueType.NOW:
89
+ return DataCubeDateValueMode.NOW;
90
+ default:
91
+ return isDateTimeType(value.type)
92
+ ? DataCubeDateValueMode.ABSOLUTE_DATE_TIME
93
+ : DataCubeDateValueMode.ABSOLUTE_DATE;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Returns the ways a condition on the specified column can specify its value;
99
+ * empty when the column carries no date (e.g. a time-of-day column), where
100
+ * none of these apply.
101
+ *
102
+ * NOTE: every column which carries a date offers all of them, like the query
103
+ * builder does: `Date` is the super type of both `StrictDate` and `DateTime`,
104
+ * so a column typed with it can hold a time of day, and comparing a date
105
+ * against a moment which carries one is meaningful either way around.
106
+ */
107
+ export function getDateValueModes(columnType: string): DataCubeDateValueMode[] {
108
+ return getCurrentMomentValueType(columnType)
109
+ ? [
110
+ DataCubeDateValueMode.ABSOLUTE_DATE,
111
+ DataCubeDateValueMode.ABSOLUTE_DATE_TIME,
112
+ DataCubeDateValueMode.TODAY,
113
+ DataCubeDateValueMode.NOW,
114
+ ]
115
+ : [];
116
+ }
117
+
118
+ /**
119
+ * Returns the moment a date value specifies, defaulting to the current one
120
+ * where it specifies none, e.g. when it is `today()`.
121
+ */
122
+ function _dateValueMoment(value: DataCubeOperationValue): Date {
123
+ if (typeof value.value === 'string') {
124
+ const moment = parseISO(value.value);
125
+ if (!Number.isNaN(moment.getTime())) {
126
+ return moment;
127
+ }
128
+ }
129
+ return new Date();
130
+ }
131
+
132
+ /**
133
+ * Returns the value a condition takes when switched to the specified mode,
134
+ * keeping the moment it currently specifies.
135
+ *
136
+ * NOTE: an absolute value is built with the primitive type matching what it
137
+ * specifies rather than with the type of the column. The type of a value names
138
+ * the literal it is written as, which the type of the column does not: `Date`
139
+ * says nothing about whether a time of day is meant, and a timestamp column is
140
+ * typed with a relational data type, which no literal can be spelled with.
141
+ * This is also the type the value comes back as when the query it is written
142
+ * to is read again, so a value keeps its shape across a round-trip.
143
+ */
144
+ export function buildDateValue(
145
+ mode: DataCubeDateValueMode,
146
+ value: DataCubeOperationValue,
147
+ ): DataCubeOperationValue {
148
+ switch (mode) {
149
+ case DataCubeDateValueMode.TODAY:
150
+ return { type: DataCubeOperationAdvancedValueType.TODAY };
151
+ case DataCubeDateValueMode.NOW:
152
+ return { type: DataCubeOperationAdvancedValueType.NOW };
153
+ case DataCubeDateValueMode.ABSOLUTE_DATE_TIME:
154
+ return {
155
+ type: PRIMITIVE_TYPE.DATETIME,
156
+ value: formatDate(_dateValueMoment(value), DATE_TIME_FORMAT),
157
+ };
158
+ case DataCubeDateValueMode.ABSOLUTE_DATE:
159
+ default:
160
+ return {
161
+ type: PRIMITIVE_TYPE.STRICTDATE,
162
+ value: formatDate(_dateValueMoment(value), DATE_FORMAT),
163
+ };
164
+ }
165
+ }
@@ -259,8 +259,11 @@ export function _primitiveValue(
259
259
  case PRECISE_PRIMITIVE_TYPE.STRICTDATE:
260
260
  return _val(new V1_CStrictDate(), guaranteeIsString(value));
261
261
  case PRIMITIVE_TYPE.DATETIME:
262
+ case PRECISE_PRIMITIVE_TYPE.DATETIME:
263
+ case PRECISE_PRIMITIVE_TYPE.TIMESTAMP:
262
264
  return _val(new V1_CDateTime(), guaranteeIsString(value));
263
265
  case PRIMITIVE_TYPE.STRICTTIME:
266
+ case PRECISE_PRIMITIVE_TYPE.STRICTTIME:
264
267
  return _val(new V1_CStrictTime(), guaranteeIsString(value));
265
268
  case PRIMITIVE_TYPE.LATESTDATE:
266
269
  return new V1_CLatestDate();
@@ -372,6 +375,10 @@ export function _value(value: DataCubeOperationValue): V1_ValueSpecification {
372
375
  }
373
376
  return _primitiveValue(value.type, value.value);
374
377
  }
378
+ case DataCubeOperationAdvancedValueType.TODAY:
379
+ return _function(_functionName(DataCubeFunction.TODAY), []);
380
+ case DataCubeOperationAdvancedValueType.NOW:
381
+ return _function(_functionName(DataCubeFunction.NOW), []);
375
382
  case DataCubeOperationAdvancedValueType.COLUMN:
376
383
  return _property(guaranteeIsString(value.value));
377
384
  case DataCubeOperationAdvancedValueType.LIST: {
@@ -47,6 +47,10 @@ export enum DataCubeFunction {
47
47
  FROM = 'meta::pure::mapping::from',
48
48
  TO_ONE = 'meta::pure::functions::multiplicity::toOne',
49
49
 
50
+ // date
51
+ TODAY = 'meta::pure::functions::date::today',
52
+ NOW = 'meta::pure::functions::date::now',
53
+
50
54
  // sort
51
55
  ASCENDING = 'meta::pure::functions::relation::ascending',
52
56
  DESCENDING = 'meta::pure::functions::relation::descending',
@@ -157,6 +161,8 @@ export enum DataCubeOperationAdvancedValueType {
157
161
  COLUMN = 'COLUMN',
158
162
  LIST = 'LIST',
159
163
  VOID = 'VOID',
164
+ TODAY = 'TODAY',
165
+ NOW = 'NOW',
160
166
  // PARAMETER
161
167
  }
162
168
 
@@ -492,6 +498,65 @@ export function isPrimitiveType(type: string) {
492
498
  ] as string[]
493
499
  ).includes(type);
494
500
  }
501
+ /**
502
+ * Current-moment values are filter values which resolve at execution time
503
+ * rather than being specified by the user: `today()` and `now()`.
504
+ */
505
+ export type DataCubeCurrentMomentValueType =
506
+ | DataCubeOperationAdvancedValueType.TODAY
507
+ | DataCubeOperationAdvancedValueType.NOW;
508
+
509
+ export function isCurrentMomentValue(
510
+ value: DataCubeOperationValue,
511
+ ): value is DataCubeOperationValue & { type: DataCubeCurrentMomentValueType } {
512
+ return (
513
+ value.type === DataCubeOperationAdvancedValueType.TODAY ||
514
+ value.type === DataCubeOperationAdvancedValueType.NOW
515
+ );
516
+ }
517
+
518
+ export function isDateType(type: string) {
519
+ return (
520
+ [
521
+ PRIMITIVE_TYPE.DATE,
522
+ PRIMITIVE_TYPE.STRICTDATE,
523
+ PRECISE_PRIMITIVE_TYPE.STRICTDATE,
524
+ ] as string[]
525
+ ).includes(type);
526
+ }
527
+
528
+ /**
529
+ * Unlike `isDateType()`, these types carry a time of day as well as a date,
530
+ * so their values must be specified (and displayed) down to the second.
531
+ */
532
+ export function isDateTimeType(type: string) {
533
+ return (
534
+ [
535
+ PRIMITIVE_TYPE.DATETIME,
536
+ PRECISE_PRIMITIVE_TYPE.DATETIME,
537
+ PRECISE_PRIMITIVE_TYPE.TIMESTAMP,
538
+ ] as string[]
539
+ ).includes(type);
540
+ }
541
+
542
+ /**
543
+ * Returns the current-moment value type applicable to a column: `today()`
544
+ * (StrictDate) for date columns, `now()` (DateTime) for date-time columns.
545
+ * Returns `undefined` for columns which carry no date (e.g. a time-of-day
546
+ * column), since a current-moment date value is meaningless there.
547
+ */
548
+ export function getCurrentMomentValueType(
549
+ columnType: string,
550
+ ): DataCubeCurrentMomentValueType | undefined {
551
+ if (isDateType(columnType)) {
552
+ return DataCubeOperationAdvancedValueType.TODAY;
553
+ }
554
+ if (isDateTimeType(columnType)) {
555
+ return DataCubeOperationAdvancedValueType.NOW;
556
+ }
557
+ return undefined;
558
+ }
559
+
495
560
  export function isPivotResultColumnName(columnName: string) {
496
561
  return columnName.includes(PIVOT_COLUMN_NAME_VALUE_SEPARATOR);
497
562
  }
@@ -250,6 +250,20 @@ export function _operationValue(
250
250
  _operationValue(val, columnGetter, columnChecker),
251
251
  ),
252
252
  };
253
+ } else if (
254
+ value instanceof V1_AppliedFunction &&
255
+ matchFunctionName(value.function, DataCubeFunction.TODAY)
256
+ ) {
257
+ return {
258
+ type: DataCubeOperationAdvancedValueType.TODAY,
259
+ };
260
+ } else if (
261
+ value instanceof V1_AppliedFunction &&
262
+ matchFunctionName(value.function, DataCubeFunction.NOW)
263
+ ) {
264
+ return {
265
+ type: DataCubeOperationAdvancedValueType.NOW,
266
+ };
253
267
  } else if (value === undefined) {
254
268
  return {
255
269
  type: DataCubeOperationAdvancedValueType.VOID,
@@ -14,7 +14,11 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import type { DataCubeOperationValue } from '../DataCubeQueryEngine.js';
17
+ import {
18
+ getCurrentMomentValueType,
19
+ isCurrentMomentValue,
20
+ type DataCubeOperationValue,
21
+ } from '../DataCubeQueryEngine.js';
18
22
  import type { DataCubeSnapshotFilterCondition } from '../DataCubeSnapshot.js';
19
23
  import type { DataCubeColumn } from '../model/DataCubeColumn.js';
20
24
  import { type V1_AppliedFunction } from '@finos/legend-graph';
@@ -52,6 +56,16 @@ export abstract class DataCubeQueryFilterOperation {
52
56
  ) {
53
57
  return undefined;
54
58
  }
59
+ // NOTE: `isCompatibleWithValue()` cannot tell whether a current-moment
60
+ // value (`today()`/`now()`) makes sense for the column it is compared
61
+ // against, since it does not have access to the column, so check here that
62
+ // the column actually carries a date.
63
+ if (
64
+ isCurrentMomentValue(value) &&
65
+ !getCurrentMomentValueType(column.type)
66
+ ) {
67
+ return undefined;
68
+ }
55
69
  return {
56
70
  ...column,
57
71
  operator: this.operator,
@@ -23,6 +23,7 @@ import {
23
23
  isPrimitiveType,
24
24
  ofDataType,
25
25
  _defaultPrimitiveTypeValue,
26
+ isCurrentMomentValue,
26
27
  type DataCubeOperationValue,
27
28
  } from '../DataCubeQueryEngine.js';
28
29
  import {
@@ -62,6 +63,12 @@ export class DataCubeQueryFilterOperation__Equal extends DataCubeQueryFilterOper
62
63
  }
63
64
 
64
65
  isCompatibleWithValue(value: DataCubeOperationValue) {
66
+ // current-moment values (`today()`/`now()`) are date values resolved at
67
+ // execution time; the column they apply to is checked in
68
+ // `_finalizeConditionSnapshot()`
69
+ if (isCurrentMomentValue(value)) {
70
+ return true;
71
+ }
65
72
  return (
66
73
  value.value !== undefined &&
67
74
  isPrimitiveType(value.type) &&