@speedkit/cli 4.26.0 → 4.27.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/CHANGELOG.md +17 -0
- package/README.md +203 -15
- package/dist/commands/auto-prewarm.d.ts +12 -1
- package/dist/commands/auto-prewarm.js +26 -7
- package/dist/commands/generate-pop-config.d.ts +1 -0
- package/dist/commands/generate-pop-config.js +9 -3
- package/dist/commands/prewarm.js +3 -1
- package/dist/commands/query/parameter.d.ts +27 -0
- package/dist/commands/query/parameter.js +28 -0
- package/dist/commands/query/prewarm.d.ts +32 -0
- package/dist/commands/query/prewarm.js +32 -0
- package/dist/helpers/clipboard.d.ts +19 -0
- package/dist/helpers/clipboard.js +70 -0
- package/dist/helpers/evaluate-speed-kit-config.d.ts +8 -18
- package/dist/helpers/evaluate-speed-kit-config.js +8 -6
- package/dist/helpers/evaluate-speed-kit-config.spec.d.ts +1 -0
- package/dist/helpers/evaluate-speed-kit-config.spec.js +78 -0
- package/dist/models/cli-parameters.d.ts +106 -0
- package/dist/models/cli-parameters.js +183 -0
- package/dist/models/cli-parameters.spec.d.ts +1 -0
- package/dist/models/cli-parameters.spec.js +47 -0
- package/dist/services/athena/athena-service-factory.d.ts +4 -1
- package/dist/services/athena/athena-service-factory.js +9 -3
- package/dist/services/athena/athena-service.d.ts +13 -1
- package/dist/services/athena/athena-service.js +27 -5
- package/dist/services/athena/athena-service.spec.d.ts +1 -0
- package/dist/services/athena/athena-service.spec.js +93 -0
- package/dist/services/deploy/handler/install-resource-handler.js +3 -4
- package/dist/services/onboarding/onboarding-service-factory.js +2 -2
- package/dist/services/pop-config/pop-config-service.d.ts +8 -1
- package/dist/services/pop-config/pop-config-service.js +16 -28
- package/dist/services/prewarm/assets/asset-api-client.d.ts +1 -1
- package/dist/services/prewarm/assets/asset-api-client.js +5 -4
- package/dist/services/prewarm/auto-pre-warm-factory.d.ts +20 -2
- package/dist/services/prewarm/auto-pre-warm-factory.js +24 -6
- package/dist/services/prewarm/csv-reader.d.ts +21 -2
- package/dist/services/prewarm/csv-reader.js +71 -9
- package/dist/services/prewarm/csv-reader.spec.d.ts +1 -0
- package/dist/services/prewarm/csv-reader.spec.js +75 -0
- package/dist/services/prewarm/index.d.ts +1 -0
- package/dist/services/prewarm/index.js +1 -0
- package/dist/services/prewarm/pre-warm-factory.d.ts +0 -2
- package/dist/services/prewarm/pre-warm-factory.js +8 -11
- package/dist/services/prewarm/pre-warm-model.d.ts +14 -1
- package/dist/services/prewarm/pre-warm-model.js +0 -1
- package/dist/services/prewarm/pre-warm-service.d.ts +9 -4
- package/dist/services/prewarm/pre-warm-service.js +18 -14
- package/dist/services/prewarm/prewarm-targets.d.ts +12 -0
- package/dist/services/prewarm/prewarm-targets.js +18 -0
- package/dist/services/prewarm/prewarm-targets.spec.d.ts +1 -0
- package/dist/services/prewarm/prewarm-targets.spec.js +29 -0
- package/dist/services/query-builder/error/match-matches-nothing-error.d.ts +11 -0
- package/dist/services/query-builder/error/match-matches-nothing-error.js +17 -0
- package/dist/services/query-builder/queries/device.d.ts +21 -0
- package/dist/services/query-builder/queries/device.js +26 -0
- package/dist/services/query-builder/queries/index.d.ts +6 -0
- package/dist/services/query-builder/queries/index.js +6 -0
- package/dist/services/query-builder/queries/page-filter.d.ts +39 -0
- package/dist/services/query-builder/queries/page-filter.js +135 -0
- package/dist/services/query-builder/queries/page-source.d.ts +30 -0
- package/dist/services/query-builder/queries/page-source.js +79 -0
- package/dist/services/query-builder/queries/parameter-query.d.ts +14 -0
- package/dist/services/query-builder/queries/parameter-query.js +142 -0
- package/dist/services/query-builder/queries/pops-query.d.ts +27 -0
- package/dist/services/query-builder/queries/pops-query.js +65 -0
- package/dist/services/query-builder/queries/prewarm-query.d.ts +18 -0
- package/dist/services/query-builder/queries/prewarm-query.js +227 -0
- package/dist/services/query-builder/queries/query-model.d.ts +97 -0
- package/dist/services/query-builder/queries/query-model.js +19 -0
- package/dist/services/query-builder/queries/spec/golden-configs.d.ts +18 -0
- package/dist/services/query-builder/queries/spec/golden-configs.js +116 -0
- package/dist/services/query-builder/queries/spec/golden.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/golden.spec.js +161 -0
- package/dist/services/query-builder/queries/spec/pops-query.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/pops-query.spec.js +58 -0
- package/dist/services/query-builder/queries/spec/queries.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/queries.spec.js +312 -0
- package/dist/services/query-builder/queries/spec/reported-bugs.spec.d.ts +1 -0
- package/dist/services/query-builder/queries/spec/reported-bugs.spec.js +86 -0
- package/dist/services/query-builder/query-builder-factory.d.ts +2 -2
- package/dist/services/query-builder/query-builder-factory.js +36 -8
- package/dist/services/query-builder/query-builder-model.d.ts +33 -19
- package/dist/services/query-builder/query-builder-model.js +5 -5
- package/dist/services/query-builder/query-builder-service.d.ts +26 -5
- package/dist/services/query-builder/query-builder-service.js +157 -44
- package/dist/services/query-builder/query-command.d.ts +27 -0
- package/dist/services/query-builder/query-command.js +36 -0
- package/dist/services/query-builder/rules/index.d.ts +8 -0
- package/dist/services/query-builder/rules/index.js +8 -0
- package/dist/services/query-builder/rules/match-pattern.d.ts +51 -0
- package/dist/services/query-builder/rules/match-pattern.js +141 -0
- package/dist/services/query-builder/rules/pattern-language.d.ts +59 -0
- package/dist/services/query-builder/rules/pattern-language.js +159 -0
- package/dist/services/query-builder/rules/rule-model.d.ts +107 -0
- package/dist/services/query-builder/rules/rule-model.js +54 -0
- package/dist/services/query-builder/rules/rule-parser.d.ts +33 -0
- package/dist/services/query-builder/rules/rule-parser.js +189 -0
- package/dist/services/query-builder/rules/rule-reducer.d.ts +71 -0
- package/dist/services/query-builder/rules/rule-reducer.js +383 -0
- package/dist/services/query-builder/rules/rule-relevance.d.ts +43 -0
- package/dist/services/query-builder/rules/rule-relevance.js +69 -0
- package/dist/services/query-builder/rules/rule-to-sql.d.ts +51 -0
- package/dist/services/query-builder/rules/rule-to-sql.js +75 -0
- package/dist/services/query-builder/rules/spec/pattern-language.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/pattern-language.spec.js +90 -0
- package/dist/services/query-builder/rules/spec/rule-parser.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-parser.spec.js +150 -0
- package/dist/services/query-builder/rules/spec/rule-reducer.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-reducer.spec.js +169 -0
- package/dist/services/query-builder/rules/spec/rule-relevance.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-relevance.spec.js +88 -0
- package/dist/services/query-builder/rules/spec/rule-to-sql.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/rule-to-sql.spec.js +110 -0
- package/dist/services/query-builder/rules/spec/runtime-semantics.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/runtime-semantics.spec.js +55 -0
- package/dist/services/query-builder/rules/spec/strip-parameters.spec.d.ts +1 -0
- package/dist/services/query-builder/rules/spec/strip-parameters.spec.js +85 -0
- package/dist/services/query-builder/rules/strip-parameters.d.ts +39 -0
- package/dist/services/query-builder/rules/strip-parameters.js +197 -0
- package/dist/services/query-builder/sql/expression.d.ts +73 -0
- package/dist/services/query-builder/sql/expression.js +133 -0
- package/dist/services/query-builder/sql/format.d.ts +14 -0
- package/dist/services/query-builder/sql/format.js +48 -0
- package/dist/services/query-builder/sql/index.d.ts +4 -0
- package/dist/services/query-builder/sql/index.js +4 -0
- package/dist/services/query-builder/sql/predicate.d.ts +66 -0
- package/dist/services/query-builder/sql/predicate.js +124 -0
- package/dist/services/query-builder/sql/regex.d.ts +28 -0
- package/dist/services/query-builder/sql/regex.js +81 -0
- package/dist/services/query-builder/sql/spec/expression.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/expression.spec.js +75 -0
- package/dist/services/query-builder/sql/spec/format.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/format.spec.js +37 -0
- package/dist/services/query-builder/sql/spec/predicate.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/predicate.spec.js +78 -0
- package/dist/services/query-builder/sql/spec/regex.spec.d.ts +1 -0
- package/dist/services/query-builder/sql/spec/regex.spec.js +62 -0
- package/oclif.manifest.json +401 -100
- package/package.json +11 -2
- package/dist/commands/build-parameter-query.d.ts +0 -14
- package/dist/commands/build-parameter-query.js +0 -25
- package/dist/commands/build-prewarm-query.d.ts +0 -16
- package/dist/commands/build-prewarm-query.js +0 -37
- package/dist/helpers/build-query-helper.d.ts +0 -32
- package/dist/helpers/build-query-helper.js +0 -223
- package/dist/helpers/get-parsed-config.d.ts +0 -15
- package/dist/helpers/get-parsed-config.js +0 -62
- package/dist/services/onboarding/dashboard/parameter-query-builder.d.ts +0 -8
- package/dist/services/onboarding/dashboard/parameter-query-builder.js +0 -63
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A boolean SQL expression represented as a small tree instead of a string.
|
|
3
|
+
*
|
|
4
|
+
* The queries this builder emits are assembled from Speed Kit rules, and rules
|
|
5
|
+
* cancel each other out: a blacklist entry that can never match, an
|
|
6
|
+
* `enabledSites` rule already covered by another one. Folding those away is
|
|
7
|
+
* only possible while the predicate is still structured, which is what this
|
|
8
|
+
* type is for. {@link formatPredicate} turns it back into SQL.
|
|
9
|
+
*
|
|
10
|
+
* Leaves carry raw SQL. A leaf that is a single column or function call is
|
|
11
|
+
* atomic and needs no parentheses; a comparison is not, and gets them when it
|
|
12
|
+
* is negated. A leaf built by hand from a low-precedence expression (`a or b`)
|
|
13
|
+
* must bring its own.
|
|
14
|
+
*/
|
|
15
|
+
export type Predicate = {
|
|
16
|
+
readonly children: readonly Predicate[];
|
|
17
|
+
readonly kind: "and" | "or";
|
|
18
|
+
} | {
|
|
19
|
+
readonly child: Predicate;
|
|
20
|
+
readonly kind: "not";
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: "false";
|
|
23
|
+
} | {
|
|
24
|
+
readonly atomic?: boolean;
|
|
25
|
+
readonly kind: "leaf";
|
|
26
|
+
readonly sql: string;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: "true";
|
|
29
|
+
};
|
|
30
|
+
/** The always-true predicate. Neutral in `and`, absorbing in `or`. */
|
|
31
|
+
export declare const TRUE: Predicate;
|
|
32
|
+
/** The always-false predicate. Absorbing in `and`, neutral in `or`. */
|
|
33
|
+
export declare const FALSE: Predicate;
|
|
34
|
+
/**
|
|
35
|
+
* Wraps a piece of SQL as a predicate. Prefer the constructors in
|
|
36
|
+
* `expression.ts`, which escape their operands.
|
|
37
|
+
*
|
|
38
|
+
* @param atomic - True for a single column or function call, which never
|
|
39
|
+
* needs parentheses around it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function leaf(sql: string, atomic?: boolean): Predicate;
|
|
42
|
+
/**
|
|
43
|
+
* Conjunction of all given predicates, simplified:
|
|
44
|
+
* nested `and`s are flattened, `TRUE` operands drop out, a single `FALSE`
|
|
45
|
+
* operand or a pair of `x` / `not x` collapses the whole conjunction, and
|
|
46
|
+
* duplicate operands are removed. An empty conjunction is `TRUE`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function and(...predicates: Predicate[]): Predicate;
|
|
49
|
+
/**
|
|
50
|
+
* Disjunction of all given predicates, simplified the same way {@link and} is
|
|
51
|
+
* — with the roles of `TRUE` and `FALSE` swapped. An empty disjunction is
|
|
52
|
+
* `FALSE`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function or(...predicates: Predicate[]): Predicate;
|
|
55
|
+
/** Negation, collapsing `not not x`, `not TRUE` and `not FALSE`. */
|
|
56
|
+
export declare function not(predicate: Predicate): Predicate;
|
|
57
|
+
/** True when the predicate is known to hold for every row. */
|
|
58
|
+
export declare function isTrue(predicate: Predicate): boolean;
|
|
59
|
+
/** True when the predicate is known to hold for no row. */
|
|
60
|
+
export declare function isFalse(predicate: Predicate): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* A canonical string identifying the predicate, used to recognise duplicate
|
|
63
|
+
* and contradicting operands. Operands of `and` / `or` are sorted, so the key
|
|
64
|
+
* does not depend on the order they were passed in.
|
|
65
|
+
*/
|
|
66
|
+
export declare function predicateKey(predicate: Predicate): string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** The always-true predicate. Neutral in `and`, absorbing in `or`. */
|
|
2
|
+
export const TRUE = { kind: "true" };
|
|
3
|
+
/** The always-false predicate. Absorbing in `and`, neutral in `or`. */
|
|
4
|
+
export const FALSE = { kind: "false" };
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a piece of SQL as a predicate. Prefer the constructors in
|
|
7
|
+
* `expression.ts`, which escape their operands.
|
|
8
|
+
*
|
|
9
|
+
* @param atomic - True for a single column or function call, which never
|
|
10
|
+
* needs parentheses around it.
|
|
11
|
+
*/
|
|
12
|
+
export function leaf(sql, atomic = false) {
|
|
13
|
+
return { atomic, kind: "leaf", sql };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Conjunction of all given predicates, simplified:
|
|
17
|
+
* nested `and`s are flattened, `TRUE` operands drop out, a single `FALSE`
|
|
18
|
+
* operand or a pair of `x` / `not x` collapses the whole conjunction, and
|
|
19
|
+
* duplicate operands are removed. An empty conjunction is `TRUE`.
|
|
20
|
+
*/
|
|
21
|
+
export function and(...predicates) {
|
|
22
|
+
return combine("and", predicates);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Disjunction of all given predicates, simplified the same way {@link and} is
|
|
26
|
+
* — with the roles of `TRUE` and `FALSE` swapped. An empty disjunction is
|
|
27
|
+
* `FALSE`.
|
|
28
|
+
*/
|
|
29
|
+
export function or(...predicates) {
|
|
30
|
+
return combine("or", predicates);
|
|
31
|
+
}
|
|
32
|
+
/** Negation, collapsing `not not x`, `not TRUE` and `not FALSE`. */
|
|
33
|
+
export function not(predicate) {
|
|
34
|
+
if (predicate.kind === "true") {
|
|
35
|
+
return FALSE;
|
|
36
|
+
}
|
|
37
|
+
if (predicate.kind === "false") {
|
|
38
|
+
return TRUE;
|
|
39
|
+
}
|
|
40
|
+
if (predicate.kind === "not") {
|
|
41
|
+
return predicate.child;
|
|
42
|
+
}
|
|
43
|
+
return { child: predicate, kind: "not" };
|
|
44
|
+
}
|
|
45
|
+
/** True when the predicate is known to hold for every row. */
|
|
46
|
+
export function isTrue(predicate) {
|
|
47
|
+
return predicate.kind === "true";
|
|
48
|
+
}
|
|
49
|
+
/** True when the predicate is known to hold for no row. */
|
|
50
|
+
export function isFalse(predicate) {
|
|
51
|
+
return predicate.kind === "false";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A canonical string identifying the predicate, used to recognise duplicate
|
|
55
|
+
* and contradicting operands. Operands of `and` / `or` are sorted, so the key
|
|
56
|
+
* does not depend on the order they were passed in.
|
|
57
|
+
*/
|
|
58
|
+
export function predicateKey(predicate) {
|
|
59
|
+
switch (predicate.kind) {
|
|
60
|
+
case "and":
|
|
61
|
+
case "or": {
|
|
62
|
+
const keys = predicate.children.map((child) => predicateKey(child));
|
|
63
|
+
return `${predicate.kind}(${[...keys].sort().join(",")})`;
|
|
64
|
+
}
|
|
65
|
+
case "false": {
|
|
66
|
+
return "false";
|
|
67
|
+
}
|
|
68
|
+
case "leaf": {
|
|
69
|
+
return `leaf(${predicate.sql})`;
|
|
70
|
+
}
|
|
71
|
+
case "not": {
|
|
72
|
+
return `not(${predicateKey(predicate.child)})`;
|
|
73
|
+
}
|
|
74
|
+
case "true": {
|
|
75
|
+
return "true";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function combine(kind, predicates) {
|
|
80
|
+
// The operand that swallows the whole expression: `false` in a conjunction,
|
|
81
|
+
// `true` in a disjunction. The other one is neutral and can be dropped.
|
|
82
|
+
const absorbing = kind === "and" ? "false" : "true";
|
|
83
|
+
const neutral = kind === "and" ? "true" : "false";
|
|
84
|
+
const children = [];
|
|
85
|
+
const keys = new Set();
|
|
86
|
+
for (const predicate of flatten(kind, predicates)) {
|
|
87
|
+
if (predicate.kind === absorbing) {
|
|
88
|
+
return absorbing === "false" ? FALSE : TRUE;
|
|
89
|
+
}
|
|
90
|
+
if (predicate.kind === neutral) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const key = predicateKey(predicate);
|
|
94
|
+
if (keys.has(key)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
keys.add(key);
|
|
98
|
+
children.push(predicate);
|
|
99
|
+
}
|
|
100
|
+
// `x and not x` is never true, `x or not x` is always true.
|
|
101
|
+
for (const child of children) {
|
|
102
|
+
if (child.kind === "not" && keys.has(predicateKey(child.child))) {
|
|
103
|
+
return absorbing === "false" ? FALSE : TRUE;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (children.length === 0) {
|
|
107
|
+
return neutral === "true" ? TRUE : FALSE;
|
|
108
|
+
}
|
|
109
|
+
if (children.length === 1) {
|
|
110
|
+
return children[0];
|
|
111
|
+
}
|
|
112
|
+
return { children, kind };
|
|
113
|
+
}
|
|
114
|
+
function flatten(kind, predicates) {
|
|
115
|
+
const flattened = [];
|
|
116
|
+
for (const predicate of predicates) {
|
|
117
|
+
if (predicate.kind === kind) {
|
|
118
|
+
flattened.push(...flatten(kind, [...predicate.children]));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
flattened.push(predicate);
|
|
122
|
+
}
|
|
123
|
+
return flattened;
|
|
124
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface AthenaRegex {
|
|
2
|
+
/** The pattern to hand to `regexp_like`, without delimiters. */
|
|
3
|
+
readonly pattern: string;
|
|
4
|
+
/**
|
|
5
|
+
* Constructs whose meaning may differ between the browser and Athena. They
|
|
6
|
+
* do not stop the translation — the filter is emitted and the warning is
|
|
7
|
+
* reported, so a reviewer can check that one rule instead of the query.
|
|
8
|
+
*/
|
|
9
|
+
readonly warnings: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Translates a JavaScript regular expression from a Speed Kit config into a
|
|
13
|
+
* pattern for Athena's `regexp_like`.
|
|
14
|
+
*
|
|
15
|
+
* Athena evaluates regular expressions with Joni in its Java syntax, which
|
|
16
|
+
* agrees with JavaScript on everything these configs use — character classes,
|
|
17
|
+
* groups, quantifiers, anchors, lookahead. It differs in three ways that
|
|
18
|
+
* matter here: flags have to travel inside the pattern as an inline group,
|
|
19
|
+
* `\/` is a JavaScript-source artefact that Joni would reject, and a handful
|
|
20
|
+
* of constructs are unsupported or subtly different. The first two are fixed,
|
|
21
|
+
* the third is reported.
|
|
22
|
+
*/
|
|
23
|
+
export declare function toAthenaRegex(regExp: RegExp): AthenaRegex;
|
|
24
|
+
/**
|
|
25
|
+
* Escapes a plain string so it can be embedded in a regular expression as a
|
|
26
|
+
* literal, e.g. a query parameter name inside an alternation.
|
|
27
|
+
*/
|
|
28
|
+
export declare function escapeRegexLiteral(value: string): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translates a JavaScript regular expression from a Speed Kit config into a
|
|
3
|
+
* pattern for Athena's `regexp_like`.
|
|
4
|
+
*
|
|
5
|
+
* Athena evaluates regular expressions with Joni in its Java syntax, which
|
|
6
|
+
* agrees with JavaScript on everything these configs use — character classes,
|
|
7
|
+
* groups, quantifiers, anchors, lookahead. It differs in three ways that
|
|
8
|
+
* matter here: flags have to travel inside the pattern as an inline group,
|
|
9
|
+
* `\/` is a JavaScript-source artefact that Joni would reject, and a handful
|
|
10
|
+
* of constructs are unsupported or subtly different. The first two are fixed,
|
|
11
|
+
* the third is reported.
|
|
12
|
+
*/
|
|
13
|
+
export function toAthenaRegex(regExp) {
|
|
14
|
+
const { pattern, warnings } = translate(regExp.source, String(regExp));
|
|
15
|
+
const flags = [
|
|
16
|
+
regExp.ignoreCase ? "i" : "",
|
|
17
|
+
regExp.multiline ? "m" : "",
|
|
18
|
+
regExp.dotAll ? "s" : "",
|
|
19
|
+
].join("");
|
|
20
|
+
// The v flag predates the lib this project compiles against, so read it
|
|
21
|
+
// off the flag string rather than the RegExp instance.
|
|
22
|
+
if (regExp.flags.includes("v")) {
|
|
23
|
+
warnings.push(`${regExp}: the v flag has no equivalent in Athena — class set operations will not be applied`);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
pattern: flags ? `(?${flags})${pattern}` : pattern,
|
|
27
|
+
warnings,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Escapes a plain string so it can be embedded in a regular expression as a
|
|
32
|
+
* literal, e.g. a query parameter name inside an alternation.
|
|
33
|
+
*/
|
|
34
|
+
export function escapeRegexLiteral(value) {
|
|
35
|
+
return value.replaceAll(/[$()*+.?[\\\]^{|}]/g, "\\$&");
|
|
36
|
+
}
|
|
37
|
+
function translate(source, label) {
|
|
38
|
+
const characters = [];
|
|
39
|
+
const warnings = new Set();
|
|
40
|
+
let inCharacterClass = false;
|
|
41
|
+
for (let index = 0; index < source.length; index++) {
|
|
42
|
+
const character = source[index];
|
|
43
|
+
if (character === "\\" && index + 1 < source.length) {
|
|
44
|
+
const escaped = source[index + 1];
|
|
45
|
+
// `\/` only exists because the pattern was written as a literal between
|
|
46
|
+
// slashes. Joni treats an unknown escape as an error, so unescape it.
|
|
47
|
+
if (escaped === "/") {
|
|
48
|
+
characters.push("/");
|
|
49
|
+
index++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!inCharacterClass && /[1-9]/.test(escaped)) {
|
|
53
|
+
warnings.add(`${label}: back references are not supported by Athena`);
|
|
54
|
+
}
|
|
55
|
+
if (escaped === "k") {
|
|
56
|
+
warnings.add(`${label}: back references are not supported by Athena`);
|
|
57
|
+
}
|
|
58
|
+
if (escaped === "u" && source[index + 2] === "{") {
|
|
59
|
+
warnings.add(`${label}: code point escapes (\\u{…}) are not supported by Athena`);
|
|
60
|
+
}
|
|
61
|
+
if (escaped === "p" || escaped === "P") {
|
|
62
|
+
warnings.add(`${label}: unicode property escapes use different property names in Athena`);
|
|
63
|
+
}
|
|
64
|
+
characters.push(character, escaped);
|
|
65
|
+
index++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (character === "[" && !inCharacterClass) {
|
|
69
|
+
inCharacterClass = true;
|
|
70
|
+
}
|
|
71
|
+
else if (character === "]" && inCharacterClass) {
|
|
72
|
+
inCharacterClass = false;
|
|
73
|
+
}
|
|
74
|
+
else if (!inCharacterClass &&
|
|
75
|
+
(source.startsWith("(?<=", index) || source.startsWith("(?<!", index))) {
|
|
76
|
+
warnings.add(`${label}: lookbehind is only partially supported by Athena — verify this filter`);
|
|
77
|
+
}
|
|
78
|
+
characters.push(character);
|
|
79
|
+
}
|
|
80
|
+
return { pattern: characters.join(""), warnings: [...warnings] };
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { booleanColumn, call, column, concat, equals, equalsIgnoreCase, inValues, literal, lower, notEquals, regexpLike, startsWith, } from "../expression.js";
|
|
4
|
+
import { formatPredicate } from "../format.js";
|
|
5
|
+
describe("literal", () => {
|
|
6
|
+
it("should quote a string", () => {
|
|
7
|
+
expect(literal("abc")).to.equal("'abc'");
|
|
8
|
+
});
|
|
9
|
+
it("should double a quote inside a string", () => {
|
|
10
|
+
expect(literal("it's")).to.equal("'it''s'");
|
|
11
|
+
});
|
|
12
|
+
it("should print numbers and booleans unquoted", () => {
|
|
13
|
+
expect(literal(42)).to.equal("42");
|
|
14
|
+
expect(literal(true)).to.equal("true");
|
|
15
|
+
});
|
|
16
|
+
it("should reject a non-finite number", () => {
|
|
17
|
+
expect(() => literal(Number.NaN)).to.throw();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe("column", () => {
|
|
21
|
+
it("should accept a qualified name", () => {
|
|
22
|
+
expect(column("cdnclient.bot")).to.equal("cdnclient.bot");
|
|
23
|
+
});
|
|
24
|
+
it("should reject anything that is not an identifier", () => {
|
|
25
|
+
expect(() => column("url'; drop")).to.throw();
|
|
26
|
+
expect(() => column("1url")).to.throw();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe("call", () => {
|
|
30
|
+
it("should join the arguments", () => {
|
|
31
|
+
expect(call("url_extract_path", column("url"))).to.equal("url_extract_path(url)");
|
|
32
|
+
});
|
|
33
|
+
it("should reject an invalid function name", () => {
|
|
34
|
+
expect(() => call("drop table x --")).to.throw();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("expression comparisons", () => {
|
|
38
|
+
it("should build an equality", () => {
|
|
39
|
+
expect(formatPredicate(equals(column("app"), literal("shop-de")))).to.equal("app = 'shop-de'");
|
|
40
|
+
});
|
|
41
|
+
it("should build an inequality", () => {
|
|
42
|
+
expect(formatPredicate(notEquals(column("device"), literal("Robot")))).to.equal("device <> 'Robot'");
|
|
43
|
+
});
|
|
44
|
+
it("should compare case-insensitively through lower", () => {
|
|
45
|
+
expect(formatPredicate(equalsIgnoreCase(column("url"), "https://Example.com/"))).to.equal("lower(url) = 'https://example.com/'");
|
|
46
|
+
});
|
|
47
|
+
it("should build an in list", () => {
|
|
48
|
+
expect(formatPredicate(inValues(column("navigationtype"), ["NAVIGATE", "RELOAD"]))).to.equal("navigationtype in ('NAVIGATE', 'RELOAD')");
|
|
49
|
+
});
|
|
50
|
+
it("should build a regexp_like", () => {
|
|
51
|
+
expect(formatPredicate(regexpLike(column("url"), "(?i)^/products/"))).to.equal("regexp_like(url, '(?i)^/products/')");
|
|
52
|
+
});
|
|
53
|
+
it("should use a boolean column directly", () => {
|
|
54
|
+
expect(formatPredicate(booleanColumn("suspicious"))).to.equal("suspicious");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe("startsWith", () => {
|
|
58
|
+
it("should lower-case both sides by default", () => {
|
|
59
|
+
expect(formatPredicate(startsWith(column("url"), "/Products/"))).to.equal("lower(url) like '/products/%'");
|
|
60
|
+
});
|
|
61
|
+
it("should keep the case when asked to", () => {
|
|
62
|
+
expect(formatPredicate(startsWith(column("url"), "/Products/", { ignoreCase: false }))).to.equal("url like '/Products/%'");
|
|
63
|
+
});
|
|
64
|
+
it("should escape like metacharacters and declare the escape character", () => {
|
|
65
|
+
expect(formatPredicate(startsWith(column("url"), "/a_b"))).to.equal(String.raw `lower(url) like '/a\_b%' escape '\'`);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe("lower and concat", () => {
|
|
69
|
+
it("should wrap a concatenation in parentheses", () => {
|
|
70
|
+
expect(concat(column("a"), column("b"))).to.equal("(a || b)");
|
|
71
|
+
});
|
|
72
|
+
it("should lower an expression", () => {
|
|
73
|
+
expect(lower(column("url"))).to.equal("lower(url)");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { formatPredicate } from "../format.js";
|
|
4
|
+
import { and, FALSE, leaf, not, or, TRUE } from "../predicate.js";
|
|
5
|
+
const a = leaf("a = 1");
|
|
6
|
+
const b = leaf("b = 2");
|
|
7
|
+
const c = leaf("c = 3");
|
|
8
|
+
describe("formatPredicate", () => {
|
|
9
|
+
it("should print a leaf as it is", () => {
|
|
10
|
+
expect(formatPredicate(a)).to.equal("a = 1");
|
|
11
|
+
});
|
|
12
|
+
it("should print the constants", () => {
|
|
13
|
+
expect(formatPredicate(TRUE)).to.equal("true");
|
|
14
|
+
expect(formatPredicate(FALSE)).to.equal("false");
|
|
15
|
+
});
|
|
16
|
+
it("should put every operand on its own line", () => {
|
|
17
|
+
expect(formatPredicate(and(a, b))).to.equal("a = 1\nand b = 2");
|
|
18
|
+
});
|
|
19
|
+
it("should parenthesise and indent a nested group", () => {
|
|
20
|
+
expect(formatPredicate(and(a, or(b, c)))).to.equal(["a = 1", "and (", " b = 2", " or c = 3", ")"].join("\n"));
|
|
21
|
+
});
|
|
22
|
+
it("should indent relative to the given level", () => {
|
|
23
|
+
expect(formatPredicate(and(a, b), { level: 1 })).to.equal("a = 1\n and b = 2");
|
|
24
|
+
});
|
|
25
|
+
it("should parenthesise a negated comparison", () => {
|
|
26
|
+
expect(formatPredicate(not(a))).to.equal("not (a = 1)");
|
|
27
|
+
});
|
|
28
|
+
it("should not parenthesise a negated call", () => {
|
|
29
|
+
expect(formatPredicate(not(leaf("suspicious", true)))).to.equal("not suspicious");
|
|
30
|
+
});
|
|
31
|
+
it("should not parenthesise a comparison that is not negated", () => {
|
|
32
|
+
expect(formatPredicate(and(a, b))).to.equal("a = 1\nand b = 2");
|
|
33
|
+
});
|
|
34
|
+
it("should parenthesise a negated group", () => {
|
|
35
|
+
expect(formatPredicate(not(or(a, b)))).to.equal(["not (", " a = 1", " or b = 2", ")"].join("\n"));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { and, FALSE, isFalse, isTrue, leaf, not, or, predicateKey, TRUE, } from "../predicate.js";
|
|
4
|
+
const a = leaf("a");
|
|
5
|
+
const b = leaf("b");
|
|
6
|
+
const c = leaf("c");
|
|
7
|
+
describe("predicate and", () => {
|
|
8
|
+
it("should be true without operands", () => {
|
|
9
|
+
expect(isTrue(and())).to.equal(true);
|
|
10
|
+
});
|
|
11
|
+
it("should return the only operand unwrapped", () => {
|
|
12
|
+
expect(and(a)).to.deep.equal(a);
|
|
13
|
+
});
|
|
14
|
+
it("should drop true operands", () => {
|
|
15
|
+
expect(and(a, TRUE, b)).to.deep.equal({
|
|
16
|
+
children: [a, b],
|
|
17
|
+
kind: "and",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it("should collapse to false on a false operand", () => {
|
|
21
|
+
expect(isFalse(and(a, FALSE))).to.equal(true);
|
|
22
|
+
});
|
|
23
|
+
it("should flatten nested conjunctions", () => {
|
|
24
|
+
expect(and(a, and(b, c))).to.deep.equal({
|
|
25
|
+
children: [a, b, c],
|
|
26
|
+
kind: "and",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it("should remove duplicate operands", () => {
|
|
30
|
+
expect(and(a, b, leaf("a"))).to.deep.equal({
|
|
31
|
+
children: [a, b],
|
|
32
|
+
kind: "and",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
it("should collapse to false on contradicting operands", () => {
|
|
36
|
+
expect(isFalse(and(a, b, not(leaf("a"))))).to.equal(true);
|
|
37
|
+
});
|
|
38
|
+
it("should keep a disjunction as an operand", () => {
|
|
39
|
+
expect(and(a, or(b, c))).to.deep.equal({
|
|
40
|
+
children: [a, { children: [b, c], kind: "or" }],
|
|
41
|
+
kind: "and",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("predicate or", () => {
|
|
46
|
+
it("should be false without operands", () => {
|
|
47
|
+
expect(isFalse(or())).to.equal(true);
|
|
48
|
+
});
|
|
49
|
+
it("should drop false operands", () => {
|
|
50
|
+
expect(or(a, FALSE, b)).to.deep.equal({
|
|
51
|
+
children: [a, b],
|
|
52
|
+
kind: "or",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it("should collapse to true on a true operand", () => {
|
|
56
|
+
expect(isTrue(or(a, TRUE))).to.equal(true);
|
|
57
|
+
});
|
|
58
|
+
it("should collapse to true on complementary operands", () => {
|
|
59
|
+
expect(isTrue(or(a, not(leaf("a"))))).to.equal(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("predicate not", () => {
|
|
63
|
+
it("should collapse a double negation", () => {
|
|
64
|
+
expect(not(not(a))).to.deep.equal(a);
|
|
65
|
+
});
|
|
66
|
+
it("should negate the constants", () => {
|
|
67
|
+
expect(isFalse(not(TRUE))).to.equal(true);
|
|
68
|
+
expect(isTrue(not(FALSE))).to.equal(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("predicateKey", () => {
|
|
72
|
+
it("should not depend on the operand order", () => {
|
|
73
|
+
expect(predicateKey(and(a, b))).to.equal(predicateKey(and(b, a)));
|
|
74
|
+
});
|
|
75
|
+
it("should differ between and and or", () => {
|
|
76
|
+
expect(predicateKey(and(a, b))).to.not.equal(predicateKey(or(a, b)));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { escapeRegexLiteral, toAthenaRegex } from "../regex.js";
|
|
4
|
+
describe("toAthenaRegex", () => {
|
|
5
|
+
it("should keep a plain pattern", () => {
|
|
6
|
+
expect(toAthenaRegex(/^\/products\/\d+$/).pattern).to.equal(String.raw `^/products/\d+$`);
|
|
7
|
+
});
|
|
8
|
+
it("should move the i flag into the pattern", () => {
|
|
9
|
+
expect(toAthenaRegex(/^\/p\//i).pattern).to.equal(String.raw `(?i)^/p/`);
|
|
10
|
+
});
|
|
11
|
+
it("should combine the flags in a single group", () => {
|
|
12
|
+
expect(toAthenaRegex(/a.b/ims).pattern).to.equal("(?ims)a.b");
|
|
13
|
+
});
|
|
14
|
+
it("should ignore flags without a pattern equivalent", () => {
|
|
15
|
+
expect(toAthenaRegex(/abc/g).pattern).to.equal("abc");
|
|
16
|
+
});
|
|
17
|
+
it("should unescape a slash", () => {
|
|
18
|
+
expect(toAthenaRegex(/\/de\/shop/i).pattern).to.equal("(?i)/de/shop");
|
|
19
|
+
});
|
|
20
|
+
it("should keep an escaped backslash", () => {
|
|
21
|
+
expect(toAthenaRegex(/a\\b/).pattern).to.equal(String.raw `a\\b`);
|
|
22
|
+
});
|
|
23
|
+
it("should not warn about a pattern it fully supports", () => {
|
|
24
|
+
expect(toAthenaRegex(/^\/(shop|store)\/[a-z-]+$/i).warnings).to.deep.equal([]);
|
|
25
|
+
});
|
|
26
|
+
it("should keep lookahead without a warning", () => {
|
|
27
|
+
const { pattern, warnings } = toAthenaRegex(/^[^?]*\/(\?|$)/i);
|
|
28
|
+
expect(pattern).to.equal(String.raw `(?i)^[^?]*/(\?|$)`);
|
|
29
|
+
expect(warnings).to.deep.equal([]);
|
|
30
|
+
});
|
|
31
|
+
it("should warn about lookbehind", () => {
|
|
32
|
+
const { warnings } = toAthenaRegex(/(?<=\/de)\/shop/i);
|
|
33
|
+
expect(warnings).to.have.lengthOf(1);
|
|
34
|
+
expect(warnings[0]).to.contain("lookbehind");
|
|
35
|
+
});
|
|
36
|
+
it("should warn about a back reference", () => {
|
|
37
|
+
const { warnings } = toAthenaRegex(/(a)\1/);
|
|
38
|
+
expect(warnings).to.have.lengthOf(1);
|
|
39
|
+
expect(warnings[0]).to.contain("back reference");
|
|
40
|
+
});
|
|
41
|
+
it("should not read a back reference inside a character class", () => {
|
|
42
|
+
// Written through the constructor because TypeScript rejects the literal.
|
|
43
|
+
const inClass = new RegExp(String.raw `[\1]`);
|
|
44
|
+
expect(toAthenaRegex(inClass).warnings).to.deep.equal([]);
|
|
45
|
+
});
|
|
46
|
+
it("should warn about a unicode property escape", () => {
|
|
47
|
+
const { warnings } = toAthenaRegex(/\p{Letter}/u);
|
|
48
|
+
expect(warnings).to.have.lengthOf(1);
|
|
49
|
+
expect(warnings[0]).to.contain("unicode property");
|
|
50
|
+
});
|
|
51
|
+
it("should report each construct once", () => {
|
|
52
|
+
expect(toAthenaRegex(/(a)\1(b)\2/).warnings).to.have.lengthOf(1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("escapeRegexLiteral", () => {
|
|
56
|
+
it("should escape the regex metacharacters", () => {
|
|
57
|
+
expect(escapeRegexLiteral("utm_source?")).to.equal(String.raw `utm_source\?`);
|
|
58
|
+
});
|
|
59
|
+
it("should leave a plain parameter name alone", () => {
|
|
60
|
+
expect(escapeRegexLiteral("gclid")).to.equal("gclid");
|
|
61
|
+
});
|
|
62
|
+
});
|