@speedkit/cli 4.27.1 → 4.27.2
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 +7 -0
- package/README.md +29 -14
- package/dist/commands/auto-prewarm.d.ts +1 -0
- package/dist/commands/auto-prewarm.js +2 -1
- package/dist/commands/query/parameter.d.ts +1 -0
- package/dist/commands/query/prewarm.d.ts +1 -0
- package/dist/helpers/clipboard.d.ts +6 -0
- package/dist/helpers/clipboard.js +7 -1
- package/dist/models/cli-parameters.d.ts +6 -0
- package/dist/models/cli-parameters.js +12 -0
- package/dist/services/athena/athena-service-factory.d.ts +5 -1
- package/dist/services/athena/athena-service-factory.js +8 -3
- package/dist/services/athena/athena-service-model.d.ts +17 -0
- package/dist/services/athena/athena-service-model.js +19 -0
- package/dist/services/athena/athena-service.d.ts +11 -1
- package/dist/services/athena/athena-service.js +10 -3
- package/dist/services/athena/athena-service.spec.js +16 -0
- package/dist/services/athena/baqend-athena-service.d.ts +13 -4
- package/dist/services/athena/baqend-athena-service.js +25 -10
- package/dist/services/athena/baqend-athena-service.spec.d.ts +1 -0
- package/dist/services/athena/baqend-athena-service.spec.js +51 -0
- package/dist/services/cli/cli-service.d.ts +9 -0
- package/dist/services/cli/cli-service.js +57 -3
- package/dist/services/cli/cli-service.spec.d.ts +1 -0
- package/dist/services/cli/cli-service.spec.js +67 -0
- package/dist/services/prewarm/auto-pre-warm-factory.d.ts +2 -0
- package/dist/services/prewarm/auto-pre-warm-factory.js +1 -0
- package/dist/services/query-builder/queries/page-source.d.ts +11 -2
- package/dist/services/query-builder/queries/page-source.js +15 -3
- package/dist/services/query-builder/queries/query-model.d.ts +2 -0
- package/dist/services/query-builder/queries/spec/queries.spec.js +17 -0
- package/dist/services/query-builder/query-builder-factory.js +2 -1
- package/dist/services/query-builder/query-builder-model.d.ts +4 -0
- package/dist/services/query-builder/query-builder-service.js +12 -7
- package/dist/services/query-builder/query-builder-service.spec.d.ts +1 -0
- package/dist/services/query-builder/query-builder-service.spec.js +65 -0
- package/dist/services/query-builder/query-command.d.ts +1 -0
- package/dist/services/query-builder/query-command.js +1 -0
- package/oclif.manifest.json +31 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [4.27.2](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.27.1...v4.27.2) (2026-09-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* query define awsCatalog via --catalog, disable spinners while CI or not a tty ([e949dbb](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/e949dbbaf73521267e03c4c043b393e76e174ece))
|
|
7
|
+
|
|
1
8
|
## [4.27.1](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.27.0...v4.27.1) (2026-09-11)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ $ npm install -g @speedkit/cli
|
|
|
21
21
|
$ sk COMMAND
|
|
22
22
|
running command...
|
|
23
23
|
$ sk (--version)
|
|
24
|
-
@speedkit/cli/4.27.
|
|
24
|
+
@speedkit/cli/4.27.2 linux-x64 node-v22.23.2
|
|
25
25
|
$ sk --help [COMMAND]
|
|
26
26
|
USAGE
|
|
27
27
|
$ sk COMMAND
|
|
@@ -103,9 +103,10 @@ Build and execute the prewarm query for a customer config and start a prewarm jo
|
|
|
103
103
|
|
|
104
104
|
```
|
|
105
105
|
USAGE
|
|
106
|
-
$ sk auto-prewarm CUSTOMERPATH [--workgroup <value>] [-c <value>] [-d <value>]
|
|
107
|
-
[--excludeSuspicious] [--explain] [--hardNavigationsOnly] [--excludeNotFound] [--limit <value>]
|
|
108
|
-
[--minHits <value>] [-p <value> | [--device desktop|mobile|tablet... | -v <value>...] | ] [-k]
|
|
106
|
+
$ sk auto-prewarm CUSTOMERPATH [--catalog <value>] [--workgroup <value>] [-c <value>] [-d <value>]
|
|
107
|
+
[--excludeNoindex] [--excludeSuspicious] [--explain] [--hardNavigationsOnly] [--excludeNotFound] [--limit <value>]
|
|
108
|
+
[--match <value>...] [--minHits <value>] [-p <value> | [--device desktop|mobile|tablet... | -v <value>...] | ] [-k]
|
|
109
|
+
[-q] [--verbose]
|
|
109
110
|
|
|
110
111
|
ARGUMENTS
|
|
111
112
|
CUSTOMERPATH The customer config path
|
|
@@ -118,6 +119,8 @@ FLAGS
|
|
|
118
119
|
-q, --quiet less output non interactive
|
|
119
120
|
-v, --variation=<value>... Pair every url of the list with each of these variations. For the custom variations no
|
|
120
121
|
traffic can reveal, e.g. 'desktop-pdp-a,mobile-pdp-a'
|
|
122
|
+
--catalog=<value> [default: live, env: SK_ATHENA_CATALOG] Athena data catalog holding the rum tables. Names
|
|
123
|
+
the catalog both in the query and in the context it runs in, so the two always agree
|
|
121
124
|
--device=<option>... Split the list by device class, so every url is warmed in the variations it was actually
|
|
122
125
|
requested in. A class not named is left out
|
|
123
126
|
<options: desktop|mobile|tablet>
|
|
@@ -261,8 +264,9 @@ Build a parameter query that can be executed in Athena
|
|
|
261
264
|
|
|
262
265
|
```
|
|
263
266
|
USAGE
|
|
264
|
-
$ sk build-parameter-query CUSTOMERPATH [--workgroup <value>] [-c <value>] [-d <value>]
|
|
265
|
-
[--excludeSuspicious] [--explain] [--hardNavigationsOnly] [-e] [-q] [--bySet] [--minImpressions
|
|
267
|
+
$ sk build-parameter-query CUSTOMERPATH [--catalog <value>] [--workgroup <value>] [-c <value>] [-d <value>]
|
|
268
|
+
[--excludeNoindex] [--excludeSuspicious] [--explain] [--hardNavigationsOnly] [-e] [-q] [--bySet] [--minImpressions
|
|
269
|
+
<value>]
|
|
266
270
|
|
|
267
271
|
ARGUMENTS
|
|
268
272
|
CUSTOMERPATH The customer config path
|
|
@@ -274,6 +278,9 @@ FLAGS
|
|
|
274
278
|
-q, --quiet Output only the raw query without clipboard copy or styling
|
|
275
279
|
--bySet Rank whole sets of parameters instead of single parameters, to see which ones travel
|
|
276
280
|
together
|
|
281
|
+
--catalog=<value> [default: live, env: SK_ATHENA_CATALOG] Athena data catalog holding the rum tables.
|
|
282
|
+
Names the catalog both in the query and in the context it runs in, so the two always
|
|
283
|
+
agree
|
|
277
284
|
--excludeNoindex Exclude page impressions of pages marked noindex
|
|
278
285
|
--excludeSuspicious Exclude page impressions Speed Kit classified as suspicious
|
|
279
286
|
--explain Account for every rule the query leaves out, reduces or approximates
|
|
@@ -302,9 +309,9 @@ Build a prewarm query that can be executed in Athena
|
|
|
302
309
|
|
|
303
310
|
```
|
|
304
311
|
USAGE
|
|
305
|
-
$ sk build-prewarm-query CUSTOMERPATH [--workgroup <value>] [-c <value>] [-d <value>]
|
|
306
|
-
[--excludeSuspicious] [--explain] [--hardNavigationsOnly] [-q] [--device desktop|mobile|tablet...
|
|
307
|
-
[--excludeNotFound] [--limit <value>] [--match <value>...] [--minHits <value>] [--count | -e]
|
|
312
|
+
$ sk build-prewarm-query CUSTOMERPATH [--catalog <value>] [--workgroup <value>] [-c <value>] [-d <value>]
|
|
313
|
+
[--excludeNoindex] [--excludeSuspicious] [--explain] [--hardNavigationsOnly] [-q] [--device desktop|mobile|tablet...
|
|
314
|
+
| -v <value>...] [--excludeNotFound] [--limit <value>] [--match <value>...] [--minHits <value>] [--count | -e]
|
|
308
315
|
|
|
309
316
|
ARGUMENTS
|
|
310
317
|
CUSTOMERPATH The customer config path
|
|
@@ -316,6 +323,8 @@ FLAGS
|
|
|
316
323
|
-q, --quiet Output only the raw query without clipboard copy or styling
|
|
317
324
|
-v, --variation=<value>... Pair every url of the list with each of these variations. For the custom variations no
|
|
318
325
|
traffic can reveal, e.g. 'desktop-pdp-a,mobile-pdp-a'
|
|
326
|
+
--catalog=<value> [default: live, env: SK_ATHENA_CATALOG] Athena data catalog holding the rum tables. Names
|
|
327
|
+
the catalog both in the query and in the context it runs in, so the two always agree
|
|
319
328
|
--count Run the query and report how large the list is instead of what is in it: urls, the
|
|
320
329
|
impressions they stand for, and a row per variation where the list carries variations.
|
|
321
330
|
Every other flag still applies, so this counts the list the same command would produce
|
|
@@ -838,8 +847,9 @@ Build a parameter query that can be executed in Athena
|
|
|
838
847
|
|
|
839
848
|
```
|
|
840
849
|
USAGE
|
|
841
|
-
$ sk query parameter CUSTOMERPATH [--workgroup <value>] [-c <value>] [-d <value>]
|
|
842
|
-
[--excludeSuspicious] [--explain] [--hardNavigationsOnly] [-e] [-q] [--bySet] [--minImpressions
|
|
850
|
+
$ sk query parameter CUSTOMERPATH [--catalog <value>] [--workgroup <value>] [-c <value>] [-d <value>]
|
|
851
|
+
[--excludeNoindex] [--excludeSuspicious] [--explain] [--hardNavigationsOnly] [-e] [-q] [--bySet] [--minImpressions
|
|
852
|
+
<value>]
|
|
843
853
|
|
|
844
854
|
ARGUMENTS
|
|
845
855
|
CUSTOMERPATH The customer config path
|
|
@@ -851,6 +861,9 @@ FLAGS
|
|
|
851
861
|
-q, --quiet Output only the raw query without clipboard copy or styling
|
|
852
862
|
--bySet Rank whole sets of parameters instead of single parameters, to see which ones travel
|
|
853
863
|
together
|
|
864
|
+
--catalog=<value> [default: live, env: SK_ATHENA_CATALOG] Athena data catalog holding the rum tables.
|
|
865
|
+
Names the catalog both in the query and in the context it runs in, so the two always
|
|
866
|
+
agree
|
|
854
867
|
--excludeNoindex Exclude page impressions of pages marked noindex
|
|
855
868
|
--excludeSuspicious Exclude page impressions Speed Kit classified as suspicious
|
|
856
869
|
--explain Account for every rule the query leaves out, reduces or approximates
|
|
@@ -879,9 +892,9 @@ Build a prewarm query that can be executed in Athena
|
|
|
879
892
|
|
|
880
893
|
```
|
|
881
894
|
USAGE
|
|
882
|
-
$ sk query prewarm CUSTOMERPATH [--workgroup <value>] [-c <value>] [-d <value>]
|
|
883
|
-
[--excludeSuspicious] [--explain] [--hardNavigationsOnly] [-q] [--device desktop|mobile|tablet...
|
|
884
|
-
[--excludeNotFound] [--limit <value>] [--match <value>...] [--minHits <value>] [--count | -e]
|
|
895
|
+
$ sk query prewarm CUSTOMERPATH [--catalog <value>] [--workgroup <value>] [-c <value>] [-d <value>]
|
|
896
|
+
[--excludeNoindex] [--excludeSuspicious] [--explain] [--hardNavigationsOnly] [-q] [--device desktop|mobile|tablet...
|
|
897
|
+
| -v <value>...] [--excludeNotFound] [--limit <value>] [--match <value>...] [--minHits <value>] [--count | -e]
|
|
885
898
|
|
|
886
899
|
ARGUMENTS
|
|
887
900
|
CUSTOMERPATH The customer config path
|
|
@@ -893,6 +906,8 @@ FLAGS
|
|
|
893
906
|
-q, --quiet Output only the raw query without clipboard copy or styling
|
|
894
907
|
-v, --variation=<value>... Pair every url of the list with each of these variations. For the custom variations no
|
|
895
908
|
traffic can reveal, e.g. 'desktop-pdp-a,mobile-pdp-a'
|
|
909
|
+
--catalog=<value> [default: live, env: SK_ATHENA_CATALOG] Athena data catalog holding the rum tables. Names
|
|
910
|
+
the catalog both in the query and in the context it runs in, so the two always agree
|
|
896
911
|
--count Run the query and report how large the list is instead of what is in it: urls, the
|
|
897
912
|
impressions they stand for, and a row per variation where the list carries variations.
|
|
898
913
|
Every other flag still applies, so this counts the list the same command would produce
|
|
@@ -24,6 +24,7 @@ export default class AutoPrewarm extends Command {
|
|
|
24
24
|
hardNavigationsOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
25
25
|
days: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
26
26
|
configName: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
27
|
+
catalog: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
27
28
|
workgroup: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
28
29
|
};
|
|
29
30
|
run(): Promise<void>;
|
|
@@ -46,7 +46,7 @@ export default class AutoPrewarm extends Command {
|
|
|
46
46
|
}),
|
|
47
47
|
};
|
|
48
48
|
async run() {
|
|
49
|
-
const { args: { customerPath }, flags: { configName, days, device, excludeNoindex, excludeNotFound, excludeSuspicious, hardNavigationsOnly, keepParameterSorting, limit, match, minHits, quiet, variation, variationPath, verbose, workgroup, }, } = await this.parse(AutoPrewarm);
|
|
49
|
+
const { args: { customerPath }, flags: { configName, days, device, excludeNoindex, excludeNotFound, excludeSuspicious, hardNavigationsOnly, keepParameterSorting, limit, match, minHits, quiet, variation, variationPath, verbose, workgroup, catalog, }, } = await this.parse(AutoPrewarm);
|
|
50
50
|
const service = await new AutoPreWarmFactory({
|
|
51
51
|
customerPath,
|
|
52
52
|
configName,
|
|
@@ -65,6 +65,7 @@ export default class AutoPrewarm extends Command {
|
|
|
65
65
|
quiet,
|
|
66
66
|
verbose,
|
|
67
67
|
workgroup,
|
|
68
|
+
catalog,
|
|
68
69
|
isWindows: this.config.windows,
|
|
69
70
|
}, new CliConfig(this.config).load()).getService();
|
|
70
71
|
await service.run();
|
|
@@ -21,6 +21,7 @@ export default class QueryParameter extends Command {
|
|
|
21
21
|
hardNavigationsOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
22
22
|
days: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
23
23
|
configName: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
24
|
+
catalog: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
24
25
|
workgroup: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
25
26
|
};
|
|
26
27
|
run(): Promise<void>;
|
|
@@ -26,6 +26,7 @@ export default class QueryPrewarm extends Command {
|
|
|
26
26
|
hardNavigationsOnly: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
27
27
|
days: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
28
28
|
configName: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
29
|
+
catalog: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
29
30
|
workgroup: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
30
31
|
};
|
|
31
32
|
run(): Promise<void>;
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
* behind a timeout, so no clipboard tool can ever be the reason a command
|
|
15
15
|
* appears to do nothing.
|
|
16
16
|
*
|
|
17
|
+
* `clipboardy` is loaded here rather than at module scope so that importing
|
|
18
|
+
* anything that can copy does not drag it in. It pulls a dependency that some
|
|
19
|
+
* Node/loader combinations refuse to resolve, which made every module on this
|
|
20
|
+
* path impossible to import from a test — and a clipboard that cannot load is
|
|
21
|
+
* already the case this function answers with `false`.
|
|
22
|
+
*
|
|
17
23
|
* @returns whether the text reached the clipboard.
|
|
18
24
|
*/
|
|
19
25
|
export declare function copyToClipboard(text: string): Promise<boolean>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import clipboardy from "clipboardy";
|
|
3
2
|
/** How long a clipboard tool may take before the CLI stops waiting for it. */
|
|
4
3
|
const TIMEOUT_IN_MS = 2000;
|
|
5
4
|
/**
|
|
@@ -18,6 +17,12 @@ const TIMEOUT_IN_MS = 2000;
|
|
|
18
17
|
* behind a timeout, so no clipboard tool can ever be the reason a command
|
|
19
18
|
* appears to do nothing.
|
|
20
19
|
*
|
|
20
|
+
* `clipboardy` is loaded here rather than at module scope so that importing
|
|
21
|
+
* anything that can copy does not drag it in. It pulls a dependency that some
|
|
22
|
+
* Node/loader combinations refuse to resolve, which made every module on this
|
|
23
|
+
* path impossible to import from a test — and a clipboard that cannot load is
|
|
24
|
+
* already the case this function answers with `false`.
|
|
25
|
+
*
|
|
21
26
|
* @returns whether the text reached the clipboard.
|
|
22
27
|
*/
|
|
23
28
|
export async function copyToClipboard(text) {
|
|
@@ -28,6 +33,7 @@ export async function copyToClipboard(text) {
|
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
try {
|
|
36
|
+
const { default: clipboardy } = await import("clipboardy");
|
|
31
37
|
return await withTimeout(clipboardy.write(text).then(() => true));
|
|
32
38
|
}
|
|
33
39
|
catch {
|
|
@@ -3,6 +3,7 @@ export declare enum CLIParameters {
|
|
|
3
3
|
Artifacts = "artifacts",
|
|
4
4
|
ConfigName = "configName",
|
|
5
5
|
ContentTypes = "contentTypes",
|
|
6
|
+
Catalog = "catalog",
|
|
6
7
|
Cookies = "cookies",
|
|
7
8
|
Count = "count",
|
|
8
9
|
CreateTests = "CreateTests",
|
|
@@ -170,6 +171,10 @@ export declare const CLI_QUERY_PREWARM: {
|
|
|
170
171
|
/**
|
|
171
172
|
* Where a query that is executed actually runs.
|
|
172
173
|
*
|
|
174
|
+
* The catalog is the account's, not the query's: our own logins reach the rum
|
|
175
|
+
* tables under `live`, while a role scoped to one account reaches the same
|
|
176
|
+
* tables under `AwsDataCatalog`. `SK_ATHENA_CATALOG` lets a CI job say so once.
|
|
177
|
+
*
|
|
173
178
|
* Athena resolves the account's default workgroup when none is named, which is
|
|
174
179
|
* only right where the credentials are allowed to use it — a role scoped to one
|
|
175
180
|
* workgroup is refused on any other, and the result location is a workgroup
|
|
@@ -177,6 +182,7 @@ export declare const CLI_QUERY_PREWARM: {
|
|
|
177
182
|
* command in the job.
|
|
178
183
|
*/
|
|
179
184
|
export declare const CLI_ATHENA: {
|
|
185
|
+
catalog: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
180
186
|
workgroup: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
181
187
|
};
|
|
182
188
|
/** How the parameter query presents what it found. */
|
|
@@ -4,12 +4,14 @@ import { Args, Flags } from "@oclif/core";
|
|
|
4
4
|
// library.
|
|
5
5
|
import { parseMatchPattern } from "../services/query-builder/rules/match-pattern.js";
|
|
6
6
|
import { DEVICE_VARIATIONS } from "../services/query-builder/queries/device.js";
|
|
7
|
+
import { DEFAULT_ATHENA_CATALOG } from "../services/athena/athena-service-model.js";
|
|
7
8
|
export var CLIParameters;
|
|
8
9
|
(function (CLIParameters) {
|
|
9
10
|
CLIParameters["AppName"] = "app";
|
|
10
11
|
CLIParameters["Artifacts"] = "artifacts";
|
|
11
12
|
CLIParameters["ConfigName"] = "configName";
|
|
12
13
|
CLIParameters["ContentTypes"] = "contentTypes";
|
|
14
|
+
CLIParameters["Catalog"] = "catalog";
|
|
13
15
|
CLIParameters["Cookies"] = "cookies";
|
|
14
16
|
CLIParameters["Count"] = "count";
|
|
15
17
|
CLIParameters["CreateTests"] = "CreateTests";
|
|
@@ -253,6 +255,10 @@ export const CLI_QUERY_PREWARM = {
|
|
|
253
255
|
/**
|
|
254
256
|
* Where a query that is executed actually runs.
|
|
255
257
|
*
|
|
258
|
+
* The catalog is the account's, not the query's: our own logins reach the rum
|
|
259
|
+
* tables under `live`, while a role scoped to one account reaches the same
|
|
260
|
+
* tables under `AwsDataCatalog`. `SK_ATHENA_CATALOG` lets a CI job say so once.
|
|
261
|
+
*
|
|
256
262
|
* Athena resolves the account's default workgroup when none is named, which is
|
|
257
263
|
* only right where the credentials are allowed to use it — a role scoped to one
|
|
258
264
|
* workgroup is refused on any other, and the result location is a workgroup
|
|
@@ -260,6 +266,12 @@ export const CLI_QUERY_PREWARM = {
|
|
|
260
266
|
* command in the job.
|
|
261
267
|
*/
|
|
262
268
|
export const CLI_ATHENA = {
|
|
269
|
+
[CLIParameters.Catalog]: Flags.string({
|
|
270
|
+
default: DEFAULT_ATHENA_CATALOG,
|
|
271
|
+
description: "Athena data catalog holding the rum tables. Names the catalog both in the query and in the context it runs in, so the two always agree",
|
|
272
|
+
env: "SK_ATHENA_CATALOG",
|
|
273
|
+
required: false,
|
|
274
|
+
}),
|
|
263
275
|
[CLIParameters.Workgroup]: Flags.string({
|
|
264
276
|
description: "Athena workgroup to run the query in, e.g. 'prewarming'. Defaults to the account's default workgroup",
|
|
265
277
|
env: "SK_ATHENA_WORKGROUP",
|
|
@@ -2,6 +2,7 @@ import { AthenaService } from "./athena-query-service.js";
|
|
|
2
2
|
export declare class AthenaServiceFactory {
|
|
3
3
|
private app?;
|
|
4
4
|
private workgroup?;
|
|
5
|
+
private catalog?;
|
|
5
6
|
service?: AthenaService;
|
|
6
7
|
/**
|
|
7
8
|
* @param app - the app to query. Enables the Baqend native query path (app
|
|
@@ -9,8 +10,11 @@ export declare class AthenaServiceFactory {
|
|
|
9
10
|
* the AWS path is available.
|
|
10
11
|
* @param workgroup - Athena workgroup for the AWS path. The Baqend path has
|
|
11
12
|
* no such notion, so it is ignored there.
|
|
13
|
+
* @param catalog - the data catalog the tables live in. The Baqend path is
|
|
14
|
+
* told so it can recognise the prefix, not so it can use it: the server
|
|
15
|
+
* resolves the catalog itself.
|
|
12
16
|
*/
|
|
13
|
-
constructor(app?: string, workgroup?: string);
|
|
17
|
+
constructor(app?: string, workgroup?: string, catalog?: string);
|
|
14
18
|
getService(): Promise<AthenaService>;
|
|
15
19
|
/**
|
|
16
20
|
* Prefer direct AWS Athena whenever AWS credentials are present — they
|
|
@@ -9,6 +9,7 @@ import { EntityManagerFactory } from "../config-api/entity-manager-factory.js";
|
|
|
9
9
|
export class AthenaServiceFactory {
|
|
10
10
|
app;
|
|
11
11
|
workgroup;
|
|
12
|
+
catalog;
|
|
12
13
|
service;
|
|
13
14
|
/**
|
|
14
15
|
* @param app - the app to query. Enables the Baqend native query path (app
|
|
@@ -16,10 +17,14 @@ export class AthenaServiceFactory {
|
|
|
16
17
|
* the AWS path is available.
|
|
17
18
|
* @param workgroup - Athena workgroup for the AWS path. The Baqend path has
|
|
18
19
|
* no such notion, so it is ignored there.
|
|
20
|
+
* @param catalog - the data catalog the tables live in. The Baqend path is
|
|
21
|
+
* told so it can recognise the prefix, not so it can use it: the server
|
|
22
|
+
* resolves the catalog itself.
|
|
19
23
|
*/
|
|
20
|
-
constructor(app, workgroup) {
|
|
24
|
+
constructor(app, workgroup, catalog) {
|
|
21
25
|
this.app = app;
|
|
22
26
|
this.workgroup = workgroup;
|
|
27
|
+
this.catalog = catalog;
|
|
23
28
|
}
|
|
24
29
|
async getService() {
|
|
25
30
|
if (!this.service) {
|
|
@@ -40,10 +45,10 @@ export class AthenaServiceFactory {
|
|
|
40
45
|
cli.comment(this.workgroup
|
|
41
46
|
? `Using AWS credentials for Athena queries, workgroup ${this.workgroup}.`
|
|
42
47
|
: "Using AWS credentials for Athena queries.");
|
|
43
|
-
return new AwsAthenaService(client, cli, this.workgroup);
|
|
48
|
+
return new AwsAthenaService(client, cli, this.workgroup, this.catalog);
|
|
44
49
|
}
|
|
45
50
|
if (this.app) {
|
|
46
|
-
return new BaqendAthenaService(this.app, new EntityManagerFactory(), cli);
|
|
51
|
+
return new BaqendAthenaService(this.app, new EntityManagerFactory(), cli, this.catalog);
|
|
47
52
|
}
|
|
48
53
|
throw new FetchCredentialsError("Cannot send Athena queries: no AWS credentials found and no app given for the Baqend API.");
|
|
49
54
|
}
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
export declare const DEFAULT_MAX_AGE_IN_MINUTES = 60;
|
|
2
|
+
/**
|
|
3
|
+
* The Athena data catalog holding the Speed Kit tables.
|
|
4
|
+
*
|
|
5
|
+
* Which catalog that is depends on the credentials: the name below is the one
|
|
6
|
+
* our own logins see, while a role scoped to one account reaches the same
|
|
7
|
+
* tables under `AwsDataCatalog`. It is therefore a default, not a constant —
|
|
8
|
+
* see the `--catalog` flag.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_ATHENA_CATALOG = "live";
|
|
11
|
+
/**
|
|
12
|
+
* The catalog to use, given what a caller asked for.
|
|
13
|
+
*
|
|
14
|
+
* A blank name is the default rather than an error: it reaches here from an
|
|
15
|
+
* unset environment variable in a CI file as easily as from a typo, and a
|
|
16
|
+
* query that reads `from .rum.pi` is no way to report it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function athenaCatalog(catalog?: string): string;
|
|
2
19
|
export declare const ATHENA_CONFIG: {
|
|
3
20
|
region: string;
|
|
4
21
|
};
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
export const DEFAULT_MAX_AGE_IN_MINUTES = 60;
|
|
2
|
+
/**
|
|
3
|
+
* The Athena data catalog holding the Speed Kit tables.
|
|
4
|
+
*
|
|
5
|
+
* Which catalog that is depends on the credentials: the name below is the one
|
|
6
|
+
* our own logins see, while a role scoped to one account reaches the same
|
|
7
|
+
* tables under `AwsDataCatalog`. It is therefore a default, not a constant —
|
|
8
|
+
* see the `--catalog` flag.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_ATHENA_CATALOG = "live";
|
|
11
|
+
/**
|
|
12
|
+
* The catalog to use, given what a caller asked for.
|
|
13
|
+
*
|
|
14
|
+
* A blank name is the default rather than an error: it reaches here from an
|
|
15
|
+
* unset environment variable in a CI file as easily as from a typo, and a
|
|
16
|
+
* query that reads `from .rum.pi` is no way to report it.
|
|
17
|
+
*/
|
|
18
|
+
export function athenaCatalog(catalog) {
|
|
19
|
+
return catalog?.trim() || DEFAULT_ATHENA_CATALOG;
|
|
20
|
+
}
|
|
2
21
|
export const ATHENA_CONFIG = {
|
|
3
22
|
region: "eu-central-1",
|
|
4
23
|
};
|
|
@@ -15,12 +15,22 @@ export declare class AwsAthenaService implements AthenaService {
|
|
|
15
15
|
* which is only right where the credentials are allowed to use it.
|
|
16
16
|
*/
|
|
17
17
|
private workgroup?;
|
|
18
|
+
/**
|
|
19
|
+
* The data catalog the query runs against. It has to be the one the query
|
|
20
|
+
* text names too, or Athena rejects the reference rather than the context.
|
|
21
|
+
*/
|
|
22
|
+
private catalog?;
|
|
18
23
|
constructor(client: AthenaClient, cli: CliServiceInterface,
|
|
19
24
|
/**
|
|
20
25
|
* Athena falls back to the account's default workgroup when this is unset,
|
|
21
26
|
* which is only right where the credentials are allowed to use it.
|
|
22
27
|
*/
|
|
23
|
-
workgroup?: string
|
|
28
|
+
workgroup?: string,
|
|
29
|
+
/**
|
|
30
|
+
* The data catalog the query runs against. It has to be the one the query
|
|
31
|
+
* text names too, or Athena rejects the reference rather than the context.
|
|
32
|
+
*/
|
|
33
|
+
catalog?: string);
|
|
24
34
|
getResult<T>(query: string, parameters?: string[], maxAgeInMinutes?: number): Promise<T[]>;
|
|
25
35
|
/** The query, from sending it off to the rows it produced. */
|
|
26
36
|
private runAndFetch;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GetQueryExecutionCommand, GetQueryResultsCommand, QueryExecutionState, StartQueryExecutionCommand, } from "@aws-sdk/client-athena";
|
|
2
|
-
import { DEFAULT_MAX_AGE_IN_MINUTES } from "./athena-service-model.js";
|
|
2
|
+
import { athenaCatalog, DEFAULT_MAX_AGE_IN_MINUTES, } from "./athena-service-model.js";
|
|
3
3
|
import { AthenaQueryError } from "./error/athena-query-error.js";
|
|
4
4
|
import { safe } from "../../helpers/safe.js";
|
|
5
5
|
/**
|
|
@@ -12,15 +12,22 @@ export class AwsAthenaService {
|
|
|
12
12
|
client;
|
|
13
13
|
cli;
|
|
14
14
|
workgroup;
|
|
15
|
+
catalog;
|
|
15
16
|
constructor(client, cli,
|
|
16
17
|
/**
|
|
17
18
|
* Athena falls back to the account's default workgroup when this is unset,
|
|
18
19
|
* which is only right where the credentials are allowed to use it.
|
|
19
20
|
*/
|
|
20
|
-
workgroup
|
|
21
|
+
workgroup,
|
|
22
|
+
/**
|
|
23
|
+
* The data catalog the query runs against. It has to be the one the query
|
|
24
|
+
* text names too, or Athena rejects the reference rather than the context.
|
|
25
|
+
*/
|
|
26
|
+
catalog) {
|
|
21
27
|
this.client = client;
|
|
22
28
|
this.cli = cli;
|
|
23
29
|
this.workgroup = workgroup;
|
|
30
|
+
this.catalog = catalog;
|
|
24
31
|
}
|
|
25
32
|
async getResult(query, parameters = [], maxAgeInMinutes = DEFAULT_MAX_AGE_IN_MINUTES) {
|
|
26
33
|
this.cli.startAction("ATHENA:QUERY", "execute athena query");
|
|
@@ -106,7 +113,7 @@ export class AwsAthenaService {
|
|
|
106
113
|
const command = new StartQueryExecutionCommand({
|
|
107
114
|
ExecutionParameters: executionParameters.length > 0 ? executionParameters : null,
|
|
108
115
|
QueryExecutionContext: {
|
|
109
|
-
Catalog:
|
|
116
|
+
Catalog: athenaCatalog(this.catalog),
|
|
110
117
|
},
|
|
111
118
|
QueryString: sql,
|
|
112
119
|
ResultReuseConfiguration: {
|
|
@@ -81,6 +81,22 @@ describe("AwsAthenaService", () => {
|
|
|
81
81
|
const start = sent.find((command) => command instanceof StartQueryExecutionCommand);
|
|
82
82
|
expect(start.input).to.not.have.property("WorkGroup");
|
|
83
83
|
});
|
|
84
|
+
it("should run against the catalog it was given", async () => {
|
|
85
|
+
// The context and the query text have to name the same catalog, or Athena
|
|
86
|
+
// rejects the reference in the query rather than the context.
|
|
87
|
+
const sent = [];
|
|
88
|
+
const service = new AwsAthenaService(clientAnswering("SUCCEEDED", undefined, sent), recordingCli().cli, undefined, "AwsDataCatalog");
|
|
89
|
+
await service.getResult("select 1", [], 0);
|
|
90
|
+
const [start] = sent.filter((command) => command instanceof StartQueryExecutionCommand);
|
|
91
|
+
expect(start.input.QueryExecutionContext.Catalog).to.equal("AwsDataCatalog");
|
|
92
|
+
});
|
|
93
|
+
it("should fall back to the live catalog", async () => {
|
|
94
|
+
const sent = [];
|
|
95
|
+
const service = new AwsAthenaService(clientAnswering("SUCCEEDED", undefined, sent), recordingCli().cli);
|
|
96
|
+
await service.getResult("select 1", [], 0);
|
|
97
|
+
const [start] = sent.filter((command) => command instanceof StartQueryExecutionCommand);
|
|
98
|
+
expect(start.input.QueryExecutionContext.Catalog).to.equal("live");
|
|
99
|
+
});
|
|
84
100
|
it("should not pass a failed query off as an empty result", async () => {
|
|
85
101
|
const { cli } = recordingCli();
|
|
86
102
|
const service = new AwsAthenaService(clientAnswering("CANCELLED"), cli);
|
|
@@ -8,8 +8,8 @@ import { CliServiceInterface } from "../cli/index.js";
|
|
|
8
8
|
*
|
|
9
9
|
* Two normalizations vs the AWS path:
|
|
10
10
|
* - `?` placeholders are inlined (the native path has no parameter binding).
|
|
11
|
-
* - the
|
|
12
|
-
*
|
|
11
|
+
* - the catalog prefix is stripped — the server derives catalog and database
|
|
12
|
+
* itself, and a 3-part `<catalog>.<db>.<table>` reference fails there
|
|
13
13
|
* (verified against a live app), whereas 2-part `<db>.<table>` works.
|
|
14
14
|
*/
|
|
15
15
|
export declare class BaqendAthenaService implements AthenaService {
|
|
@@ -17,8 +17,17 @@ export declare class BaqendAthenaService implements AthenaService {
|
|
|
17
17
|
private entityManagerFactory;
|
|
18
18
|
private cli;
|
|
19
19
|
private static readonly TRIGGERED_BY;
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/** A catalog qualifier in front of a `<db>.<table>` reference. */
|
|
21
|
+
private readonly catalogPrefix;
|
|
22
|
+
/** The catalog whose qualifier has to come back out of the query. */
|
|
23
|
+
private readonly catalog;
|
|
24
|
+
constructor(app: string, entityManagerFactory: EntityManagerFactory, cli: CliServiceInterface,
|
|
25
|
+
/**
|
|
26
|
+
* The catalog the queries name. Whichever one the caller runs under, the
|
|
27
|
+
* prefix has to come back out here — so the name is read rather than
|
|
28
|
+
* assumed.
|
|
29
|
+
*/
|
|
30
|
+
catalog?: string);
|
|
22
31
|
getResult<T>(query: string, parameters?: AthenaParameter[], maxAgeInMinutes?: number, bucket?: AthenaBucket): Promise<T[]>;
|
|
23
32
|
/**
|
|
24
33
|
* Inline the `?` placeholders with the given parameters (same quoting as the
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { DEFAULT_MAX_AGE_IN_MINUTES } from "./athena-service-model.js";
|
|
1
|
+
import { athenaCatalog, DEFAULT_MAX_AGE_IN_MINUTES, } from "./athena-service-model.js";
|
|
2
2
|
import { safe } from "../../helpers/safe.js";
|
|
3
|
+
/** A literal for use inside a regular expression. */
|
|
4
|
+
function escapeRegExp(value) {
|
|
5
|
+
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
8
|
* Runs Athena SQL through the Baqend API, authenticated by the app token only
|
|
5
9
|
* (no AWS login). Hits the native query path via
|
|
@@ -7,8 +11,8 @@ import { safe } from "../../helpers/safe.js";
|
|
|
7
11
|
*
|
|
8
12
|
* Two normalizations vs the AWS path:
|
|
9
13
|
* - `?` placeholders are inlined (the native path has no parameter binding).
|
|
10
|
-
* - the
|
|
11
|
-
*
|
|
14
|
+
* - the catalog prefix is stripped — the server derives catalog and database
|
|
15
|
+
* itself, and a 3-part `<catalog>.<db>.<table>` reference fails there
|
|
12
16
|
* (verified against a live app), whereas 2-part `<db>.<table>` works.
|
|
13
17
|
*/
|
|
14
18
|
export class BaqendAthenaService {
|
|
@@ -16,12 +20,22 @@ export class BaqendAthenaService {
|
|
|
16
20
|
entityManagerFactory;
|
|
17
21
|
cli;
|
|
18
22
|
static TRIGGERED_BY = "speed-kit-cli";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
/** A catalog qualifier in front of a `<db>.<table>` reference. */
|
|
24
|
+
catalogPrefix;
|
|
25
|
+
/** The catalog whose qualifier has to come back out of the query. */
|
|
26
|
+
catalog;
|
|
27
|
+
constructor(app, entityManagerFactory, cli,
|
|
28
|
+
/**
|
|
29
|
+
* The catalog the queries name. Whichever one the caller runs under, the
|
|
30
|
+
* prefix has to come back out here — so the name is read rather than
|
|
31
|
+
* assumed.
|
|
32
|
+
*/
|
|
33
|
+
catalog) {
|
|
22
34
|
this.app = app;
|
|
23
35
|
this.entityManagerFactory = entityManagerFactory;
|
|
24
36
|
this.cli = cli;
|
|
37
|
+
this.catalog = athenaCatalog(catalog);
|
|
38
|
+
this.catalogPrefix = new RegExp(`^${escapeRegExp(this.catalog)}\\.[a-z_]\\w*\\.[a-z_]`, "i");
|
|
25
39
|
}
|
|
26
40
|
async getResult(query, parameters = [], maxAgeInMinutes = DEFAULT_MAX_AGE_IN_MINUTES, bucket) {
|
|
27
41
|
if (!bucket) {
|
|
@@ -78,11 +92,12 @@ export class BaqendAthenaService {
|
|
|
78
92
|
out += typeof value === "string" ? `'${value}'` : String(value);
|
|
79
93
|
continue;
|
|
80
94
|
}
|
|
81
|
-
// strip
|
|
82
|
-
if ((
|
|
95
|
+
// strip the catalog qualifier at a word boundary
|
|
96
|
+
if (char.toLowerCase() === this.catalog[0].toLowerCase() &&
|
|
83
97
|
(index === 0 || !/\w/.test(query[index - 1])) &&
|
|
84
|
-
|
|
85
|
-
|
|
98
|
+
this.catalogPrefix.test(query.slice(index))) {
|
|
99
|
+
// skip the name; the loop's index++ skips the dot behind it
|
|
100
|
+
index += this.catalog.length;
|
|
86
101
|
continue;
|
|
87
102
|
}
|
|
88
103
|
out += char;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { AthenaBucket } from "./athena-query-service.js";
|
|
4
|
+
import { BaqendAthenaService } from "./baqend-athena-service.js";
|
|
5
|
+
const cli = {
|
|
6
|
+
failAction: () => undefined,
|
|
7
|
+
startAction: () => undefined,
|
|
8
|
+
successAction: () => undefined,
|
|
9
|
+
writeError: () => undefined,
|
|
10
|
+
};
|
|
11
|
+
/** An entity manager that records the sql it was asked to run. */
|
|
12
|
+
function factoryCapturing(sent) {
|
|
13
|
+
return {
|
|
14
|
+
getEntityManager: async () => ({
|
|
15
|
+
[AthenaBucket.RumPi]: {
|
|
16
|
+
executeQuery: async (sql) => {
|
|
17
|
+
sent.push(sql);
|
|
18
|
+
return [];
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** The sql that reached the Baqend API for a query run under `catalog`. */
|
|
25
|
+
async function sentFor(query, catalog) {
|
|
26
|
+
const sent = [];
|
|
27
|
+
const service = new BaqendAthenaService("shop-de", factoryCapturing(sent), cli, catalog);
|
|
28
|
+
await service.getResult(query, [], 60, AthenaBucket.RumPi);
|
|
29
|
+
return sent[0];
|
|
30
|
+
}
|
|
31
|
+
describe("BaqendAthenaService", () => {
|
|
32
|
+
it("should strip the default catalog qualifier", async () => {
|
|
33
|
+
// The native path derives catalog and database itself and rejects a
|
|
34
|
+
// three-part reference.
|
|
35
|
+
expect(await sentFor("select url from live.rum.pi")).to.equal("select url from rum.pi");
|
|
36
|
+
});
|
|
37
|
+
it("should strip whichever catalog the query names", async () => {
|
|
38
|
+
expect(await sentFor("select url from AwsDataCatalog.rum.pi", "AwsDataCatalog")).to.equal("select url from rum.pi");
|
|
39
|
+
});
|
|
40
|
+
it("should leave a catalog name inside a string literal alone", async () => {
|
|
41
|
+
expect(await sentFor("select 'https://live.foo.com' from live.rum.pi")).to.equal("select 'https://live.foo.com' from rum.pi");
|
|
42
|
+
});
|
|
43
|
+
it("should fall back to the default catalog when given a blank one", async () => {
|
|
44
|
+
// An unset variable in a CI file arrives as an empty string, and the
|
|
45
|
+
// stripping reads the first character of the name.
|
|
46
|
+
expect(await sentFor("select url from live.rum.pi", " ")).to.equal("select url from rum.pi");
|
|
47
|
+
});
|
|
48
|
+
it("should leave a word that merely starts like the catalog alone", async () => {
|
|
49
|
+
expect(await sentFor("select liveliness from live.rum.pi")).to.equal("select liveliness from rum.pi");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -8,6 +8,9 @@ export declare class CliService implements CliServiceInterface {
|
|
|
8
8
|
private buffer;
|
|
9
9
|
private progressBar?;
|
|
10
10
|
private spinners;
|
|
11
|
+
private readonly animates;
|
|
12
|
+
/** The text of each running action, for the log line that replaces it. */
|
|
13
|
+
private readonly actions;
|
|
11
14
|
constructor(quiet?: boolean);
|
|
12
15
|
code(code: string, buffered?: boolean): void;
|
|
13
16
|
codeStyle(code: string): string;
|
|
@@ -30,6 +33,12 @@ export declare class CliService implements CliServiceInterface {
|
|
|
30
33
|
endAction(name: string, message?: string): void;
|
|
31
34
|
failAction(name: string, message?: string): void;
|
|
32
35
|
successAction(name: string, message?: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* One line for an action that ended, where a spinner cannot be drawn. The
|
|
38
|
+
* text comes from the action that started, so a caller that ends one without
|
|
39
|
+
* a message still says which one it ended.
|
|
40
|
+
*/
|
|
41
|
+
private logAction;
|
|
33
42
|
startProgress(total: number, start?: number, payload?: object, config?: Options): void;
|
|
34
43
|
updateProgress(current: number, payload?: object): void;
|
|
35
44
|
stopProgress(): void;
|
|
@@ -6,12 +6,27 @@ import Spinnies from "spinnies";
|
|
|
6
6
|
import { safe } from "../../helpers/safe.js";
|
|
7
7
|
import { execSync } from "node:child_process";
|
|
8
8
|
import ApplicationError from "../error-handling/error/application-error.js";
|
|
9
|
+
/**
|
|
10
|
+
* Whether a spinner can actually spin here.
|
|
11
|
+
*
|
|
12
|
+
* The same condition spinnies itself uses to decide, so the two never
|
|
13
|
+
* disagree: without it spinnies falls back to re-rendering *every* spinner it
|
|
14
|
+
* knows on every update, which in a CI log means the first action's line is
|
|
15
|
+
* printed again under each later one — a page of "build prewarm-query" for a
|
|
16
|
+
* job that only ever built one.
|
|
17
|
+
*/
|
|
18
|
+
function canAnimate() {
|
|
19
|
+
return Boolean(!process.env.CI && process.stderr && process.stderr.isTTY);
|
|
20
|
+
}
|
|
9
21
|
export class CliService {
|
|
10
22
|
quiet;
|
|
11
23
|
style;
|
|
12
24
|
buffer = [];
|
|
13
25
|
progressBar;
|
|
14
26
|
spinners;
|
|
27
|
+
animates = canAnimate();
|
|
28
|
+
/** The text of each running action, for the log line that replaces it. */
|
|
29
|
+
actions = new Map();
|
|
15
30
|
constructor(quiet = false) {
|
|
16
31
|
this.quiet = quiet;
|
|
17
32
|
this.spinners = new Spinnies();
|
|
@@ -90,26 +105,65 @@ export class CliService {
|
|
|
90
105
|
if (this.quiet) {
|
|
91
106
|
return;
|
|
92
107
|
}
|
|
108
|
+
// Nothing is logged while the action runs: a log that cannot animate has
|
|
109
|
+
// no use for "this is happening", only for what happened.
|
|
110
|
+
if (!this.animates) {
|
|
111
|
+
this.actions.set(name, message);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
93
114
|
this.spinners.add(name, { text: message });
|
|
94
115
|
}
|
|
95
116
|
endAction(name, message) {
|
|
96
|
-
if (this.quiet
|
|
117
|
+
if (this.quiet) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!this.animates) {
|
|
121
|
+
this.logAction("-", name, message);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!this.spinners.pick(name)) {
|
|
97
125
|
return;
|
|
98
126
|
}
|
|
99
127
|
this.spinners.update(name, { status: "stopped", text: message });
|
|
100
128
|
}
|
|
101
129
|
failAction(name, message) {
|
|
102
|
-
if (this.quiet
|
|
130
|
+
if (this.quiet) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!this.animates) {
|
|
134
|
+
this.logAction(this.style.red("✖"), name, message);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!this.spinners.pick(name)) {
|
|
103
138
|
return;
|
|
104
139
|
}
|
|
105
140
|
this.spinners.fail(name, { text: message });
|
|
106
141
|
}
|
|
107
142
|
successAction(name, message) {
|
|
108
|
-
if (this.quiet
|
|
143
|
+
if (this.quiet) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (!this.animates) {
|
|
147
|
+
this.logAction(this.style.green("✔"), name, message);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!this.spinners.pick(name)) {
|
|
109
151
|
return;
|
|
110
152
|
}
|
|
111
153
|
this.spinners.succeed(name, { text: message, succeedColor: "white" });
|
|
112
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* One line for an action that ended, where a spinner cannot be drawn. The
|
|
157
|
+
* text comes from the action that started, so a caller that ends one without
|
|
158
|
+
* a message still says which one it ended.
|
|
159
|
+
*/
|
|
160
|
+
logAction(symbol, name, message) {
|
|
161
|
+
const text = message ?? this.actions.get(name);
|
|
162
|
+
if (!this.actions.delete(name) && !message) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
console.log(`${symbol} ${text}`);
|
|
166
|
+
}
|
|
113
167
|
startProgress(total, start = 0, payload, config) {
|
|
114
168
|
if (this.quiet) {
|
|
115
169
|
return;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "mocha";
|
|
3
|
+
import { CliService } from "./cli-service.js";
|
|
4
|
+
/** The lines a block of work put on stdout. */
|
|
5
|
+
function captured(work) {
|
|
6
|
+
const lines = [];
|
|
7
|
+
const log = console.log;
|
|
8
|
+
console.log = (line) => lines.push(line);
|
|
9
|
+
try {
|
|
10
|
+
work();
|
|
11
|
+
}
|
|
12
|
+
finally {
|
|
13
|
+
console.log = log;
|
|
14
|
+
}
|
|
15
|
+
return lines;
|
|
16
|
+
}
|
|
17
|
+
describe("CliService actions without a terminal", () => {
|
|
18
|
+
let ci;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
ci = process.env.CI;
|
|
21
|
+
// What a CI runner sets, and what spinnies reads to decide it cannot spin.
|
|
22
|
+
process.env.CI = "true";
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
if (ci === undefined) {
|
|
26
|
+
delete process.env.CI;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
process.env.CI = ci;
|
|
30
|
+
});
|
|
31
|
+
it("should say nothing while an action runs", () => {
|
|
32
|
+
const cli = new CliService();
|
|
33
|
+
expect(captured(() => cli.startAction("BUILD", "build prewarm-query"))).to.have.lengthOf(0);
|
|
34
|
+
});
|
|
35
|
+
it("should log an action that ended exactly once", () => {
|
|
36
|
+
// Spinnies re-renders every spinner it knows on each update when it cannot
|
|
37
|
+
// animate, so a running action reappears under each later one — a job with
|
|
38
|
+
// four actions logged "build prewarm-query" four times.
|
|
39
|
+
const cli = new CliService();
|
|
40
|
+
const lines = captured(() => {
|
|
41
|
+
cli.startAction("BUILD", "build prewarm-query");
|
|
42
|
+
cli.successAction("BUILD");
|
|
43
|
+
cli.startAction("QUERY", "execute athena query");
|
|
44
|
+
cli.failAction("QUERY");
|
|
45
|
+
});
|
|
46
|
+
expect(lines).to.have.lengthOf(2);
|
|
47
|
+
expect(lines[0]).to.contain("build prewarm-query");
|
|
48
|
+
expect(lines[1]).to.contain("execute athena query");
|
|
49
|
+
expect(lines.join("\n")).to.not.contain("build prewarm-query\n✖ execute athena query\n✔ build prewarm-query");
|
|
50
|
+
});
|
|
51
|
+
it("should keep the text of the action it ends", () => {
|
|
52
|
+
const cli = new CliService();
|
|
53
|
+
const lines = captured(() => {
|
|
54
|
+
cli.startAction("WARM", "warm the list");
|
|
55
|
+
cli.endAction("WARM");
|
|
56
|
+
});
|
|
57
|
+
expect(lines).to.deep.equal(["- warm the list"]);
|
|
58
|
+
});
|
|
59
|
+
it("should stay silent when it was told to be quiet", () => {
|
|
60
|
+
const cli = new CliService(true);
|
|
61
|
+
const lines = captured(() => {
|
|
62
|
+
cli.startAction("BUILD", "build prewarm-query");
|
|
63
|
+
cli.successAction("BUILD");
|
|
64
|
+
});
|
|
65
|
+
expect(lines).to.have.lengthOf(0);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -16,6 +16,8 @@ export interface AutoPreWarmContext extends QueryFilterOptions {
|
|
|
16
16
|
readonly minHits?: number;
|
|
17
17
|
/** Athena workgroup the query runs in. */
|
|
18
18
|
readonly workgroup?: string;
|
|
19
|
+
/** Athena data catalog the rum tables live in. */
|
|
20
|
+
readonly catalog?: string;
|
|
19
21
|
/** Variations to pair every url with, when no traffic can reveal them. */
|
|
20
22
|
readonly variation?: readonly string[];
|
|
21
23
|
readonly variationPath?: string;
|
|
@@ -45,6 +45,7 @@ export class AutoPreWarmFactory {
|
|
|
45
45
|
quiet: this.context.quiet,
|
|
46
46
|
variations: this.context.variation,
|
|
47
47
|
workgroup: this.context.workgroup,
|
|
48
|
+
catalog: this.context.catalog,
|
|
48
49
|
}, this.userConfig).buildService(QueryType.prewarm);
|
|
49
50
|
const app = queryService.app;
|
|
50
51
|
const rows = await queryService.execute();
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { RuleSubjectExpressions } from "../rules/index.js";
|
|
2
2
|
import { QueryInput } from "./query-model.js";
|
|
3
|
-
/** The
|
|
4
|
-
export declare const PAGE_IMPRESSIONS = "
|
|
3
|
+
/** The database and table every query reads from, within its catalog. */
|
|
4
|
+
export declare const PAGE_IMPRESSIONS = "rum.pi";
|
|
5
|
+
/**
|
|
6
|
+
* The relation every query reads from, qualified by the catalog it lives in.
|
|
7
|
+
*
|
|
8
|
+
* The catalog is named in the query and not only in the context it runs in, so
|
|
9
|
+
* that a query pasted into the Athena console reads the same tables as the one
|
|
10
|
+
* the CLI ran — the console resolves an unqualified name against whichever
|
|
11
|
+
* catalog its dropdown happens to hold.
|
|
12
|
+
*/
|
|
13
|
+
export declare function pageImpressions(catalog?: string): string;
|
|
5
14
|
/** The name of the common table expression the queries select from. */
|
|
6
15
|
export declare const PAGE_SOURCE = "pages";
|
|
7
16
|
export interface PageSource {
|
|
@@ -2,8 +2,20 @@ import { column, formatPredicate } from "../sql/index.js";
|
|
|
2
2
|
import { resolvedSubject, urlSubjects, } from "../rules/index.js";
|
|
3
3
|
import { normalizeDevices } from "./device.js";
|
|
4
4
|
import { buildRowFilter } from "./page-filter.js";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { athenaCatalog } from "../../athena/athena-service-model.js";
|
|
6
|
+
/** The database and table every query reads from, within its catalog. */
|
|
7
|
+
export const PAGE_IMPRESSIONS = "rum.pi";
|
|
8
|
+
/**
|
|
9
|
+
* The relation every query reads from, qualified by the catalog it lives in.
|
|
10
|
+
*
|
|
11
|
+
* The catalog is named in the query and not only in the context it runs in, so
|
|
12
|
+
* that a query pasted into the Athena console reads the same tables as the one
|
|
13
|
+
* the CLI ran — the console resolves an unqualified name against whichever
|
|
14
|
+
* catalog its dropdown happens to hold.
|
|
15
|
+
*/
|
|
16
|
+
export function pageImpressions(catalog) {
|
|
17
|
+
return `${athenaCatalog(catalog)}.${PAGE_IMPRESSIONS}`;
|
|
18
|
+
}
|
|
7
19
|
/** The name of the common table expression the queries select from. */
|
|
8
20
|
export const PAGE_SOURCE = "pages";
|
|
9
21
|
const COLUMN_NAMES = {
|
|
@@ -37,7 +49,7 @@ export function buildPageSource(input, options = {}) {
|
|
|
37
49
|
const cte = `${PAGE_SOURCE} as (
|
|
38
50
|
select
|
|
39
51
|
${projections.join(",\n ")}
|
|
40
|
-
from ${
|
|
52
|
+
from ${pageImpressions(input.catalog)}
|
|
41
53
|
where ${formatPredicate(buildRowFilter(input), { level: 2 })}
|
|
42
54
|
)`;
|
|
43
55
|
// An unused subject keeps its inline expression: nothing refers to it, and
|
|
@@ -61,6 +61,8 @@ export interface ParameterOptions {
|
|
|
61
61
|
/** Everything a query is built from. */
|
|
62
62
|
export interface QueryInput extends ParameterOptions, PrewarmOptions {
|
|
63
63
|
readonly app: string;
|
|
64
|
+
/** The data catalog the rum tables live in. Defaults to `live`. */
|
|
65
|
+
readonly catalog?: string;
|
|
64
66
|
/** Size of the window the query looks at, in days. */
|
|
65
67
|
readonly days: number;
|
|
66
68
|
/** Drop page impressions of pages marked `noindex`. */
|
|
@@ -188,6 +188,23 @@ describe("the --variation pairing", () => {
|
|
|
188
188
|
expect(buildPrewarmQuery(inputFor(CONFIG, { variations: ["it's"] })).sql).to.contain("array['it''s']");
|
|
189
189
|
});
|
|
190
190
|
});
|
|
191
|
+
describe("the --catalog the query reads from", () => {
|
|
192
|
+
it("should read the rum tables from the live catalog by default", () => {
|
|
193
|
+
expect(buildPrewarmQuery(inputFor(CONFIG)).sql).to.contain("from live.rum.pi");
|
|
194
|
+
expect(buildParameterQuery(inputFor(CONFIG)).sql).to.contain("from live.rum.pi");
|
|
195
|
+
});
|
|
196
|
+
it("should read from the live catalog when given a blank one", () => {
|
|
197
|
+
expect(buildPrewarmQuery(inputFor(CONFIG, { catalog: "" })).sql).to.contain("from live.rum.pi");
|
|
198
|
+
});
|
|
199
|
+
it("should name the catalog it was given instead", () => {
|
|
200
|
+
// Which catalog holds the tables depends on the credentials: our own
|
|
201
|
+
// logins see them under `live`, a role scoped to one account under
|
|
202
|
+
// `AwsDataCatalog`.
|
|
203
|
+
const options = { catalog: "AwsDataCatalog" };
|
|
204
|
+
expect(buildPrewarmQuery(inputFor(CONFIG, options)).sql).to.contain("from AwsDataCatalog.rum.pi");
|
|
205
|
+
expect(buildParameterQuery(inputFor(CONFIG, options)).sql).to.not.contain("live.rum.pi");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
191
208
|
describe("the --count report", () => {
|
|
192
209
|
it("should count the urls and the traffic they stand for", () => {
|
|
193
210
|
const { sql } = buildPrewarmQuery(inputFor(CONFIG, { count: true }));
|
|
@@ -25,6 +25,7 @@ export class QueryBuilderFactory {
|
|
|
25
25
|
const serviceContext = {
|
|
26
26
|
app,
|
|
27
27
|
bySet: this.context.bySet,
|
|
28
|
+
catalog: this.context.catalog,
|
|
28
29
|
count: this.context.count,
|
|
29
30
|
days: this.context.days,
|
|
30
31
|
devices: this.context.devices,
|
|
@@ -47,7 +48,7 @@ export class QueryBuilderFactory {
|
|
|
47
48
|
// The Athena service is only needed when the query is executed directly —
|
|
48
49
|
// which `--count` is, a count query being of no use as text.
|
|
49
50
|
const athenaService = this.context.execute || this.context.count
|
|
50
|
-
? await new AthenaServiceFactory(app, this.context.workgroup).getService()
|
|
51
|
+
? await new AthenaServiceFactory(app, this.context.workgroup, this.context.catalog).getService()
|
|
51
52
|
: undefined;
|
|
52
53
|
return new QueryBuilderService(serviceContext, parseRuleSets(speedKitConfig), cli, athenaService);
|
|
53
54
|
}
|
|
@@ -16,6 +16,8 @@ export interface QueryFilterOptions {
|
|
|
16
16
|
}
|
|
17
17
|
export interface QueryBuilderContext extends ParameterOptions, PrewarmOptions, QueryFilterOptions {
|
|
18
18
|
readonly app: string;
|
|
19
|
+
/** Athena data catalog the rum tables live in. */
|
|
20
|
+
readonly catalog?: string;
|
|
19
21
|
readonly execute?: boolean;
|
|
20
22
|
/** Report every rule the query leaves out, reduces or approximates. */
|
|
21
23
|
readonly explain?: boolean;
|
|
@@ -26,6 +28,8 @@ export interface QueryBuilderContext extends ParameterOptions, PrewarmOptions, Q
|
|
|
26
28
|
readonly quiet?: boolean;
|
|
27
29
|
}
|
|
28
30
|
export interface QueryBuilderFactoryContext extends ParameterOptions, PrewarmOptions, QueryFilterOptions {
|
|
31
|
+
/** Athena data catalog the rum tables live in. */
|
|
32
|
+
readonly catalog?: string;
|
|
29
33
|
readonly configName: string;
|
|
30
34
|
readonly customerPath: string;
|
|
31
35
|
readonly execute?: boolean;
|
|
@@ -44,10 +44,10 @@ export class QueryBuilderService {
|
|
|
44
44
|
this.cli.startAction(action, `build ${this.context.queryType}-query`);
|
|
45
45
|
const result = safe(() => this.build());
|
|
46
46
|
if (result.success !== true) {
|
|
47
|
+
// Fail the action before throwing: the spinner is a timer, and one left
|
|
48
|
+
// running keeps the process alive with nothing to do.
|
|
47
49
|
this.cli.failAction(action, result.error);
|
|
48
|
-
|
|
49
|
-
console.error(result.errorObj);
|
|
50
|
-
return;
|
|
50
|
+
throw result.errorObj ?? new Error(result.error);
|
|
51
51
|
}
|
|
52
52
|
this.cli.successAction(action);
|
|
53
53
|
const query = result.data;
|
|
@@ -99,6 +99,7 @@ export class QueryBuilderService {
|
|
|
99
99
|
build() {
|
|
100
100
|
const input = {
|
|
101
101
|
app: this.context.app,
|
|
102
|
+
catalog: this.context.catalog,
|
|
102
103
|
bySet: this.context.bySet,
|
|
103
104
|
count: this.context.count,
|
|
104
105
|
days: this.context.days ?? DEFAULT_QUERY_DAYS[this.context.queryType],
|
|
@@ -178,9 +179,12 @@ export class QueryBuilderService {
|
|
|
178
179
|
this.cli.startAction("QUERY:COUNT", "count the pre-warm list");
|
|
179
180
|
const result = await safe(this.runQuery(query));
|
|
180
181
|
if (result.success !== true) {
|
|
182
|
+
// Thrown, not reported and swallowed. A count that did not run is a
|
|
183
|
+
// failed command, and something driving this from a script — a CI job
|
|
184
|
+
// reading the exit code — has no other way to find that out. Printing
|
|
185
|
+
// the message and exiting 0 made a broken query look like a clean run.
|
|
181
186
|
this.cli.failAction("QUERY:COUNT");
|
|
182
|
-
|
|
183
|
-
return;
|
|
187
|
+
throw result.errorObj ?? new Error(result.error);
|
|
184
188
|
}
|
|
185
189
|
this.cli.successAction("QUERY:COUNT");
|
|
186
190
|
const rows = result.data;
|
|
@@ -210,9 +214,10 @@ export class QueryBuilderService {
|
|
|
210
214
|
this.cli.startAction("QUERY:EXECUTE", "execute query and export csv");
|
|
211
215
|
const result = await safe(this.runQuery(query));
|
|
212
216
|
if (result.success !== true) {
|
|
217
|
+
// Thrown for the same reason as the count path: an export that produced
|
|
218
|
+
// no file must not exit 0.
|
|
213
219
|
this.cli.failAction("QUERY:EXECUTE");
|
|
214
|
-
|
|
215
|
-
return;
|
|
220
|
+
throw result.errorObj ?? new Error(result.error);
|
|
216
221
|
}
|
|
217
222
|
const rows = result.data;
|
|
218
223
|
const date = new Date().toISOString().split("T")[0];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { QueryBuilderService } from "./query-builder-service.js";
|
|
4
|
+
import { QueryType } from "./query-builder-model.js";
|
|
5
|
+
import { parseRuleSets } from "./rules/index.js";
|
|
6
|
+
/** Records the actions so a left-running spinner shows up as a missing fail. */
|
|
7
|
+
function recordingCli() {
|
|
8
|
+
const calls = [];
|
|
9
|
+
const cli = {
|
|
10
|
+
comment: () => calls.push("comment"),
|
|
11
|
+
failAction: (name) => calls.push(`fail:${name}`),
|
|
12
|
+
startAction: (name) => calls.push(`start:${name}`),
|
|
13
|
+
successAction: (name) => calls.push(`success:${name}`),
|
|
14
|
+
table: () => calls.push("table"),
|
|
15
|
+
writeError: (message) => calls.push(`error:${message}`),
|
|
16
|
+
writeSuccess: () => calls.push("success"),
|
|
17
|
+
writeWarning: () => calls.push("warning"),
|
|
18
|
+
};
|
|
19
|
+
return { calls, cli };
|
|
20
|
+
}
|
|
21
|
+
/** An Athena that always refuses, the way a bad catalog or workgroup does. */
|
|
22
|
+
const refusingAthena = {
|
|
23
|
+
getResult: () => Promise.reject(new Error("The Amazon Athena query failed to run with error message: CATALOG_NOT_FOUND")),
|
|
24
|
+
};
|
|
25
|
+
function countService(cli) {
|
|
26
|
+
return new QueryBuilderService({
|
|
27
|
+
app: "obi-de",
|
|
28
|
+
count: true,
|
|
29
|
+
hosts: ["www.obi.de"],
|
|
30
|
+
queryType: QueryType.prewarm,
|
|
31
|
+
quiet: true,
|
|
32
|
+
},
|
|
33
|
+
// The real parser on an empty config: a hand-rolled stand-in would fail the
|
|
34
|
+
// build step and the test would pass for the wrong reason.
|
|
35
|
+
parseRuleSets({}), cli, refusingAthena);
|
|
36
|
+
}
|
|
37
|
+
describe("QueryBuilderService — a query that could not run", () => {
|
|
38
|
+
// Regression: this used to print the message and return, so the process
|
|
39
|
+
// exited 0. A CI job counting urls then reported success with no count, and
|
|
40
|
+
// the pipeline that read it had no way to tell a broken query from an empty
|
|
41
|
+
// one.
|
|
42
|
+
it("fails the command instead of exiting cleanly", async () => {
|
|
43
|
+
const { cli } = recordingCli();
|
|
44
|
+
const outcome = await countService(cli)
|
|
45
|
+
.run()
|
|
46
|
+
.then(() => "resolved")
|
|
47
|
+
.catch(() => "rejected");
|
|
48
|
+
expect(outcome).to.equal("rejected");
|
|
49
|
+
});
|
|
50
|
+
it("carries the reason, so the log says what went wrong", async () => {
|
|
51
|
+
const { cli } = recordingCli();
|
|
52
|
+
const error = await countService(cli)
|
|
53
|
+
.run()
|
|
54
|
+
.then(() => undefined)
|
|
55
|
+
.catch((thrown) => thrown);
|
|
56
|
+
expect(error?.message).to.contain("CATALOG_NOT_FOUND");
|
|
57
|
+
});
|
|
58
|
+
it("ends the spinner before throwing — it is a timer that holds the process open", async () => {
|
|
59
|
+
const { calls, cli } = recordingCli();
|
|
60
|
+
await countService(cli)
|
|
61
|
+
.run()
|
|
62
|
+
.catch(() => undefined);
|
|
63
|
+
expect(calls).to.contain("fail:QUERY:COUNT");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -8,6 +8,7 @@ import { QueryFilterOptions, QueryType } from "./query-builder-model.js";
|
|
|
8
8
|
* the two the caller gives rather than the list the builder works on.
|
|
9
9
|
*/
|
|
10
10
|
export interface QueryCommandFlags extends ParameterOptions, Omit<PrewarmOptions, "devices" | "variations">, QueryFilterOptions {
|
|
11
|
+
readonly catalog?: string;
|
|
11
12
|
readonly configName: string;
|
|
12
13
|
readonly device?: readonly string[];
|
|
13
14
|
readonly execute?: boolean;
|
|
@@ -12,6 +12,7 @@ import { normalizeDevices } from "./queries/device.js";
|
|
|
12
12
|
export async function runQuery(queryType, customerPath, flags, config) {
|
|
13
13
|
const service = await new QueryBuilderFactory({
|
|
14
14
|
bySet: flags.bySet,
|
|
15
|
+
catalog: flags.catalog,
|
|
15
16
|
configName: flags.configName,
|
|
16
17
|
count: flags.count,
|
|
17
18
|
customerPath,
|
package/oclif.manifest.json
CHANGED
|
@@ -19,6 +19,16 @@
|
|
|
19
19
|
"$ sk auto-prewarm customers/decathlon.de --workgroup prewarming"
|
|
20
20
|
],
|
|
21
21
|
"flags": {
|
|
22
|
+
"catalog": {
|
|
23
|
+
"description": "Athena data catalog holding the rum tables. Names the catalog both in the query and in the context it runs in, so the two always agree",
|
|
24
|
+
"env": "SK_ATHENA_CATALOG",
|
|
25
|
+
"name": "catalog",
|
|
26
|
+
"required": false,
|
|
27
|
+
"default": "live",
|
|
28
|
+
"hasDynamicHelp": false,
|
|
29
|
+
"multiple": false,
|
|
30
|
+
"type": "option"
|
|
31
|
+
},
|
|
22
32
|
"workgroup": {
|
|
23
33
|
"description": "Athena workgroup to run the query in, e.g. 'prewarming'. Defaults to the account's default workgroup",
|
|
24
34
|
"env": "SK_ATHENA_WORKGROUP",
|
|
@@ -1342,6 +1352,16 @@
|
|
|
1342
1352
|
"$ sk query parameter customers/decathlon.de --explain"
|
|
1343
1353
|
],
|
|
1344
1354
|
"flags": {
|
|
1355
|
+
"catalog": {
|
|
1356
|
+
"description": "Athena data catalog holding the rum tables. Names the catalog both in the query and in the context it runs in, so the two always agree",
|
|
1357
|
+
"env": "SK_ATHENA_CATALOG",
|
|
1358
|
+
"name": "catalog",
|
|
1359
|
+
"required": false,
|
|
1360
|
+
"default": "live",
|
|
1361
|
+
"hasDynamicHelp": false,
|
|
1362
|
+
"multiple": false,
|
|
1363
|
+
"type": "option"
|
|
1364
|
+
},
|
|
1345
1365
|
"workgroup": {
|
|
1346
1366
|
"description": "Athena workgroup to run the query in, e.g. 'prewarming'. Defaults to the account's default workgroup",
|
|
1347
1367
|
"env": "SK_ATHENA_WORKGROUP",
|
|
@@ -1462,6 +1482,16 @@
|
|
|
1462
1482
|
"$ sk query prewarm customers/decathlon.de --count"
|
|
1463
1483
|
],
|
|
1464
1484
|
"flags": {
|
|
1485
|
+
"catalog": {
|
|
1486
|
+
"description": "Athena data catalog holding the rum tables. Names the catalog both in the query and in the context it runs in, so the two always agree",
|
|
1487
|
+
"env": "SK_ATHENA_CATALOG",
|
|
1488
|
+
"name": "catalog",
|
|
1489
|
+
"required": false,
|
|
1490
|
+
"default": "live",
|
|
1491
|
+
"hasDynamicHelp": false,
|
|
1492
|
+
"multiple": false,
|
|
1493
|
+
"type": "option"
|
|
1494
|
+
},
|
|
1465
1495
|
"workgroup": {
|
|
1466
1496
|
"description": "Athena workgroup to run the query in, e.g. 'prewarming'. Defaults to the account's default workgroup",
|
|
1467
1497
|
"env": "SK_ATHENA_WORKGROUP",
|
|
@@ -1615,5 +1645,5 @@
|
|
|
1615
1645
|
]
|
|
1616
1646
|
}
|
|
1617
1647
|
},
|
|
1618
|
-
"version": "4.27.
|
|
1648
|
+
"version": "4.27.2"
|
|
1619
1649
|
}
|