@oneuptime/common 10.0.85 → 10.0.88

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 (90) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +54 -0
  2. package/Models/DatabaseModels/GlobalConfig.ts +51 -0
  3. package/Server/API/EnterpriseLicenseAPI.ts +83 -0
  4. package/Server/API/GlobalConfigAPI.ts +59 -0
  5. package/Server/API/TelemetryAPI.ts +24 -0
  6. package/Server/EnvironmentConfig.ts +10 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.ts +59 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  9. package/Server/Infrastructure/Queue.ts +4 -4
  10. package/Server/Services/TelemetryAttributeService.ts +37 -3
  11. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +174 -7
  12. package/Tests/Types/Date.test.ts +46 -0
  13. package/Types/Date.ts +9 -4
  14. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +60 -21
  15. package/UI/Components/Dictionary/Dictionary.tsx +188 -26
  16. package/UI/Components/Dictionary/DictionaryFilterOperator.ts +357 -0
  17. package/UI/Components/Dictionary/DictionaryOfStrings.tsx +12 -7
  18. package/UI/Components/EditionLabel/EditionLabel.tsx +224 -10
  19. package/UI/Components/Filters/FilterViewer.tsx +81 -16
  20. package/UI/Components/Filters/FiltersForm.tsx +18 -3
  21. package/UI/Components/Filters/JSONFilter.tsx +11 -2
  22. package/UI/Components/Filters/Types/Filter.ts +3 -0
  23. package/UI/Components/Forms/Fields/FormField.tsx +6 -1
  24. package/UI/Components/Forms/Types/Field.ts +5 -0
  25. package/UI/Components/LogsViewer/LogsViewer.tsx +73 -4
  26. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +77 -31
  27. package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +44 -1
  28. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +7 -5
  29. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +6 -0
  30. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +84 -25
  31. package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +44 -1
  32. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +57 -0
  33. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  34. package/build/dist/Models/DatabaseModels/GlobalConfig.js +54 -0
  35. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  36. package/build/dist/Server/API/EnterpriseLicenseAPI.js +64 -1
  37. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  38. package/build/dist/Server/API/GlobalConfigAPI.js +47 -0
  39. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  40. package/build/dist/Server/API/TelemetryAPI.js +9 -0
  41. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  42. package/build/dist/Server/EnvironmentConfig.js +3 -0
  43. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  44. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js +26 -0
  45. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js.map +1 -0
  46. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  48. package/build/dist/Server/Infrastructure/Queue.js +3 -3
  49. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  50. package/build/dist/Server/Services/TelemetryAttributeService.js +36 -7
  51. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  52. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +135 -5
  53. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  54. package/build/dist/Tests/Types/Date.test.js +40 -0
  55. package/build/dist/Tests/Types/Date.test.js.map +1 -1
  56. package/build/dist/Types/Date.js +7 -2
  57. package/build/dist/Types/Date.js.map +1 -1
  58. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +21 -10
  59. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  60. package/build/dist/UI/Components/Dictionary/Dictionary.js +109 -16
  61. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  62. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +263 -0
  63. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -0
  64. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js +10 -6
  65. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js.map +1 -1
  66. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +124 -6
  67. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  68. package/build/dist/UI/Components/Filters/FilterViewer.js +50 -12
  69. package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
  70. package/build/dist/UI/Components/Filters/FiltersForm.js +5 -4
  71. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  72. package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
  73. package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
  74. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  75. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  76. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +54 -5
  77. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  78. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +59 -29
  79. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
  80. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +10 -2
  81. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -1
  82. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +2 -5
  83. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  84. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  85. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  86. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +59 -22
  87. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  88. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +10 -2
  89. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -1
  90. package/package.json +1 -1
@@ -11,6 +11,15 @@ import React, {
11
11
  } from "react";
12
12
  import AutocompleteTextInput from "../AutocompleteTextInput/AutocompleteTextInput";
