@adaptabletools/adaptable 24.0.0-canary.9 → 24.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/devtools.d.ts +13 -0
- package/index.css +76 -76
- package/package.json +1 -1
- package/src/Api/ThemeApi.d.ts +1 -1
- package/src/env.js +2 -2
- package/src/migration/VersionUpgrade24.d.ts +44 -9
- package/src/migration/VersionUpgrade24.js +97 -25
- package/src/migration/legacyPredicateLowering.d.ts +1 -1
- package/src/migration/legacyPredicateLowering.js +2 -0
- package/src/migration/predicateExpressionMigration.d.ts +49 -12
- package/src/migration/predicateExpressionMigration.js +141 -10
- package/src/setupDevTools.js +106 -170
- package/tsconfig.esm.tsbuildinfo +1 -1
- package/vendor/components/index.css +71 -71
|
@@ -9,6 +9,13 @@ import { badgePredicateToBooleanExpression, columnFilterPredicatesToExpression,
|
|
|
9
9
|
import { migrateGreenfieldExpressionTexts } from './migrateGreenfieldExpressionTexts';
|
|
10
10
|
import { isAdaptableSharedEntity } from '../AdaptableState/TeamSharingState';
|
|
11
11
|
import { migrateLayoutTagsToRegistry } from './migrateLayoutTagsToRegistry';
|
|
12
|
+
const UNCONVERTED_PREDICATE_OUTCOME = {
|
|
13
|
+
'Column Filter': 'they keep their legacy Predicates, which are ignored, so the filter is inactive',
|
|
14
|
+
Alert: 'their Rule becomes empty, so the Alert never fires',
|
|
15
|
+
'Flashing Cell': 'their Rule becomes empty, so the Flashing Cell never flashes',
|
|
16
|
+
'Format Column': 'their Rule is removed, so the format applies to every cell in Scope',
|
|
17
|
+
'Badge Style': 'the Badge has no Rule, so it is shown as the fallback (always) Badge',
|
|
18
|
+
};
|
|
12
19
|
const NEEDS_MIGRATION_SUFFIX = ' (needs migration)';
|
|
13
20
|
const LEGACY_SOURCE_PATTERN = new RegExp(`\\b(${[...LEGACY_OBSERVABLE_SOURCE_NAMES].join('|')})\\s*[({]`);
|
|
14
21
|
export class VersionUpgrade24 extends VersionUpgrade {
|
|
@@ -17,13 +24,19 @@ export class VersionUpgrade24 extends VersionUpgrade {
|
|
|
17
24
|
super(logger);
|
|
18
25
|
this.options = options;
|
|
19
26
|
}
|
|
27
|
+
customFunctionUsages = new Map();
|
|
28
|
+
percentChangeObjects = [];
|
|
20
29
|
migrateState(state) {
|
|
30
|
+
this.customFunctionUsages.clear();
|
|
31
|
+
this.percentChangeObjects = [];
|
|
21
32
|
this.migrateNoteTimestamps(state);
|
|
22
33
|
this.migrateColumnFilters(state);
|
|
23
34
|
this.migrateAlertRules(state);
|
|
24
35
|
this.migrateFlashingCellRules(state);
|
|
25
36
|
this.migrateFormatColumnRules(state);
|
|
26
37
|
this.migrateBadgeStyleRules(state);
|
|
38
|
+
this.logCustomPredicateFunctions();
|
|
39
|
+
this.logPercentChangeApproximations();
|
|
27
40
|
this.migrateExpressionQueryKeys(state);
|
|
28
41
|
this.migrateNamedQueries(state);
|
|
29
42
|
this.migrateCalculatedColumnTypes(state);
|
|
@@ -92,13 +105,14 @@ export class VersionUpgrade24 extends VersionUpgrade {
|
|
|
92
105
|
return;
|
|
93
106
|
}
|
|
94
107
|
let migratedCount = 0;
|
|
95
|
-
|
|
108
|
+
const preserved = [];
|
|
96
109
|
for (const layout of layouts) {
|
|
97
110
|
const filters = layout.ColumnFilters;
|
|
98
111
|
if (!filters?.length) {
|
|
99
112
|
continue;
|
|
100
113
|
}
|
|
101
114
|
for (const filter of filters) {
|
|
115
|
+
const label = `Column Filter '${filter.ColumnId}' (Layout '${layout.Name}')`;
|
|
102
116
|
if (filter.Expression) {
|
|
103
117
|
delete filter.Predicates;
|
|
104
118
|
delete filter.PredicatesOperator;
|
|
@@ -108,12 +122,15 @@ export class VersionUpgrade24 extends VersionUpgrade {
|
|
|
108
122
|
if (!filter.Predicates?.length) {
|
|
109
123
|
continue;
|
|
110
124
|
}
|
|
111
|
-
const
|
|
125
|
+
const customCalls = [];
|
|
126
|
+
const expression = columnFilterPredicatesToExpression(filter, customCalls);
|
|
112
127
|
if (expression != null) {
|
|
113
128
|
filter.Expression = expression;
|
|
129
|
+
this.recordPercentChange(filter.Predicates, label);
|
|
114
130
|
delete filter.PreferredFilter;
|
|
115
131
|
delete filter.Predicates;
|
|
116
132
|
delete filter.PredicatesOperator;
|
|
133
|
+
this.recordCustomFunctionCalls(customCalls, label);
|
|
117
134
|
migratedCount++;
|
|
118
135
|
continue;
|
|
119
136
|
}
|
|
@@ -126,10 +143,10 @@ export class VersionUpgrade24 extends VersionUpgrade {
|
|
|
126
143
|
migratedCount++;
|
|
127
144
|
continue;
|
|
128
145
|
}
|
|
129
|
-
|
|
146
|
+
preserved.push(this.describePredicates(label, filter.Predicates));
|
|
130
147
|
}
|
|
131
148
|
}
|
|
132
|
-
this.logPredicateMigrationSummary('Column Filter', migratedCount,
|
|
149
|
+
this.logPredicateMigrationSummary('Column Filter', migratedCount, preserved);
|
|
133
150
|
}
|
|
134
151
|
migrateAlertRules(state) {
|
|
135
152
|
this.migratePredicateRulesInDefinitions(state.Alert?.AlertDefinitions, 'Alert', (definition) => definition.Rule);
|
|
@@ -146,51 +163,58 @@ export class VersionUpgrade24 extends VersionUpgrade {
|
|
|
146
163
|
return;
|
|
147
164
|
}
|
|
148
165
|
let migratedCount = 0;
|
|
149
|
-
|
|
166
|
+
const preserved = [];
|
|
150
167
|
for (const styledColumn of columns) {
|
|
151
168
|
const badges = styledColumn.BadgeStyle?.Badges;
|
|
152
169
|
if (!badges?.length) {
|
|
153
170
|
continue;
|
|
154
171
|
}
|
|
155
|
-
|
|
172
|
+
badges.forEach((badge, index) => {
|
|
156
173
|
const legacyExpression = badge.Expression;
|
|
157
174
|
if (badge.Rule?.Expression || legacyExpression?.Expression || legacyExpression?.BooleanExpression) {
|
|
158
175
|
delete badge.Predicate;
|
|
159
|
-
|
|
176
|
+
return;
|
|
160
177
|
}
|
|
161
178
|
if (!badge.Predicate?.PredicateId) {
|
|
162
|
-
|
|
179
|
+
return;
|
|
163
180
|
}
|
|
164
|
-
const
|
|
181
|
+
const label = `Badge ${index + 1} of Styled Column '${styledColumn.Name ?? styledColumn.ColumnId}'`;
|
|
182
|
+
const customCalls = [];
|
|
183
|
+
const expression = badgePredicateToBooleanExpression(badge.Predicate, styledColumn.ColumnId, customCalls);
|
|
165
184
|
if (expression == null) {
|
|
166
|
-
|
|
167
|
-
|
|
185
|
+
preserved.push(this.describePredicates(label, [badge.Predicate]));
|
|
186
|
+
return;
|
|
168
187
|
}
|
|
169
188
|
badge.Rule = { Expression: expression };
|
|
189
|
+
this.recordPercentChange([badge.Predicate], label);
|
|
170
190
|
delete badge.Predicate;
|
|
191
|
+
this.recordCustomFunctionCalls(customCalls, label);
|
|
171
192
|
migratedCount++;
|
|
172
|
-
}
|
|
193
|
+
});
|
|
173
194
|
}
|
|
174
|
-
this.logPredicateMigrationSummary('Badge Style', migratedCount,
|
|
195
|
+
this.logPredicateMigrationSummary('Badge Style', migratedCount, preserved);
|
|
175
196
|
}
|
|
176
197
|
migratePredicateRulesInDefinitions(definitions, moduleLabel, getRule) {
|
|
177
198
|
if (!definitions?.length) {
|
|
178
199
|
return;
|
|
179
200
|
}
|
|
180
201
|
let migratedCount = 0;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
202
|
+
const preserved = [];
|
|
203
|
+
definitions.forEach((definition, index) => {
|
|
204
|
+
const name = definition.Name;
|
|
205
|
+
const label = `${moduleLabel} '${typeof name === 'string' && name ? name : `#${index + 1}`}'`;
|
|
206
|
+
const rule = getRule(definition);
|
|
207
|
+
const result = this.migratePredicateRule(rule, label);
|
|
184
208
|
if (result === 'migrated') {
|
|
185
209
|
migratedCount++;
|
|
186
210
|
}
|
|
187
211
|
if (result === 'preserved') {
|
|
188
|
-
|
|
212
|
+
preserved.push(this.describePredicates(label, rule?.Predicates));
|
|
189
213
|
}
|
|
190
|
-
}
|
|
191
|
-
this.logPredicateMigrationSummary(moduleLabel, migratedCount,
|
|
214
|
+
});
|
|
215
|
+
this.logPredicateMigrationSummary(moduleLabel, migratedCount, preserved);
|
|
192
216
|
}
|
|
193
|
-
migratePredicateRule(rule) {
|
|
217
|
+
migratePredicateRule(rule, label) {
|
|
194
218
|
if (!isLegacyAdaptableQuery(rule)) {
|
|
195
219
|
return 'skip';
|
|
196
220
|
}
|
|
@@ -209,12 +233,15 @@ export class VersionUpgrade24 extends VersionUpgrade {
|
|
|
209
233
|
if (!rule.Predicates?.length) {
|
|
210
234
|
return 'skip';
|
|
211
235
|
}
|
|
212
|
-
const
|
|
236
|
+
const customCalls = [];
|
|
237
|
+
const expression = rulePredicatesToBooleanExpression(rule.Predicates, customCalls);
|
|
213
238
|
if (expression == null) {
|
|
214
239
|
return 'preserved';
|
|
215
240
|
}
|
|
241
|
+
this.recordPercentChange(rule.Predicates, label);
|
|
216
242
|
delete rule.Predicates;
|
|
217
243
|
rule.Expression = expression;
|
|
244
|
+
this.recordCustomFunctionCalls(customCalls, label);
|
|
218
245
|
return 'migrated';
|
|
219
246
|
}
|
|
220
247
|
migrateExpressionQueryKeys(state) {
|
|
@@ -397,12 +424,57 @@ export class VersionUpgrade24 extends VersionUpgrade {
|
|
|
397
424
|
if (migrated > 0) {
|
|
398
425
|
this.logger.info(`Migrated ${migrated} ${module} predicate rule(s) to AdaptableQL Expression (v24).`);
|
|
399
426
|
}
|
|
400
|
-
if (preserved > 0) {
|
|
401
|
-
this.logger.warn(`
|
|
402
|
-
`
|
|
403
|
-
|
|
427
|
+
if (preserved.length > 0) {
|
|
428
|
+
this.logger.warn(`Could not convert ${preserved.length} ${module} predicate rule(s) to an AdaptableQL ` +
|
|
429
|
+
`Expression (v24): ${preserved.join('; ')}. Predicates are not evaluated in v24: ` +
|
|
430
|
+
`${UNCONVERTED_PREDICATE_OUTCOME[module]}. Re-author them as Expressions.`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
describePredicates(label, predicates) {
|
|
434
|
+
const ids = (predicates ?? []).map((predicate) => predicate?.PredicateId ?? '?');
|
|
435
|
+
return ids.length ? `${label} (${ids.join(', ')})` : label;
|
|
436
|
+
}
|
|
437
|
+
recordCustomFunctionCalls(calls, label) {
|
|
438
|
+
for (const call of calls) {
|
|
439
|
+
let usage = this.customFunctionUsages.get(call.functionName);
|
|
440
|
+
if (!usage) {
|
|
441
|
+
usage = { predicateIds: new Set(), maxInputCount: 0, objects: [] };
|
|
442
|
+
this.customFunctionUsages.set(call.functionName, usage);
|
|
443
|
+
}
|
|
444
|
+
usage.predicateIds.add(call.predicateId);
|
|
445
|
+
usage.maxInputCount = Math.max(usage.maxInputCount, call.inputCount);
|
|
446
|
+
if (!usage.objects.includes(label)) {
|
|
447
|
+
usage.objects.push(label);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
recordPercentChange(predicates, label) {
|
|
452
|
+
if (predicates?.some((predicate) => predicate?.PredicateId === 'PercentChange') &&
|
|
453
|
+
!this.percentChangeObjects.includes(label)) {
|
|
454
|
+
this.percentChangeObjects.push(label);
|
|
404
455
|
}
|
|
405
456
|
}
|
|
457
|
+
logPercentChangeApproximations() {
|
|
458
|
+
if (!this.percentChangeObjects.length) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
this.logger.warn(`PercentChange Predicate migrated to PERCENT_CHANGE(...) > n in: ${this.percentChangeObjects.join(', ')} (v24). ` +
|
|
462
|
+
`PERCENT_CHANGE divides the change by the previous value; PercentChange divided it by the smaller ` +
|
|
463
|
+
`of the previous and new values. Rises give the same percentage, but falls give a smaller one ` +
|
|
464
|
+
`(a fall from 100 to 94 was 6.38% and is now 6%), so these rules can fire less often on falls; ` +
|
|
465
|
+
`lower the threshold if needed.`);
|
|
466
|
+
}
|
|
467
|
+
logCustomPredicateFunctions() {
|
|
468
|
+
this.customFunctionUsages.forEach((usage, functionName) => {
|
|
469
|
+
const ids = [...usage.predicateIds].map((id) => `'${id}'`).join(', ');
|
|
470
|
+
const args = usage.maxInputCount > 0
|
|
471
|
+
? `args[0] is the Column value, args[1..${usage.maxInputCount}] are the former Predicate Inputs`
|
|
472
|
+
: 'args[0] is the Column value';
|
|
473
|
+
this.logger.info(`Custom Predicate ${ids} migrated to ${functionName}(...) in: ${usage.objects.join(', ')} (v24). ` +
|
|
474
|
+
`Define ${functionName} in expressionOptions.customStandardFunctions with returnType ` +
|
|
475
|
+
`'boolean'; ${args}. Until it is defined these rules are invalid and are not applied.`);
|
|
476
|
+
});
|
|
477
|
+
}
|
|
406
478
|
migrateQueryWrappersToStrings(state) {
|
|
407
479
|
let migratedCount = 0;
|
|
408
480
|
const textOf = (value) => {
|
|
@@ -13,7 +13,7 @@ export type LegacyPredicate = {
|
|
|
13
13
|
/**
|
|
14
14
|
* Maps a legacy predicate id + raw inputs to the equivalent AQL
|
|
15
15
|
* `QlPredicate` (primitive or AND/OR group). Returns `null` for custom
|
|
16
|
-
* defs
|
|
16
|
+
* defs (lowered to custom function calls by `predicateExpressionMigration`).
|
|
17
17
|
*/
|
|
18
18
|
export declare const predicateIdToQlPredicate: (predicateId: string, inputs: unknown[] | undefined, column: LegacyPredicateColumn) => QlPredicate | null;
|
|
19
19
|
/**
|
|
@@ -134,6 +134,8 @@ export const predicateIdToQlPredicate = (predicateId, inputs, column) => {
|
|
|
134
134
|
}
|
|
135
135
|
case 'AnyChange':
|
|
136
136
|
return qlPrimitive('ANY_CHANGE', [col]);
|
|
137
|
+
case 'PercentChange':
|
|
138
|
+
return qlPrimitive('GT', [qlPrimitive('PERCENT_CHANGE', [col]), ...literals.slice(0, 1)]);
|
|
137
139
|
default:
|
|
138
140
|
return null;
|
|
139
141
|
}
|
|
@@ -16,14 +16,23 @@
|
|
|
16
16
|
* (they evaluate against the scoped column at runtime, exactly what
|
|
17
17
|
* `$SCOPE` resolves to).
|
|
18
18
|
*
|
|
19
|
+
* Custom predicate ids (user `customPredicateDefs`) lower to a call to the
|
|
20
|
+
* custom function that replaces them in v24: the id in UPPER_SNAKE form,
|
|
21
|
+
* the subject first and the stored `Inputs` after it
|
|
22
|
+
* (`largeNotional` → `LARGE_NOTIONAL([notional])`). The function itself is
|
|
23
|
+
* not checked — it must be defined in `customStandardFunctions` at runtime,
|
|
24
|
+
* otherwise the rule is invalid (logged, never applied).
|
|
25
|
+
*
|
|
19
26
|
* Every helper is **all-or-nothing**: if any predicate in the set cannot
|
|
20
|
-
* be lowered to a complete AQL primitive (custom
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* the runtime fallback and keeps the migration idempotent.
|
|
27
|
+
* be lowered to a complete AQL primitive (a custom id with no valid or
|
|
28
|
+
* non-clashing function name, or an incomplete/empty predicate) the helper
|
|
29
|
+
* returns `null` and the caller must leave the legacy predicate
|
|
30
|
+
* representation intact. This keeps the migration idempotent.
|
|
25
31
|
*
|
|
26
|
-
* `AnyChange` lowers to `ANY_CHANGE([col])` / `ANY_CHANGE($SCOPE)
|
|
32
|
+
* `AnyChange` lowers to `ANY_CHANGE([col])` / `ANY_CHANGE($SCOPE)`;
|
|
33
|
+
* `PercentChange` to `PERCENT_CHANGE([col]) > n`, which divides the change
|
|
34
|
+
* by the previous value where the predicate divided it by the smaller of
|
|
35
|
+
* the previous and new values.
|
|
27
36
|
*/
|
|
28
37
|
import { ColumnFilter } from '../AdaptableState/Common/ColumnFilter';
|
|
29
38
|
import { LegacyPredicate } from './legacyPredicateLowering';
|
|
@@ -32,15 +41,37 @@ import { QlPredicate, QlPrimitive } from "../../vendor/aql/index.js";
|
|
|
32
41
|
type LegacyColumnPredicate = LegacyPredicate & {
|
|
33
42
|
ColumnId?: string;
|
|
34
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* A custom predicate that was lowered to a call to the custom function
|
|
46
|
+
* replacing it.
|
|
47
|
+
*/
|
|
48
|
+
export interface CustomPredicateFunctionCall {
|
|
49
|
+
predicateId: string;
|
|
50
|
+
functionName: string;
|
|
51
|
+
/** Number of stored `Inputs`, passed after the subject (`args[1..]`). */
|
|
52
|
+
inputCount: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The custom function name a custom predicate id migrates to: the id in
|
|
56
|
+
* UPPER_SNAKE form (`largeNotional` → `LARGE_NOTIONAL`, `isUSDTrade` →
|
|
57
|
+
* `IS_USD_TRADE`, `top-tier` → `TOP_TIER`).
|
|
58
|
+
*
|
|
59
|
+
* @returns the name, or `null` when the id is a v23 system predicate, the
|
|
60
|
+
* name is not a valid AQL function name, or it clashes with a system
|
|
61
|
+
* function (the rule would silently call the system function).
|
|
62
|
+
*/
|
|
63
|
+
export declare const customPredicateIdToFunctionName: (predicateId: string) => string | null;
|
|
35
64
|
/**
|
|
36
65
|
* Lowers a single legacy predicate to a complete AQL `QlPredicate`.
|
|
37
66
|
*
|
|
38
67
|
* @param subjectColumnId the column the predicate applies to, or `null`
|
|
39
68
|
* to emit a `$SCOPE` subject (rule predicate without a `ColumnId`).
|
|
40
|
-
* @
|
|
41
|
-
*
|
|
69
|
+
* @param customCalls receives the custom function call when the predicate
|
|
70
|
+
* is a custom predicate.
|
|
71
|
+
* @returns the predicate, or `null` when the predicate has no AQL
|
|
72
|
+
* equivalent or is incomplete.
|
|
42
73
|
*/
|
|
43
|
-
export declare const lowerPredicateToQlPredicate: (predicate: LegacyPredicate, subjectColumnId: string | null) => QlPredicate | null;
|
|
74
|
+
export declare const lowerPredicateToQlPredicate: (predicate: LegacyPredicate, subjectColumnId: string | null, customCalls?: CustomPredicateFunctionCall[]) => QlPredicate | null;
|
|
44
75
|
/** @deprecated Use {@link lowerPredicateToQlPredicate} — range predicates lower to groups. */
|
|
45
76
|
export declare const lowerPredicateToPrimitive: (predicate: LegacyPredicate, subjectColumnId: string | null) => QlPrimitive | null;
|
|
46
77
|
/**
|
|
@@ -50,8 +81,10 @@ export declare const lowerPredicateToPrimitive: (predicate: LegacyPredicate, sub
|
|
|
50
81
|
*
|
|
51
82
|
* Returns `null` when the filter has no predicates or any predicate is
|
|
52
83
|
* unconvertible — the caller keeps the legacy `Predicates`.
|
|
84
|
+
*
|
|
85
|
+
* @param customCalls receives the custom function calls of a converted filter.
|
|
53
86
|
*/
|
|
54
|
-
export declare const columnFilterPredicatesToExpression: (columnFilter: ColumnFilter) => string | null;
|
|
87
|
+
export declare const columnFilterPredicatesToExpression: (columnFilter: ColumnFilter, customCalls?: CustomPredicateFunctionCall[]) => string | null;
|
|
55
88
|
/**
|
|
56
89
|
* Converts a single incomplete ColumnFilter Predicate (empty Inputs —
|
|
57
90
|
* operator seed only) to a `PreferredFilter` AQL operator name.
|
|
@@ -67,11 +100,15 @@ export declare const columnFilterPredicatesToPreferredFilter: (columnFilter: Col
|
|
|
67
100
|
* predicate uses its own `ColumnId`, or `$SCOPE` when absent.
|
|
68
101
|
*
|
|
69
102
|
* Returns `null` when there are no predicates or any is unconvertible.
|
|
103
|
+
*
|
|
104
|
+
* @param customCalls receives the custom function calls of a converted rule.
|
|
70
105
|
*/
|
|
71
|
-
export declare const rulePredicatesToBooleanExpression: (predicates: LegacyColumnPredicate[] | undefined) => string | null;
|
|
106
|
+
export declare const rulePredicatesToBooleanExpression: (predicates: LegacyColumnPredicate[] | undefined, customCalls?: CustomPredicateFunctionCall[]) => string | null;
|
|
72
107
|
/**
|
|
73
108
|
* Lowers a single Badge Style `Predicate` (subject = the styled column)
|
|
74
109
|
* to a `BooleanExpression`. Returns `null` when unconvertible.
|
|
110
|
+
*
|
|
111
|
+
* @param customCalls receives the custom function call of a converted badge.
|
|
75
112
|
*/
|
|
76
|
-
export declare const badgePredicateToBooleanExpression: (predicate: LegacyPredicate, columnId: string) => string | null;
|
|
113
|
+
export declare const badgePredicateToBooleanExpression: (predicate: LegacyPredicate, columnId: string, customCalls?: CustomPredicateFunctionCall[]) => string | null;
|
|
77
114
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { systemScalarExpressionFunctions as scalarExpressionFunctions } from '../aql/systemExpressionFunctions';
|
|
1
|
+
import { systemAggregatedExpressionFunctions, systemAggregationClauseFunctions, systemObservableExpressionFunctions, systemScalarExpressionFunctions as scalarExpressionFunctions, } from '../aql/systemExpressionFunctions';
|
|
2
2
|
import { predicateIdToQlPredicate, } from './legacyPredicateLowering';
|
|
3
|
-
import { isQlColRef, isQlPredicateComplete, mapQlPredicateToExpression, migrateExpression, } from "../../vendor/aql/index.js";
|
|
3
|
+
import { AGGREGATION_ONLY_NAMES, colRef, DUAL_AGGREGATION_NAMES, isAST_Function, isQlColRef, isQlPredicateComplete, LEGACY_OBSERVABLE_MARKER_NAMES, LEGACY_OBSERVABLE_SOURCE_NAMES, literalForRawValue, mapQlPredicateToExpression, migrateExpression, OBSERVABLE_SOURCE_NAMES, OPERAND_CLAUSE_NAMES, parse, qlPrimitive, } from "../../vendor/aql/index.js";
|
|
4
4
|
const MIGRATION_FUNCTION_POOL = {
|
|
5
5
|
scalarFunctions: scalarExpressionFunctions,
|
|
6
6
|
};
|
|
@@ -33,12 +33,14 @@ const NUMBER_PREDICATE_IDS = new Set([
|
|
|
33
33
|
'Positive',
|
|
34
34
|
'Negative',
|
|
35
35
|
'Zero',
|
|
36
|
+
'PercentChange',
|
|
36
37
|
]);
|
|
37
38
|
const NUMERIC_INPUT_PREDICATE_IDS = new Set([
|
|
38
39
|
'GreaterThan',
|
|
39
40
|
'LessThan',
|
|
40
41
|
'Between',
|
|
41
42
|
'NotBetween',
|
|
43
|
+
'PercentChange',
|
|
42
44
|
]);
|
|
43
45
|
const BOOLEAN_PREDICATE_IDS = new Set(['True', 'False', 'BooleanToggle']);
|
|
44
46
|
const SCOPE_REF = { kind: 'scope' };
|
|
@@ -92,14 +94,139 @@ const withScopeSubject = (predicate) => {
|
|
|
92
94
|
}),
|
|
93
95
|
};
|
|
94
96
|
};
|
|
95
|
-
|
|
97
|
+
const SYSTEM_PREDICATE_IDS = new Set([
|
|
98
|
+
'After',
|
|
99
|
+
'AnyChange',
|
|
100
|
+
'Before',
|
|
101
|
+
'Between',
|
|
102
|
+
'Blanks',
|
|
103
|
+
'BooleanToggle',
|
|
104
|
+
'Contains',
|
|
105
|
+
'EndsWith',
|
|
106
|
+
'Equals',
|
|
107
|
+
'False',
|
|
108
|
+
'GreaterThan',
|
|
109
|
+
'GreaterThanOrEqual',
|
|
110
|
+
'Holiday',
|
|
111
|
+
'In',
|
|
112
|
+
'InFuture',
|
|
113
|
+
'InPast',
|
|
114
|
+
'InRange',
|
|
115
|
+
'Is',
|
|
116
|
+
'IsNot',
|
|
117
|
+
'LastWorkDay',
|
|
118
|
+
'LessThan',
|
|
119
|
+
'LessThanOrEqual',
|
|
120
|
+
'Negative',
|
|
121
|
+
'NextWorkDay',
|
|
122
|
+
'NonBlanks',
|
|
123
|
+
'NotBetween',
|
|
124
|
+
'NotContains',
|
|
125
|
+
'NotEquals',
|
|
126
|
+
'NotIn',
|
|
127
|
+
'NotOn',
|
|
128
|
+
'On',
|
|
129
|
+
'OnOrAfter',
|
|
130
|
+
'OnOrBefore',
|
|
131
|
+
'PercentChange',
|
|
132
|
+
'Positive',
|
|
133
|
+
'Range',
|
|
134
|
+
'Regex',
|
|
135
|
+
'StartsWith',
|
|
136
|
+
'ThisMonth',
|
|
137
|
+
'ThisQuarter',
|
|
138
|
+
'ThisWeek',
|
|
139
|
+
'ThisYear',
|
|
140
|
+
'Today',
|
|
141
|
+
'Tomorrow',
|
|
142
|
+
'True',
|
|
143
|
+
'WorkDay',
|
|
144
|
+
'Yesterday',
|
|
145
|
+
'Zero',
|
|
146
|
+
]);
|
|
147
|
+
let systemFunctionNames;
|
|
148
|
+
const getSystemFunctionNames = () => {
|
|
149
|
+
if (!systemFunctionNames) {
|
|
150
|
+
systemFunctionNames = new Set([
|
|
151
|
+
...Object.keys(scalarExpressionFunctions),
|
|
152
|
+
...Object.keys(systemAggregatedExpressionFunctions),
|
|
153
|
+
...Object.keys(systemAggregationClauseFunctions),
|
|
154
|
+
...Object.keys(systemObservableExpressionFunctions),
|
|
155
|
+
...AGGREGATION_ONLY_NAMES,
|
|
156
|
+
...DUAL_AGGREGATION_NAMES,
|
|
157
|
+
...OBSERVABLE_SOURCE_NAMES,
|
|
158
|
+
...LEGACY_OBSERVABLE_SOURCE_NAMES,
|
|
159
|
+
...LEGACY_OBSERVABLE_MARKER_NAMES,
|
|
160
|
+
...OPERAND_CLAUSE_NAMES,
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
return systemFunctionNames;
|
|
164
|
+
};
|
|
165
|
+
const FUNCTION_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
166
|
+
const parsesAsFunctionCall = (name) => {
|
|
167
|
+
try {
|
|
168
|
+
const { ast } = parse(`${name}(1)`);
|
|
169
|
+
return ast.length === 1 && isAST_Function(ast[0]) && ast[0].type === name;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
export const customPredicateIdToFunctionName = (predicateId) => {
|
|
176
|
+
if (!predicateId || SYSTEM_PREDICATE_IDS.has(predicateId)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const name = predicateId
|
|
180
|
+
.trim()
|
|
181
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
182
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
183
|
+
.replace(/[^A-Za-z0-9]+/g, '_')
|
|
184
|
+
.replace(/^_+|_+$/g, '')
|
|
185
|
+
.toUpperCase();
|
|
186
|
+
if (!FUNCTION_NAME_PATTERN.test(name) || getSystemFunctionNames().has(name)) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return parsesAsFunctionCall(name) ? name : null;
|
|
190
|
+
};
|
|
191
|
+
const lowerCustomPredicate = (predicate, subjectColumnId) => {
|
|
192
|
+
const functionName = customPredicateIdToFunctionName(predicate.PredicateId);
|
|
193
|
+
if (!functionName) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const inputs = (predicate.Inputs ?? []).filter((input) => input !== undefined);
|
|
197
|
+
const isScalarInput = (input) => typeof input === 'string' || typeof input === 'number' || typeof input === 'boolean';
|
|
198
|
+
if (!inputs.every(isScalarInput)) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const subject = subjectColumnId == null ? SCOPE_REF : colRef(subjectColumnId);
|
|
202
|
+
const qlPredicate = qlPrimitive(functionName, [
|
|
203
|
+
subject,
|
|
204
|
+
...inputs.map((input) => literalForRawValue(input)),
|
|
205
|
+
]);
|
|
206
|
+
if (!isQlPredicateComplete(qlPredicate, MIGRATION_FUNCTION_POOL)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
qlPredicate,
|
|
211
|
+
call: { predicateId: predicate.PredicateId, functionName, inputCount: inputs.length },
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
export const lowerPredicateToQlPredicate = (predicate, subjectColumnId, customCalls) => {
|
|
96
215
|
const predicateId = predicate.PredicateId;
|
|
97
216
|
const column = {
|
|
98
217
|
columnId: subjectColumnId ?? '__scope__',
|
|
99
218
|
dataType: inferDataType(predicateId, predicate.Inputs),
|
|
100
219
|
};
|
|
101
220
|
const qlPredicate = predicateIdToQlPredicate(predicateId, coerceInputs(predicateId, predicate.Inputs), column);
|
|
102
|
-
if (!qlPredicate
|
|
221
|
+
if (!qlPredicate) {
|
|
222
|
+
const custom = lowerCustomPredicate(predicate, subjectColumnId);
|
|
223
|
+
if (!custom) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
customCalls?.push(custom.call);
|
|
227
|
+
return custom.qlPredicate;
|
|
228
|
+
}
|
|
229
|
+
if (!isQlPredicateComplete(qlPredicate, MIGRATION_FUNCTION_POOL)) {
|
|
103
230
|
return null;
|
|
104
231
|
}
|
|
105
232
|
return subjectColumnId == null ? withScopeSubject(qlPredicate) : qlPredicate;
|
|
@@ -119,20 +246,22 @@ const toGroupExpression = (children, combinator) => {
|
|
|
119
246
|
const group = { kind: 'group', combinator, children };
|
|
120
247
|
return toCanonicalExpression(mapQlPredicateToExpression(group));
|
|
121
248
|
};
|
|
122
|
-
export const columnFilterPredicatesToExpression = (columnFilter) => {
|
|
249
|
+
export const columnFilterPredicatesToExpression = (columnFilter, customCalls) => {
|
|
123
250
|
const legacy = columnFilter;
|
|
124
251
|
const predicates = legacy.Predicates ?? [];
|
|
125
252
|
if (!predicates.length) {
|
|
126
253
|
return null;
|
|
127
254
|
}
|
|
255
|
+
const calls = [];
|
|
128
256
|
const children = [];
|
|
129
257
|
for (const predicate of predicates) {
|
|
130
|
-
const qlPredicate = lowerPredicateToQlPredicate(predicate, columnFilter.ColumnId);
|
|
258
|
+
const qlPredicate = lowerPredicateToQlPredicate(predicate, columnFilter.ColumnId, calls);
|
|
131
259
|
if (!qlPredicate) {
|
|
132
260
|
return null;
|
|
133
261
|
}
|
|
134
262
|
children.push(qlPredicate);
|
|
135
263
|
}
|
|
264
|
+
customCalls?.push(...calls);
|
|
136
265
|
const combinator = legacy.PredicatesOperator === 'OR' ? 'OR' : 'AND';
|
|
137
266
|
return toGroupExpression(children, combinator);
|
|
138
267
|
};
|
|
@@ -178,22 +307,24 @@ export const columnFilterPredicatesToPreferredFilter = (columnFilter) => {
|
|
|
178
307
|
}
|
|
179
308
|
return operator;
|
|
180
309
|
};
|
|
181
|
-
export const rulePredicatesToBooleanExpression = (predicates) => {
|
|
310
|
+
export const rulePredicatesToBooleanExpression = (predicates, customCalls) => {
|
|
182
311
|
if (!predicates?.length) {
|
|
183
312
|
return null;
|
|
184
313
|
}
|
|
314
|
+
const calls = [];
|
|
185
315
|
const children = [];
|
|
186
316
|
for (const predicate of predicates) {
|
|
187
|
-
const qlPredicate = lowerPredicateToQlPredicate(predicate, predicate.ColumnId ?? null);
|
|
317
|
+
const qlPredicate = lowerPredicateToQlPredicate(predicate, predicate.ColumnId ?? null, calls);
|
|
188
318
|
if (!qlPredicate) {
|
|
189
319
|
return null;
|
|
190
320
|
}
|
|
191
321
|
children.push(qlPredicate);
|
|
192
322
|
}
|
|
323
|
+
customCalls?.push(...calls);
|
|
193
324
|
return toGroupExpression(children, 'AND');
|
|
194
325
|
};
|
|
195
|
-
export const badgePredicateToBooleanExpression = (predicate, columnId) => {
|
|
196
|
-
const qlPredicate = lowerPredicateToQlPredicate(predicate, columnId);
|
|
326
|
+
export const badgePredicateToBooleanExpression = (predicate, columnId, customCalls) => {
|
|
327
|
+
const qlPredicate = lowerPredicateToQlPredicate(predicate, columnId, customCalls);
|
|
197
328
|
if (!qlPredicate) {
|
|
198
329
|
return null;
|
|
199
330
|
}
|