13
13
  import FieldLabelElement from "../Forms/Fields/FieldLabel";
14
+ import {
15
+ DICTIONARY_FILTER_OPERATOR_OPTIONS,
16
+ DictionaryEntryValue,
17
+ DictionaryFilterOperator,
18
+ DictionaryFilterOperatorOption,
19
+ buildDictionaryValue,
20
+ detectOperatorFromValue,
21
+ getOperatorOption,
22
+ } from "./DictionaryFilterOperator";
14
23
 
15
24
  export enum ValueType {
16
25
  Text = "Text",
@@ -19,10 +28,8 @@ export enum ValueType {
19
28
  }
20
29
 
21
30
  export interface ComponentProps {
22
- onChange?:
23
- | undefined
24
- | ((value: Dictionary<string | boolean | number>) => void);
25
- initialValue?: Dictionary<string | boolean | number>;
31
+ onChange?: undefined | ((value: Dictionary<DictionaryEntryValue>) => void);
32
+ initialValue?: Dictionary<DictionaryEntryValue>;
26
33
  keyPlaceholder?: string;
27
34
  valuePlaceholder?: string;
28
35
  addButtonSuffix?: string | undefined;
@@ -30,12 +37,21 @@ export interface ComponentProps {
30
37
  keys?: Array<string> | undefined;
31
38
  valueSuggestions?: Record<string, Array<string>> | undefined;
32
39
  onKeySelected?: ((key: string) => void) | undefined;
40
+ isLoadingKeys?: boolean | undefined;
41
+ loadingValueKeys?: Array<string> | undefined;
42
+ /*
43
+ * When true, render an operator dropdown (=, !=, contains, etc.)
44
+ * between the key and value inputs. Defaults to false for backwards
45
+ * compatibility with simple key/value forms.
46
+ */
47
+ enableOperators?: boolean | undefined;
33
48
  }
34
49
 
35
50
  interface Item {
36
51
  key: string;
37
52
  value: string | number | boolean;
38
53
  type: ValueType;
54
+ operator: DictionaryFilterOperator;
39
55
  }
40
56
 
41
57
  const DictionaryForm: FunctionComponent<ComponentProps> = (
@@ -57,12 +73,10 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
57
73
  const [data, setData] = useState<Array<Item>>([]);
58
74
  const [isInitialValueSet, setIsInitialValueSet] = useState<boolean>(false);
59
75
 
60
- type UpdateDataFunction = (
61
- json: Dictionary<string | number | boolean>,
62
- ) => void;
76
+ type UpdateDataFunction = (json: Dictionary<DictionaryEntryValue>) => void;
63
77
 
64
78
  const updateData: UpdateDataFunction = (
65
- json: Dictionary<string | number | boolean>,
79
+ json: Dictionary<DictionaryEntryValue>,
66
80
  ): void => {
67
81
  const newData: Array<Item> = Object.keys(json).map((key: string) => {
68
82
  // check if the value type is in data
@@ -73,22 +87,46 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
73
87
 
74
88
  let valueType: ValueType = valueTypeInData || ValueType.Text;
75
89
 
90
+ const rawEntry: DictionaryEntryValue | undefined = json[key];
91
+
76
92
  if (!valueTypeInData) {
77
- if (typeof json[key] === "number") {
93
+ if (typeof rawEntry === "number") {
78
94
  valueType = ValueType.Number;
79
95
  }
80
96
 
81
- if (typeof json[key] === "boolean") {
97
+ if (typeof rawEntry === "boolean") {
82
98
  valueType = ValueType.Boolean;
83
99
  }
84
100
  }
85
101
 
86
- const value: string | number | boolean | undefined = json[key];
102
+ const detected: {
103
+ operator: DictionaryFilterOperator;
104
+ rawValue: string;
105
+ } = detectOperatorFromValue(rawEntry);
106
+
107
+ /*
108
+ * Restore typed values (number/boolean) for the form input from
109
+ * the stringified raw representation when the column type allows.
110
+ */
111
+ let restoredValue: string | number | boolean = detected.rawValue;
112
+ if (valueType === ValueType.Number && detected.rawValue !== "") {
113
+ const parsed: number = Number(detected.rawValue);
114
+ restoredValue = isNaN(parsed) ? detected.rawValue : parsed;
115
+ } else if (valueType === ValueType.Boolean) {
116
+ if (typeof rawEntry === "boolean") {
117
+ restoredValue = rawEntry;
118
+ } else if (detected.rawValue.toLowerCase() === "true") {
119
+ restoredValue = true;
120
+ } else if (detected.rawValue.toLowerCase() === "false") {
121
+ restoredValue = false;
122
+ }
123
+ }
87
124
 
88
125
  return {
89
126
  key: key,
90
- value: value === undefined || value === null ? "" : value,
127
+ value: restoredValue,
91
128
  type: valueType,
129
+ operator: detected.operator,
92
130
  };
93
131
  });
94
132
 
@@ -109,9 +147,35 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
109
147
  type OnDataChangeFunction = (data: Array<Item>) => void;
110
148
 
111
149
  const onDataChange: OnDataChangeFunction = (data: Array<Item>): void => {
112
- const result: Dictionary<string | number | boolean> = {};
150
+ const result: Dictionary<DictionaryEntryValue> = {};
113
151
  data.forEach((item: Item) => {
114
- result[item.key] = item.value;
152
+ /*
153
+ * Non-text types skip the operator system — the existing
154
+ * numeric/boolean inputs always represent equality.
155
+ */
156
+ if (item.type === ValueType.Number) {
157
+ result[item.key] = item.value as number;
158
+ return;
159
+ }
160
+ if (item.type === ValueType.Boolean) {
161
+ result[item.key] = item.value as boolean;
162
+ return;
163
+ }
164
+
165
+ const operatorOption: DictionaryFilterOperatorOption = getOperatorOption(
166
+ item.operator,
167
+ );
168
+ if (operatorOption.hidesValueInput) {
169
+ result[item.key] = buildDictionaryValue({
170
+ operator: item.operator,
171
+ rawValue: "",
172
+ });
173
+ return;
174
+ }
175
+ result[item.key] = buildDictionaryValue({
176
+ operator: item.operator,
177
+ rawValue: String(item.value ?? ""),
178
+ });
115
179
  });
116
180
  if (props.onChange) {
117
181
  props.onChange(result);
@@ -138,13 +202,36 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
138
202
  return "";
139
203
  };
140
204
 
205
+ const operatorDropdownOptions: Array<DropdownOption> =
206
+ DICTIONARY_FILTER_OPERATOR_OPTIONS.map(
207
+ (option: DictionaryFilterOperatorOption) => {
208
+ return {
209
+ label: option.symbol,
210
+ value: option.operator,
211
+ };
212
+ },
213
+ );
214
+
141
215
  return (
142
216
  <div>
143
217
  <div>
144
218
  {data.map((item: Item, index: number) => {
219
+ const operatorOption: DictionaryFilterOperatorOption =
220
+ getOperatorOption(item.operator);
221
+ const showOperatorSelector: boolean = Boolean(props.enableOperators);
222
+ const hideValueInput: boolean = Boolean(
223
+ showOperatorSelector && operatorOption.hidesValueInput,
224
+ );
225
+ const valueColumnClass: string = showOperatorSelector
226
+ ? "ml-1 w-2/5"
227
+ : "ml-1 w-1/2";
228
+ const keyColumnClass: string = showOperatorSelector
229
+ ? "mr-1 w-2/5"
230
+ : "mr-1 w-1/2";
231
+
145
232
  return (
146
233
  <div key={index} className="flex items-start mb-4 last:mb-0">
147
- <div className="mr-1 w-1/2">
234
+ <div className={keyColumnClass}>
148
235
  <div className="mb-1">
149
236
  <FieldLabelElement
150
237
  title="Key"
@@ -157,6 +244,8 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
157
244
  value={item.key}
158
245
  placeholder={props.keyPlaceholder}
159
246
  suggestions={props.keys}
247
+ isLoadingSuggestions={props.isLoadingKeys}
248
+ loadingMessage="Loading attributes..."
160
249
  onChange={(value: string) => {
161
250
  const newData: Array<Item> = [...data];
162
251
  newData[index]!.key = value;
@@ -171,9 +260,56 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
171
260
  />
172
261
  </div>
173
262
 
174
- <div className="mr-1 ml-1 flex items-center justify-center pt-8">
175
- <span className="text-slate-500 text-2xl leading-none">=</span>
176
- </div>
263
+ {showOperatorSelector ? (
264
+ <div className="mr-1 ml-1 w-1/5 min-w-[120px]">
265
+ <div className="mb-1">
266
+ <FieldLabelElement
267
+ title="Operator"
268
+ required={true}
269
+ hideOptionalLabel={true}
270
+ className="block text-xs text-gray-500 font-normal flex justify-between"
271
+ />
272
+ </div>
273
+ <Dropdown
274
+ value={operatorDropdownOptions.find(
275
+ (option: DropdownOption) => {
276
+ return option.value === item.operator;
277
+ },
278
+ )}
279
+ options={operatorDropdownOptions}
280
+ isMultiSelect={false}
281
+ onChange={(
282
+ selectedOption:
283
+ | DropdownValue
284
+ | Array<DropdownValue>
285
+ | null,
286
+ ) => {
287
+ const newOperator: DictionaryFilterOperator =
288
+ (selectedOption as DictionaryFilterOperator) ||
289
+ DictionaryFilterOperator.EqualTo;
290
+ const newOperatorOption: DictionaryFilterOperatorOption =
291
+ getOperatorOption(newOperator);
292
+ const newData: Array<Item> = [...data];
293
+ newData[index]!.operator = newOperator;
294
+ /*
295
+ * Reset the value when switching to a value-less
296
+ * operator so we don't ship stale text downstream.
297
+ */
298
+ if (newOperatorOption.hidesValueInput) {
299
+ newData[index]!.value = "";
300
+ }
301
+ setData(newData);
302
+ onDataChange(newData);
303
+ }}
304
+ />
305
+ </div>
306
+ ) : (
307
+ <div className="mr-1 ml-1 flex items-center justify-center pt-8">
308
+ <span className="text-slate-500 text-2xl leading-none">
309
+ =
310
+ </span>
311
+ </div>
312
+ )}
177
313
  {valueTypes.length > 1 && (
178
314
  <div className="ml-1 w-1/2">
179
315
  <div className="mb-1">
@@ -209,7 +345,7 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
209
345
  />
210
346
  </div>
211
347
  )}
212
- <div className="ml-1 w-1/2">
348
+ <div className={valueColumnClass}>
213
349
  <div className="mb-1">
214
350
  <FieldLabelElement
215
351
  title="Value"
@@ -218,15 +354,40 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
218
354
  className="block text-xs text-gray-500 font-normal flex justify-between"
219
355
  />
220
356
  </div>
221
- {item.type === ValueType.Text && (
357
+ {hideValueInput && (
358
+ <Input
359
+ value=""
360
+ placeholder={operatorOption.label}
361
+ disabled={true}
362
+ onChange={() => {
363
+ // no-op — IsEmpty/IsNotEmpty have no value
364
+ }}
365
+ />
366
+ )}
367
+ {!hideValueInput && item.type === ValueType.Text && (
222
368
  <AutocompleteTextInput
223
369
  value={item.value.toString()}
224
- placeholder={props.valuePlaceholder}
370
+ placeholder={
371
+ operatorOption.expectsNumericValue
372
+ ? "Number"
373
+ : props.valuePlaceholder
374
+ }
225
375
  suggestions={
226
- item.key && props.valueSuggestions?.[item.key]
227
- ? props.valueSuggestions[item.key]
228
- : undefined
376
+ operatorOption.expectsNumericValue
377
+ ? undefined
378
+ : item.key && props.valueSuggestions?.[item.key]
379
+ ? props.valueSuggestions[item.key]
380
+ : undefined
381
+ }
382
+ isLoadingSuggestions={
383
+ operatorOption.expectsNumericValue
384
+ ? false
385
+ : Boolean(
386
+ item.key &&
387
+ props.loadingValueKeys?.includes(item.key),
388
+ )
229
389
  }
390
+ loadingMessage="Loading values..."
230
391
  onChange={(value: string) => {
231
392
  const newData: Array<Item> = [...data];
232
393
  newData[index]!.value = value;
@@ -236,7 +397,7 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
236
397
  />
237
398
  )}
238
399
 
239
- {item.type === ValueType.Number && (
400
+ {!hideValueInput && item.type === ValueType.Number && (
240
401
  <Input
241
402
  value={item.value.toString()}
242
403
  placeholder={props.valuePlaceholder}
@@ -256,7 +417,7 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
256
417
  />
257
418
  )}
258
419
 
259
- {item.type === ValueType.Boolean && (
420
+ {!hideValueInput && item.type === ValueType.Boolean && (
260
421
  <Dropdown
261
422
  value={
262
423
  item.value === true
@@ -315,6 +476,7 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
315
476
  key: "",
316
477
  value: getDefaultValueForType(valueTypes[0] as ValueType),
317
478
  type: valueTypes[0] as ValueType,
479
+ operator: DictionaryFilterOperator.EqualTo,
318
480
  },
319
481
  ]);
320
482
  }}
@@ -0,0 +1,357 @@
1
+ import EqualTo from "../../../Types/BaseDatabase/EqualTo";
2
+ import NotEqual from "../../../Types/BaseDatabase/NotEqual";
3
+ import Search from "../../../Types/BaseDatabase/Search";
4
+ import NotContains from "../../../Types/BaseDatabase/NotContains";
5
+ import StartsWith from "../../../Types/BaseDatabase/StartsWith";
6
+ import EndsWith from "../../../Types/BaseDatabase/EndsWith";
7
+ import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
8
+ import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
9
+ import LessThan from "../../../Types/BaseDatabase/LessThan";
10
+ import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
11
+ import IsNull from "../../../Types/BaseDatabase/IsNull";
12
+ import NotNull from "../../../Types/BaseDatabase/NotNull";
13
+ import { ObjectType } from "../../../Types/JSON";
14
+
15
+ /*
16
+ * UI-facing operator identifiers. We store these in the Dictionary form
17
+ * state and translate to the corresponding backend operator wrapper at
18
+ * the API boundary so the existing serialization pipeline works.
19
+ */
20
+ export enum DictionaryFilterOperator {
21
+ EqualTo = "EqualTo",
22
+ NotEqual = "NotEqual",
23
+ Contains = "Contains",
24
+ NotContains = "NotContains",
25
+ StartsWith = "StartsWith",
26
+ EndsWith = "EndsWith",
27
+ GreaterThan = "GreaterThan",
28
+ LessThan = "LessThan",
29
+ GreaterThanOrEqual = "GreaterThanOrEqual",
30
+ LessThanOrEqual = "LessThanOrEqual",
31
+ IsEmpty = "IsEmpty",
32
+ IsNotEmpty = "IsNotEmpty",
33
+ }
34
+
35
+ export interface DictionaryFilterOperatorOption {
36
+ operator: DictionaryFilterOperator;
37
+ label: string;
38
+ symbol: string;
39
+ // Operators like IsEmpty/IsNotEmpty don't take a user-supplied value.
40
+ hidesValueInput?: boolean | undefined;
41
+ // Numeric operators force a numeric value input.
42
+ expectsNumericValue?: boolean | undefined;
43
+ }
44
+
45
+ export const DICTIONARY_FILTER_OPERATOR_OPTIONS: ReadonlyArray<DictionaryFilterOperatorOption> =
46
+ [
47
+ {
48
+ operator: DictionaryFilterOperator.EqualTo,
49
+ label: "equals",
50
+ symbol: "=",
51
+ },
52
+ {
53
+ operator: DictionaryFilterOperator.NotEqual,
54
+ label: "does not equal",
55
+ symbol: "!=",
56
+ },
57
+ {
58
+ operator: DictionaryFilterOperator.Contains,
59
+ label: "contains",
60
+ symbol: "contains",
61
+ },
62
+ {
63
+ operator: DictionaryFilterOperator.NotContains,
64
+ label: "does not contain",
65
+ symbol: "does not contain",
66
+ },
67
+ {
68
+ operator: DictionaryFilterOperator.StartsWith,
69
+ label: "starts with",
70
+ symbol: "starts with",
71
+ },
72
+ {
73
+ operator: DictionaryFilterOperator.EndsWith,
74
+ label: "ends with",
75
+ symbol: "ends with",
76
+ },
77
+ {
78
+ operator: DictionaryFilterOperator.GreaterThan,
79
+ label: "greater than",
80
+ symbol: ">",
81
+ expectsNumericValue: true,
82
+ },
83
+ {
84
+ operator: DictionaryFilterOperator.GreaterThanOrEqual,
85
+ label: "greater than or equal",
86
+ symbol: ">=",
87
+ expectsNumericValue: true,
88
+ },
89
+ {
90
+ operator: DictionaryFilterOperator.LessThan,
91
+ label: "less than",
92
+ symbol: "<",
93
+ expectsNumericValue: true,
94
+ },
95
+ {
96
+ operator: DictionaryFilterOperator.LessThanOrEqual,
97
+ label: "less than or equal",
98
+ symbol: "<=",
99
+ expectsNumericValue: true,
100
+ },
101
+ {
102
+ operator: DictionaryFilterOperator.IsEmpty,
103
+ label: "is empty",
104
+ symbol: "is empty",
105
+ hidesValueInput: true,
106
+ },
107
+ {
108
+ operator: DictionaryFilterOperator.IsNotEmpty,
109
+ label: "is not empty",
110
+ symbol: "is not empty",
111
+ hidesValueInput: true,
112
+ },
113
+ ];
114
+
115
+ export type DictionaryEntryValue =
116
+ | string
117
+ | number
118
+ | boolean
119
+ | EqualTo<string>
120
+ | NotEqual<string>
121
+ | Search<string>
122
+ | NotContains<string>
123
+ | StartsWith<string>
124
+ | EndsWith<string>
125
+ | GreaterThan<number>
126
+ | GreaterThanOrEqual<number>
127
+ | LessThan<number>
128
+ | LessThanOrEqual<number>
129
+ | IsNull
130
+ | NotNull;
131
+
132
+ export const getOperatorOption: (
133
+ operator: DictionaryFilterOperator,
134
+ ) => DictionaryFilterOperatorOption = (
135
+ operator: DictionaryFilterOperator,
136
+ ): DictionaryFilterOperatorOption => {
137
+ return (
138
+ DICTIONARY_FILTER_OPERATOR_OPTIONS.find(
139
+ (option: DictionaryFilterOperatorOption) => {
140
+ return option.operator === operator;
141
+ },
142
+ ) ?? DICTIONARY_FILTER_OPERATOR_OPTIONS[0]!
143
+ );
144
+ };
145
+
146
+ /*
147
+ * Detect operator wrapper instances or `_type`-tagged plain objects
148
+ * (already-deserialized vs raw-from-storage).
149
+ */
150
+ type ObjectTypeLike = { _type?: string };
151
+
152
+ const matchesObjectType: (value: unknown, type: ObjectType) => boolean = (
153
+ value: unknown,
154
+ type: ObjectType,
155
+ ): boolean => {
156
+ return Boolean(
157
+ value &&
158
+ typeof value === "object" &&
159
+ (value as ObjectTypeLike)._type === type,
160
+ );
161
+ };
162
+
163
+ interface RawValueAndOperator {
164
+ operator: DictionaryFilterOperator;
165
+ rawValue: string;
166
+ }
167
+
168
+ /**
169
+ * Inspect a stored dictionary entry value (which may be a plain string,
170
+ * a hydrated operator instance, or a raw `{_type, value}` JSON shape)
171
+ * and recover the operator + display value used to populate the form.
172
+ */
173
+ export const detectOperatorFromValue: (
174
+ value: unknown,
175
+ ) => RawValueAndOperator = (value: unknown): RawValueAndOperator => {
176
+ if (value === null || value === undefined) {
177
+ return {
178
+ operator: DictionaryFilterOperator.EqualTo,
179
+ rawValue: "",
180
+ };
181
+ }
182
+
183
+ if (
184
+ typeof value === "string" ||
185
+ typeof value === "number" ||
186
+ typeof value === "boolean"
187
+ ) {
188
+ return {
189
+ operator: DictionaryFilterOperator.EqualTo,
190
+ rawValue: String(value),
191
+ };
192
+ }
193
+
194
+ if (value instanceof IsNull || matchesObjectType(value, ObjectType.IsNull)) {
195
+ return { operator: DictionaryFilterOperator.IsEmpty, rawValue: "" };
196
+ }
197
+ if (
198
+ value instanceof NotNull ||
199
+ matchesObjectType(value, ObjectType.NotNull)
200
+ ) {
201
+ return { operator: DictionaryFilterOperator.IsNotEmpty, rawValue: "" };
202
+ }
203
+
204
+ const wrapperValue: string =
205
+ value instanceof Object && "value" in value
206
+ ? String(
207
+ (value as { value?: unknown }).value === undefined ||
208
+ (value as { value?: unknown }).value === null
209
+ ? ""
210
+ : (value as { value?: unknown }).value,
211
+ )
212
+ : "";
213
+
214
+ if (
215
+ value instanceof NotEqual ||
216
+ matchesObjectType(value, ObjectType.NotEqual)
217
+ ) {
218
+ return {
219
+ operator: DictionaryFilterOperator.NotEqual,
220
+ rawValue: wrapperValue,
221
+ };
222
+ }
223
+ if (
224
+ value instanceof EqualTo ||
225
+ matchesObjectType(value, ObjectType.EqualTo)
226
+ ) {
227
+ return {
228
+ operator: DictionaryFilterOperator.EqualTo,
229
+ rawValue: wrapperValue,
230
+ };
231
+ }
232
+ if (value instanceof Search || matchesObjectType(value, ObjectType.Search)) {
233
+ return {
234
+ operator: DictionaryFilterOperator.Contains,
235
+ rawValue: wrapperValue,
236
+ };
237
+ }
238
+ if (
239
+ value instanceof NotContains ||
240
+ matchesObjectType(value, ObjectType.NotContains)
241
+ ) {
242
+ return {
243
+ operator: DictionaryFilterOperator.NotContains,
244
+ rawValue: wrapperValue,
245
+ };
246
+ }
247
+ if (
248
+ value instanceof StartsWith ||
249
+ matchesObjectType(value, ObjectType.StartsWith)
250
+ ) {
251
+ return {
252
+ operator: DictionaryFilterOperator.StartsWith,
253
+ rawValue: wrapperValue,
254
+ };
255
+ }
256
+ if (
257
+ value instanceof EndsWith ||
258
+ matchesObjectType(value, ObjectType.EndsWith)
259
+ ) {
260
+ return {
261
+ operator: DictionaryFilterOperator.EndsWith,
262
+ rawValue: wrapperValue,
263
+ };
264
+ }
265
+ if (
266
+ value instanceof GreaterThan ||
267
+ matchesObjectType(value, ObjectType.GreaterThan)
268
+ ) {
269
+ return {
270
+ operator: DictionaryFilterOperator.GreaterThan,
271
+ rawValue: wrapperValue,
272
+ };
273
+ }
274
+ if (
275
+ value instanceof GreaterThanOrEqual ||
276
+ matchesObjectType(value, ObjectType.GreaterThanOrEqual)
277
+ ) {
278
+ return {
279
+ operator: DictionaryFilterOperator.GreaterThanOrEqual,
280
+ rawValue: wrapperValue,
281
+ };
282
+ }
283
+ if (
284
+ value instanceof LessThan ||
285
+ matchesObjectType(value, ObjectType.LessThan)
286
+ ) {
287
+ return {
288
+ operator: DictionaryFilterOperator.LessThan,
289
+ rawValue: wrapperValue,
290
+ };
291
+ }
292
+ if (
293
+ value instanceof LessThanOrEqual ||
294
+ matchesObjectType(value, ObjectType.LessThanOrEqual)
295
+ ) {
296
+ return {
297
+ operator: DictionaryFilterOperator.LessThanOrEqual,
298
+ rawValue: wrapperValue,
299
+ };
300
+ }
301
+
302
+ // Unknown structure — fall back to bare equality with stringified value.
303
+ return {
304
+ operator: DictionaryFilterOperator.EqualTo,
305
+ rawValue: wrapperValue,
306
+ };
307
+ };
308
+
309
+ /**
310
+ * Build the actual stored dictionary value for an operator + raw value.
311
+ * `EqualTo` produces a bare string for backwards compatibility with
312
+ * existing saved filters; everything else produces an operator wrapper
313
+ * instance.
314
+ */
315
+ export const buildDictionaryValue: (input: {
316
+ operator: DictionaryFilterOperator;
317
+ rawValue: string;
318
+ }) => DictionaryEntryValue = (input: {
319
+ operator: DictionaryFilterOperator;
320
+ rawValue: string;
321
+ }): DictionaryEntryValue => {
322
+ const { operator, rawValue } = input;
323
+ const trimmed: string = rawValue ?? "";
324
+
325
+ switch (operator) {
326
+ case DictionaryFilterOperator.EqualTo:
327
+ return trimmed;
328
+ case DictionaryFilterOperator.NotEqual:
329
+ return new NotEqual<string>(trimmed);
330
+ case DictionaryFilterOperator.Contains:
331
+ /*
332
+ * Statement.serialize already wraps Search instances with `%...%`,
333
+ * so pass the bare needle here.
334
+ */
335
+ return new Search<string>(trimmed);
336
+ case DictionaryFilterOperator.NotContains:
337
+ return new NotContains<string>(trimmed);
338
+ case DictionaryFilterOperator.StartsWith:
339
+ return new StartsWith<string>(trimmed);
340
+ case DictionaryFilterOperator.EndsWith:
341
+ return new EndsWith<string>(trimmed);
342
+ case DictionaryFilterOperator.GreaterThan:
343
+ return new GreaterThan<number>(Number(trimmed));
344
+ case DictionaryFilterOperator.GreaterThanOrEqual:
345
+ return new GreaterThanOrEqual<number>(Number(trimmed));
346
+ case DictionaryFilterOperator.LessThan:
347
+ return new LessThan<number>(Number(trimmed));
348
+ case DictionaryFilterOperator.LessThanOrEqual:
349
+ return new LessThanOrEqual<number>(Number(trimmed));
350
+ case DictionaryFilterOperator.IsEmpty:
351
+ return new IsNull();
352
+ case DictionaryFilterOperator.IsNotEmpty:
353
+ return new NotNull();
354
+ default:
355
+ return trimmed;
356
+ }
357
+ };