@tenderprompt/cli 0.1.11 → 0.1.13
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/README.md +69 -8
- package/dist/index.js +703 -275
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,14 +43,24 @@ class TenderCliAnalyticsApiError extends Error {
|
|
|
43
43
|
reasonCode;
|
|
44
44
|
constructor(message, reasonCode) {
|
|
45
45
|
super(message);
|
|
46
|
-
this.reasonCode = reasonCode;
|
|
47
46
|
this.name = "TenderCliAnalyticsApiError";
|
|
47
|
+
this.reasonCode = reasonCode;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
class TenderCliDbApiError extends Error {
|
|
51
|
+
reasonCode;
|
|
52
|
+
constructor(message, reasonCode) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "TenderCliDbApiError";
|
|
55
|
+
this.reasonCode = reasonCode;
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
58
|
const DEFAULT_PROFILE_NAME = "default";
|
|
51
59
|
const DEFAULT_BASE_URL = "https://app.tenderprompt.com";
|
|
52
60
|
const ARTIFACT_SOURCE_EXPORT_MAX_FILES = 1000;
|
|
53
61
|
const ARTIFACT_SOURCE_EXPORT_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
62
|
+
const DB_SQL_STATEMENT_MAX_BYTES = 100 * 1024;
|
|
63
|
+
const DB_MAX_BOUND_PARAMETERS = 100;
|
|
54
64
|
function rootHelp() {
|
|
55
65
|
return `Usage: tender <command> [options]
|
|
56
66
|
|
|
@@ -89,6 +99,10 @@ Commands:
|
|
|
89
99
|
Run a server-validated chart spec
|
|
90
100
|
tender app analytics suggestions <app-id>
|
|
91
101
|
Generate useful starter chart suggestions
|
|
102
|
+
tender app db capabilities <app-id>
|
|
103
|
+
Describe app database query limits and policy
|
|
104
|
+
tender app db query <app-id> --sql <sql>
|
|
105
|
+
Run one read-only app database query
|
|
92
106
|
tender app context fetch <app-id>
|
|
93
107
|
Fetch managed agent context into a checkout
|
|
94
108
|
tender app context status <app-id>
|
|
@@ -120,6 +134,10 @@ Examples:
|
|
|
120
134
|
tender app analytics summary artifact_123 --range 30d --json
|
|
121
135
|
tender app analytics query artifact_123 --spec chart.json --json
|
|
122
136
|
tender app analytics suggestions artifact_123 --range 30d --json
|
|
137
|
+
tender app db capabilities artifact_123 --json
|
|
138
|
+
tender app db tables artifact_123 --json
|
|
139
|
+
tender app db schema artifact_123 --table votes --json
|
|
140
|
+
tender app db query artifact_123 --sql "select * from votes limit 20" --json
|
|
123
141
|
tender app context fetch artifact_123 --dir ./widget --dry-run --json
|
|
124
142
|
tender app context status artifact_123 --dir ./widget --json
|
|
125
143
|
tender app context refresh artifact_123 --dir ./widget --dry-run --json
|
|
@@ -142,12 +160,12 @@ Examples:
|
|
|
142
160
|
tender auth status --json`;
|
|
143
161
|
}
|
|
144
162
|
function authLoginHelp() {
|
|
145
|
-
return `Usage: tender auth login --device [--base-url <url>] [--profile <edit-preview|publish|create>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll] [--json]
|
|
163
|
+
return `Usage: tender auth login --device [--base-url <url>] [--profile <edit-preview|publish|create|db-read>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll] [--json]
|
|
146
164
|
|
|
147
165
|
Options:
|
|
148
166
|
--device Use the OAuth device-code flow.
|
|
149
167
|
--base-url <url> Tender base URL. Defaults to TENDER_BASE_URL or https://app.tenderprompt.com.
|
|
150
|
-
--profile <value> Token permission profile: edit-preview, publish, or
|
|
168
|
+
--profile <value> Token permission profile: edit-preview, publish, create, or db-read. Defaults to edit-preview.
|
|
151
169
|
--artifact <artifact-id> Scope the token to one artifact. Required for normal edit-preview agent work.
|
|
152
170
|
--ttl <value> Token lifetime: 7d, 14d, or 30d. Defaults to 7d.
|
|
153
171
|
--save-profile <name> Local config profile name. Defaults to default.
|
|
@@ -157,7 +175,8 @@ Options:
|
|
|
157
175
|
Examples:
|
|
158
176
|
tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
|
|
159
177
|
tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --json
|
|
160
|
-
tender auth login --device --profile create --ttl 7d --save-profile account
|
|
178
|
+
tender auth login --device --profile create --ttl 7d --save-profile account
|
|
179
|
+
tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db`;
|
|
161
180
|
}
|
|
162
181
|
function authStatusHelp() {
|
|
163
182
|
return `Usage: tender auth status [--profile <name>] [--json]
|
|
@@ -213,10 +232,18 @@ Commands:
|
|
|
213
232
|
Update a saved chart title or spec
|
|
214
233
|
tender app analytics charts delete <app-id>
|
|
215
234
|
Delete a saved chart
|
|
216
|
-
tender app analytics
|
|
217
|
-
List
|
|
218
|
-
tender app analytics
|
|
219
|
-
|
|
235
|
+
tender app analytics properties list <app-id>
|
|
236
|
+
List properties available to dashboard queries
|
|
237
|
+
tender app analytics properties activate <app-id>
|
|
238
|
+
Make a custom event property queryable
|
|
239
|
+
tender app db capabilities <app-id>
|
|
240
|
+
Describe app database query limits and policy
|
|
241
|
+
tender app db tables <app-id>
|
|
242
|
+
List app database tables and views
|
|
243
|
+
tender app db schema <app-id> --table <name>
|
|
244
|
+
Show table and index schema for one table
|
|
245
|
+
tender app db query <app-id> --sql <sql>
|
|
246
|
+
Run one read-only app database query
|
|
220
247
|
tender app context fetch <app-id>
|
|
221
248
|
Fetch AGENTS.md, skills, and Tender App scaffold files
|
|
222
249
|
tender app context status <app-id>
|
|
@@ -255,8 +282,12 @@ Examples:
|
|
|
255
282
|
tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json
|
|
256
283
|
tender app analytics charts update artifact_123 --chart analytics_chart_123 --title "Signup funnel" --spec funnel.json --dry-run --json
|
|
257
284
|
tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
|
|
258
|
-
tender app analytics
|
|
259
|
-
tender app analytics
|
|
285
|
+
tender app analytics properties list artifact_123 --json
|
|
286
|
+
tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
|
|
287
|
+
tender app db capabilities artifact_123 --json
|
|
288
|
+
tender app db tables artifact_123 --json
|
|
289
|
+
tender app db schema artifact_123 --table votes --json
|
|
290
|
+
tender app db query artifact_123 --sql "select * from votes limit 20" --json
|
|
260
291
|
tender app context fetch artifact_123 --dir ./widget --dry-run --json
|
|
261
292
|
tender app context status artifact_123 --dir ./widget --json
|
|
262
293
|
tender app context refresh artifact_123 --dir ./widget --dry-run --json
|
|
@@ -294,8 +325,8 @@ Commands:
|
|
|
294
325
|
tender app analytics charts list <app-id> List saved charts in one dashboard
|
|
295
326
|
tender app analytics charts update <app-id> Update a saved chart
|
|
296
327
|
tender app analytics charts delete <app-id> Delete a saved chart
|
|
297
|
-
tender app analytics
|
|
298
|
-
tender app analytics
|
|
328
|
+
tender app analytics properties list <app-id> List queryable custom event properties
|
|
329
|
+
tender app analytics properties activate <app-id> Make a property queryable
|
|
299
330
|
|
|
300
331
|
Examples:
|
|
301
332
|
tender app analytics summary artifact_123 --range 30d --json
|
|
@@ -311,8 +342,8 @@ Examples:
|
|
|
311
342
|
tender app analytics charts list artifact_123 --dashboard analytics_dashboard_123 --json
|
|
312
343
|
tender app analytics charts update artifact_123 --chart analytics_chart_123 --title "Signup funnel" --spec funnel.json --dry-run --json
|
|
313
344
|
tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
|
|
314
|
-
tender app analytics
|
|
315
|
-
tender app analytics
|
|
345
|
+
tender app analytics properties list artifact_123 --json
|
|
346
|
+
tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json`;
|
|
316
347
|
}
|
|
317
348
|
function analyticsSummaryHelp() {
|
|
318
349
|
return `Usage: tender app analytics summary <app-id> [--range <7d|30d|90d|YYYY-MM-DD..YYYY-MM-DD>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
@@ -388,7 +419,7 @@ Examples:
|
|
|
388
419
|
function analyticsChartsCreateHelp() {
|
|
389
420
|
return `Usage: tender app analytics charts create <app-id> --dashboard <dashboard-id> --title <title> --spec <file|-> [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
390
421
|
|
|
391
|
-
Property charts automatically
|
|
422
|
+
Property charts automatically make required custom event properties queryable before
|
|
392
423
|
saving when the spec has an event_name = filter.
|
|
393
424
|
|
|
394
425
|
Examples:
|
|
@@ -406,7 +437,7 @@ function analyticsChartsUpdateHelp() {
|
|
|
406
437
|
|
|
407
438
|
At least one of --title or --spec is required. When one is omitted, the CLI reads
|
|
408
439
|
the existing saved chart and keeps that value.
|
|
409
|
-
Property charts automatically
|
|
440
|
+
Property charts automatically make required custom event properties queryable before
|
|
410
441
|
saving when the spec has an event_name = filter.
|
|
411
442
|
|
|
412
443
|
Examples:
|
|
@@ -421,19 +452,70 @@ Examples:
|
|
|
421
452
|
tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
|
|
422
453
|
tender app analytics charts delete artifact_123 --chart analytics_chart_123 --json`;
|
|
423
454
|
}
|
|
424
|
-
function
|
|
425
|
-
return `Usage: tender app analytics
|
|
455
|
+
function analyticsPropertiesListHelp() {
|
|
456
|
+
return `Usage: tender app analytics properties list <app-id> [--event <event-name>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
457
|
+
|
|
458
|
+
Examples:
|
|
459
|
+
tender app analytics properties list artifact_123 --json
|
|
460
|
+
tender app analytics properties list artifact_123 --event signup_started --json`;
|
|
461
|
+
}
|
|
462
|
+
function analyticsPropertiesActivateHelp() {
|
|
463
|
+
return `Usage: tender app analytics properties activate <app-id> --event <event-name> --property <property-key> --type <dimension|metric> [--label <label>] [--dry-run] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
464
|
+
|
|
465
|
+
Examples:
|
|
466
|
+
tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
|
|
467
|
+
tender app analytics properties activate artifact_123 --event score_saved --property score --type metric --json`;
|
|
468
|
+
}
|
|
469
|
+
function dbHelp() {
|
|
470
|
+
return `Usage: tender app db <command> <app-id> [options]
|
|
471
|
+
|
|
472
|
+
Commands:
|
|
473
|
+
tender app db capabilities <app-id> Describe app database policy and limits
|
|
474
|
+
tender app db tables <app-id> List app database tables and views
|
|
475
|
+
tender app db schema <app-id> Show schema for one table
|
|
476
|
+
tender app db query <app-id> Run one read-only SQL query
|
|
477
|
+
|
|
478
|
+
Examples:
|
|
479
|
+
tender app db capabilities artifact_123 --json
|
|
480
|
+
tender app db tables artifact_123 --json
|
|
481
|
+
tender app db schema artifact_123 --table votes --json
|
|
482
|
+
tender app db query artifact_123 --sql "select * from votes limit 20" --json
|
|
483
|
+
tender app db query artifact_123 --file ./debug.sql --param active --limit 50 --json`;
|
|
484
|
+
}
|
|
485
|
+
function dbCapabilitiesHelp() {
|
|
486
|
+
return `Usage: tender app db capabilities <app-id> [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
487
|
+
|
|
488
|
+
Examples:
|
|
489
|
+
tender app db capabilities artifact_123 --json`;
|
|
490
|
+
}
|
|
491
|
+
function dbTablesHelp() {
|
|
492
|
+
return `Usage: tender app db tables <app-id> [--env preview] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
493
|
+
|
|
494
|
+
Examples:
|
|
495
|
+
tender app db tables artifact_123 --json
|
|
496
|
+
tender app db tables artifact_123 --env preview --json`;
|
|
497
|
+
}
|
|
498
|
+
function dbSchemaHelp() {
|
|
499
|
+
return `Usage: tender app db schema <app-id> --table <name> [--env preview] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
426
500
|
|
|
427
501
|
Examples:
|
|
428
|
-
tender app
|
|
429
|
-
tender app
|
|
502
|
+
tender app db schema artifact_123 --table votes --json
|
|
503
|
+
tender app db schema artifact_123 --table preference_votes --env preview --json`;
|
|
430
504
|
}
|
|
431
|
-
function
|
|
432
|
-
return `Usage: tender app
|
|
505
|
+
function dbQueryHelp() {
|
|
506
|
+
return `Usage: tender app db query <app-id> (--sql <sql> | --file <path|->) [--param <value>] [--limit <n>] [--env preview] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
507
|
+
|
|
508
|
+
Options:
|
|
509
|
+
--sql <sql> SQL statement. Must be one read-only statement.
|
|
510
|
+
--file <path|-> Read SQL from a file or stdin with -.
|
|
511
|
+
--param <value> Bind one positional parameter. Repeat for multiple params. Values parse as JSON scalars, falling back to string.
|
|
512
|
+
--limit <n> Interactive row limit. Server caps the maximum.
|
|
433
513
|
|
|
434
514
|
Examples:
|
|
435
|
-
tender app
|
|
436
|
-
tender app
|
|
515
|
+
tender app db query artifact_123 --sql "select * from votes limit 20" --json
|
|
516
|
+
tender app db query artifact_123 --sql "select * from votes where choice = ?" --param coffee --json
|
|
517
|
+
tender app db query artifact_123 --file ./debug.sql --limit 50 --json
|
|
518
|
+
tender app db query artifact_123 --file - --json < debug.sql`;
|
|
437
519
|
}
|
|
438
520
|
function artifactsListHelp() {
|
|
439
521
|
return `Usage: tender app list [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
@@ -767,6 +849,7 @@ function tenderCliCapabilities() {
|
|
|
767
849
|
"tender auth --help",
|
|
768
850
|
"tender app --help",
|
|
769
851
|
"tender app analytics --help",
|
|
852
|
+
"tender app db --help",
|
|
770
853
|
"tender app bindings outbound --help",
|
|
771
854
|
],
|
|
772
855
|
},
|
|
@@ -816,8 +899,8 @@ function tenderCliCapabilities() {
|
|
|
816
899
|
"tender app analytics charts templates --json",
|
|
817
900
|
"tender app analytics query <artifact-id> --spec chart.json --json",
|
|
818
901
|
"tender app analytics export query <artifact-id> --spec chart.json --format csv",
|
|
819
|
-
"tender app analytics
|
|
820
|
-
"tender app analytics
|
|
902
|
+
"tender app analytics properties list <artifact-id> --json",
|
|
903
|
+
"tender app analytics properties activate <artifact-id> --event <event-name> --property <property-key> --type dimension --dry-run --json",
|
|
821
904
|
"tender app analytics dashboards <artifact-id> --create \"Agent dashboard\" --dry-run --json",
|
|
822
905
|
"tender app analytics charts create <artifact-id> --dashboard <dashboard-id> --title <title> --spec chart.json --dry-run --json",
|
|
823
906
|
"tender app analytics charts list <artifact-id> --dashboard <dashboard-id> --json",
|
|
@@ -827,11 +910,28 @@ function tenderCliCapabilities() {
|
|
|
827
910
|
notes: [
|
|
828
911
|
"Combine CLI capabilities with local source inspection of analytics calls.",
|
|
829
912
|
"Validate chart specs with query before creating or updating saved charts.",
|
|
830
|
-
"Property chart create/update commands automatically
|
|
831
|
-
"Ad hoc query is read-only; if it reports a missing property
|
|
913
|
+
"Property chart create/update commands automatically make missing custom event properties queryable when the spec has an event_name = filter.",
|
|
914
|
+
"Ad hoc query is read-only; if it reports a missing dashboard-query property, activate the property first or save the chart through charts create/update.",
|
|
832
915
|
"Use list before writes so agents can avoid duplicates and explain the exact chart they are changing.",
|
|
833
916
|
],
|
|
834
917
|
},
|
|
918
|
+
{
|
|
919
|
+
name: "app_database_inspection",
|
|
920
|
+
when: "When inspecting app-local database tables from a scoped token.",
|
|
921
|
+
commands: [
|
|
922
|
+
"tender auth login --device --profile db-read --artifact <artifact-id> --ttl 7d --save-profile db --json",
|
|
923
|
+
"tender app db capabilities <artifact-id> --profile db --json",
|
|
924
|
+
"tender app db tables <artifact-id> --profile db --json",
|
|
925
|
+
"tender app db schema <artifact-id> --table <table-name> --profile db --json",
|
|
926
|
+
"tender app db query <artifact-id> --sql \"select * from <table-name> limit 20\" --profile db --json",
|
|
927
|
+
"tender app db query <artifact-id> --file ./debug.sql --profile db --json",
|
|
928
|
+
],
|
|
929
|
+
notes: [
|
|
930
|
+
"The CLI only accepts artifact ids. It never accepts tenant ids, runner names, provider account ids, or runtime storage identifiers.",
|
|
931
|
+
"Read-only policy is enforced by the server before the runtime shim executes SQL.",
|
|
932
|
+
"Use --file - to pipe SQL from stdin without prompts.",
|
|
933
|
+
],
|
|
934
|
+
},
|
|
835
935
|
{
|
|
836
936
|
name: "outbound_http_binding",
|
|
837
937
|
when: "When app code needs external APIs, OAuth/token exchange, webhooks, provider SDKs, or secrets.",
|
|
@@ -886,14 +986,14 @@ function tenderCliCapabilities() {
|
|
|
886
986
|
writes: false,
|
|
887
987
|
},
|
|
888
988
|
{
|
|
889
|
-
command: "tender app analytics
|
|
890
|
-
purpose: "List
|
|
989
|
+
command: "tender app analytics properties list <artifact-id> --json",
|
|
990
|
+
purpose: "List custom event properties available to dashboard queries.",
|
|
891
991
|
requiresAuth: true,
|
|
892
992
|
writes: false,
|
|
893
993
|
},
|
|
894
994
|
{
|
|
895
|
-
command: "tender app analytics
|
|
896
|
-
purpose: "Preview or activate the
|
|
995
|
+
command: "tender app analytics properties activate <artifact-id> --event <event-name> --property <property-key> --type <dimension|metric> --dry-run --json",
|
|
996
|
+
purpose: "Preview or activate the custom event property needed for a property group-by or property metric chart.",
|
|
897
997
|
requiresAuth: true,
|
|
898
998
|
writes: true,
|
|
899
999
|
dryRun: true,
|
|
@@ -925,6 +1025,30 @@ function tenderCliCapabilities() {
|
|
|
925
1025
|
writes: true,
|
|
926
1026
|
dryRun: true,
|
|
927
1027
|
},
|
|
1028
|
+
{
|
|
1029
|
+
command: "tender app db capabilities <artifact-id> --json",
|
|
1030
|
+
purpose: "Inspect app database policy, limits, and supported capabilities for one artifact.",
|
|
1031
|
+
requiresAuth: true,
|
|
1032
|
+
writes: false,
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
command: "tender app db tables <artifact-id> --json",
|
|
1036
|
+
purpose: "List app database tables and views for one artifact.",
|
|
1037
|
+
requiresAuth: true,
|
|
1038
|
+
writes: false,
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
command: "tender app db schema <artifact-id> --table <table-name> --json",
|
|
1042
|
+
purpose: "Inspect table, index, and trigger schema for one app database table.",
|
|
1043
|
+
requiresAuth: true,
|
|
1044
|
+
writes: false,
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
command: "tender app db query <artifact-id> --sql \"select * from <table-name> limit 20\" --json",
|
|
1048
|
+
purpose: "Run one bounded read-only app database query.",
|
|
1049
|
+
requiresAuth: true,
|
|
1050
|
+
writes: false,
|
|
1051
|
+
},
|
|
928
1052
|
],
|
|
929
1053
|
security: [
|
|
930
1054
|
"Prefer artifact-scoped device tokens.",
|
|
@@ -932,6 +1056,7 @@ function tenderCliCapabilities() {
|
|
|
932
1056
|
"Do not call Tender HTTP APIs directly from agent workflows.",
|
|
933
1057
|
"Do not put third-party fetch calls in src/server.ts; use outbound bindings.",
|
|
934
1058
|
"Do not request direct query credentials or raw SQL for analytics.",
|
|
1059
|
+
"Do not use tenant ids, runner names, provider account ids, or runtime storage identifiers for app database inspection.",
|
|
935
1060
|
],
|
|
936
1061
|
};
|
|
937
1062
|
}
|
|
@@ -2264,10 +2389,11 @@ function parseContextRefreshArgs(args) {
|
|
|
2264
2389
|
function parseTokenProfile(value) {
|
|
2265
2390
|
if (value === "edit-preview" ||
|
|
2266
2391
|
value === "publish" ||
|
|
2267
|
-
value === "create"
|
|
2392
|
+
value === "create" ||
|
|
2393
|
+
value === "db-read") {
|
|
2268
2394
|
return value;
|
|
2269
2395
|
}
|
|
2270
|
-
throw new TenderCliUsageError("--profile must be one of edit-preview, publish, or
|
|
2396
|
+
throw new TenderCliUsageError("--profile must be one of edit-preview, publish, create, or db-read.");
|
|
2271
2397
|
}
|
|
2272
2398
|
function parsePositiveIntegerFlag(value, flag) {
|
|
2273
2399
|
const parsed = Number(value);
|
|
@@ -2341,6 +2467,9 @@ function parseAuthLoginArgs(args) {
|
|
|
2341
2467
|
if (tokenProfile === "create" && artifactId) {
|
|
2342
2468
|
throw new TenderCliUsageError("--profile create must be account-scoped; remove --artifact.");
|
|
2343
2469
|
}
|
|
2470
|
+
if (tokenProfile === "db-read" && !artifactId) {
|
|
2471
|
+
throw new TenderCliUsageError("--profile db-read requires --artifact <artifact-id>.");
|
|
2472
|
+
}
|
|
2344
2473
|
return {
|
|
2345
2474
|
command: "auth login",
|
|
2346
2475
|
baseUrl,
|
|
@@ -2938,13 +3067,15 @@ function analyticsChartTemplates() {
|
|
|
2938
3067
|
id: "property_breakdown",
|
|
2939
3068
|
title: "Property breakdown",
|
|
2940
3069
|
visualization: "bar",
|
|
2941
|
-
purpose: "Compare a
|
|
3070
|
+
purpose: "Compare a queryable dimension property observed in events.",
|
|
2942
3071
|
usableAsIs: false,
|
|
2943
3072
|
placeholders: {
|
|
2944
|
-
|
|
3073
|
+
eventName: "<event_name>",
|
|
3074
|
+
property: "<queryable_dimension_property>",
|
|
2945
3075
|
},
|
|
2946
3076
|
adaptWith: [
|
|
2947
|
-
"Use a property key
|
|
3077
|
+
"Use a queryable property key from capabilities chartSpec.groupBy, such as property:plan.",
|
|
3078
|
+
"Choose the event that owns the property and keep the event_name filter.",
|
|
2948
3079
|
"Choose stable categorical properties with enough observations to compare.",
|
|
2949
3080
|
],
|
|
2950
3081
|
spec: {
|
|
@@ -2952,8 +3083,8 @@ function analyticsChartTemplates() {
|
|
|
2952
3083
|
dataset: "events",
|
|
2953
3084
|
visualization: "bar",
|
|
2954
3085
|
metric: { name: "custom_event_count" },
|
|
2955
|
-
groupBy: [{ field: "property:<
|
|
2956
|
-
filters: [],
|
|
3086
|
+
groupBy: [{ field: "property:<queryable_dimension_property>" }],
|
|
3087
|
+
filters: [{ field: "event_name", op: "=", value: "<event_name>" }],
|
|
2957
3088
|
range: { preset: "30d" },
|
|
2958
3089
|
sort: [{ field: "value", direction: "desc" }],
|
|
2959
3090
|
limit: 10,
|
|
@@ -2966,19 +3097,21 @@ function analyticsChartTemplates() {
|
|
|
2966
3097
|
purpose: "Track an observed numeric event property over time.",
|
|
2967
3098
|
usableAsIs: false,
|
|
2968
3099
|
placeholders: {
|
|
2969
|
-
|
|
3100
|
+
eventName: "<event_name>",
|
|
3101
|
+
metric: "avg:<queryable_metric_property>",
|
|
2970
3102
|
},
|
|
2971
3103
|
adaptWith: [
|
|
2972
|
-
"Use a numeric property key
|
|
3104
|
+
"Use a queryable numeric property key from capabilities chartSpec.metrics.",
|
|
3105
|
+
"Choose the event that owns the property and keep the event_name filter.",
|
|
2973
3106
|
"Prefer averages for scores, durations, quantities, or ordinal values.",
|
|
2974
3107
|
],
|
|
2975
3108
|
spec: {
|
|
2976
3109
|
version: 1,
|
|
2977
3110
|
dataset: "events",
|
|
2978
3111
|
visualization: "line",
|
|
2979
|
-
metric: { name: "avg:<
|
|
3112
|
+
metric: { name: "avg:<queryable_metric_property>" },
|
|
2980
3113
|
groupBy: [{ field: "day" }],
|
|
2981
|
-
filters: [],
|
|
3114
|
+
filters: [{ field: "event_name", op: "=", value: "<event_name>" }],
|
|
2982
3115
|
range: { preset: "30d" },
|
|
2983
3116
|
sort: [{ field: "label", direction: "asc" }],
|
|
2984
3117
|
limit: 30,
|
|
@@ -3019,7 +3152,7 @@ function runAnalyticsChartsTemplates(command, io) {
|
|
|
3019
3152
|
templates,
|
|
3020
3153
|
next: [
|
|
3021
3154
|
"Run tender app analytics capabilities <artifact-id> --include-catalog --range 30d --json.",
|
|
3022
|
-
"Choose a template and replace placeholders with observed events, paths, or
|
|
3155
|
+
"Choose a template and replace placeholders with observed events, paths, or queryable property keys.",
|
|
3023
3156
|
"Validate with tender app analytics query <artifact-id> --spec chart.json --json.",
|
|
3024
3157
|
"Preview saved writes with tender app analytics dashboards ... --dry-run and charts create ... --dry-run.",
|
|
3025
3158
|
],
|
|
@@ -3206,13 +3339,13 @@ function parseAnalyticsChartsDeleteArgs(args) {
|
|
|
3206
3339
|
...parsed,
|
|
3207
3340
|
};
|
|
3208
3341
|
}
|
|
3209
|
-
function
|
|
3342
|
+
function parseAnalyticsPropertiesListArgs(args) {
|
|
3210
3343
|
if (args.includes("--help") || args.includes("-h")) {
|
|
3211
|
-
return { command: "help", topic: "app analytics
|
|
3344
|
+
return { command: "help", topic: "app analytics properties list" };
|
|
3212
3345
|
}
|
|
3213
3346
|
const artifactId = args[0];
|
|
3214
3347
|
if (!artifactId || artifactId.startsWith("-")) {
|
|
3215
|
-
throw new TenderCliUsageError("app id is required. Example: tender app analytics
|
|
3348
|
+
throw new TenderCliUsageError("app id is required. Example: tender app analytics properties list <app-id> --json");
|
|
3216
3349
|
}
|
|
3217
3350
|
const parsed = parseAnalyticsCommonArgs(args, 1);
|
|
3218
3351
|
let eventName = null;
|
|
@@ -3223,28 +3356,27 @@ function parseAnalyticsPropertySlotsListArgs(args) {
|
|
|
3223
3356
|
index += 1;
|
|
3224
3357
|
continue;
|
|
3225
3358
|
}
|
|
3226
|
-
throw new TenderCliUsageError(`Unknown analytics
|
|
3359
|
+
throw new TenderCliUsageError(`Unknown analytics properties list option: ${arg}`);
|
|
3227
3360
|
}
|
|
3228
3361
|
return {
|
|
3229
|
-
command: "app analytics
|
|
3362
|
+
command: "app analytics properties list",
|
|
3230
3363
|
artifactId,
|
|
3231
3364
|
eventName,
|
|
3232
3365
|
...parsed,
|
|
3233
3366
|
};
|
|
3234
3367
|
}
|
|
3235
|
-
function
|
|
3368
|
+
function parseAnalyticsPropertiesActivateArgs(args) {
|
|
3236
3369
|
if (args.includes("--help") || args.includes("-h")) {
|
|
3237
|
-
return { command: "help", topic: "app analytics
|
|
3370
|
+
return { command: "help", topic: "app analytics properties activate" };
|
|
3238
3371
|
}
|
|
3239
3372
|
const artifactId = args[0];
|
|
3240
3373
|
if (!artifactId || artifactId.startsWith("-")) {
|
|
3241
|
-
throw new TenderCliUsageError("app id is required. Example: tender app analytics
|
|
3374
|
+
throw new TenderCliUsageError("app id is required. Example: tender app analytics properties activate <app-id> --event <event-name> --property <property-key> --type dimension --json");
|
|
3242
3375
|
}
|
|
3243
3376
|
const parsed = parseAnalyticsCommonArgs(args, 1);
|
|
3244
3377
|
let eventName = null;
|
|
3245
3378
|
let propertyKey = null;
|
|
3246
|
-
let
|
|
3247
|
-
let slotName = null;
|
|
3379
|
+
let propertyType = null;
|
|
3248
3380
|
let label = null;
|
|
3249
3381
|
let dryRun = false;
|
|
3250
3382
|
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
@@ -3264,12 +3396,7 @@ function parseAnalyticsPropertySlotsActivateArgs(args) {
|
|
|
3264
3396
|
if (value !== "dimension" && value !== "metric") {
|
|
3265
3397
|
throw new TenderCliUsageError("--type must be dimension or metric.");
|
|
3266
3398
|
}
|
|
3267
|
-
|
|
3268
|
-
index += 1;
|
|
3269
|
-
continue;
|
|
3270
|
-
}
|
|
3271
|
-
if (arg === "--slot") {
|
|
3272
|
-
slotName = parseRequiredFlagValue(parsed.remaining[index + 1], "--slot");
|
|
3399
|
+
propertyType = value;
|
|
3273
3400
|
index += 1;
|
|
3274
3401
|
continue;
|
|
3275
3402
|
}
|
|
@@ -3282,23 +3409,227 @@ function parseAnalyticsPropertySlotsActivateArgs(args) {
|
|
|
3282
3409
|
dryRun = true;
|
|
3283
3410
|
continue;
|
|
3284
3411
|
}
|
|
3285
|
-
throw new TenderCliUsageError(`Unknown analytics
|
|
3412
|
+
throw new TenderCliUsageError(`Unknown analytics properties activate option: ${arg}`);
|
|
3286
3413
|
}
|
|
3287
|
-
if (!eventName || !propertyKey || !
|
|
3288
|
-
throw new TenderCliUsageError("--event, --property, and --type are required. Example: tender app analytics
|
|
3414
|
+
if (!eventName || !propertyKey || !propertyType) {
|
|
3415
|
+
throw new TenderCliUsageError("--event, --property, and --type are required. Example: tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --json");
|
|
3289
3416
|
}
|
|
3290
3417
|
return {
|
|
3291
|
-
command: "app analytics
|
|
3418
|
+
command: "app analytics properties activate",
|
|
3292
3419
|
artifactId,
|
|
3293
3420
|
eventName,
|
|
3294
3421
|
propertyKey,
|
|
3295
|
-
|
|
3296
|
-
slotName,
|
|
3422
|
+
propertyType,
|
|
3297
3423
|
label,
|
|
3298
3424
|
dryRun,
|
|
3299
3425
|
...parsed,
|
|
3300
3426
|
};
|
|
3301
3427
|
}
|
|
3428
|
+
function parseDbCommonArgs(args, startIndex) {
|
|
3429
|
+
let baseUrl = null;
|
|
3430
|
+
let flagToken = null;
|
|
3431
|
+
let profileName = null;
|
|
3432
|
+
let json = false;
|
|
3433
|
+
const remaining = [];
|
|
3434
|
+
for (let index = startIndex; index < args.length; index += 1) {
|
|
3435
|
+
const arg = args[index];
|
|
3436
|
+
if (arg === "--json") {
|
|
3437
|
+
json = true;
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
if (arg === "--base-url") {
|
|
3441
|
+
baseUrl = parseBaseUrlFlag(args, index);
|
|
3442
|
+
index += 1;
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
3445
|
+
if (arg === "--token") {
|
|
3446
|
+
flagToken = parseTokenFlag(args, index);
|
|
3447
|
+
index += 1;
|
|
3448
|
+
continue;
|
|
3449
|
+
}
|
|
3450
|
+
if (arg === "--profile") {
|
|
3451
|
+
profileName = parseProfileName(args[index + 1], "--profile");
|
|
3452
|
+
index += 1;
|
|
3453
|
+
continue;
|
|
3454
|
+
}
|
|
3455
|
+
remaining.push(arg);
|
|
3456
|
+
}
|
|
3457
|
+
return { baseUrl, flagToken, profileName, json, remaining };
|
|
3458
|
+
}
|
|
3459
|
+
function parseDbEnvironment(value) {
|
|
3460
|
+
if (value === undefined || value === "preview") {
|
|
3461
|
+
return "preview";
|
|
3462
|
+
}
|
|
3463
|
+
throw new TenderCliUsageError("--env must be preview.");
|
|
3464
|
+
}
|
|
3465
|
+
function parseDbFlagValue(value, flag) {
|
|
3466
|
+
if (!value || value.startsWith("--")) {
|
|
3467
|
+
throw new TenderCliUsageError(`${flag} requires a value.`);
|
|
3468
|
+
}
|
|
3469
|
+
return value;
|
|
3470
|
+
}
|
|
3471
|
+
function parseDbFilePath(value) {
|
|
3472
|
+
if (!value || (value !== "-" && value.startsWith("-"))) {
|
|
3473
|
+
throw new TenderCliUsageError("--file requires a SQL file path or - for stdin.");
|
|
3474
|
+
}
|
|
3475
|
+
return value;
|
|
3476
|
+
}
|
|
3477
|
+
function parseDbParameter(value) {
|
|
3478
|
+
const raw = parseDbFlagValue(value, "--param");
|
|
3479
|
+
try {
|
|
3480
|
+
const parsed = JSON.parse(raw);
|
|
3481
|
+
if (parsed === null ||
|
|
3482
|
+
typeof parsed === "string" ||
|
|
3483
|
+
typeof parsed === "number") {
|
|
3484
|
+
return parsed;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
catch {
|
|
3488
|
+
// Fall back to a string binding.
|
|
3489
|
+
}
|
|
3490
|
+
return raw;
|
|
3491
|
+
}
|
|
3492
|
+
function parseDbCapabilitiesArgs(args) {
|
|
3493
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3494
|
+
return { command: "help", topic: "app db capabilities" };
|
|
3495
|
+
}
|
|
3496
|
+
const artifactId = args[0];
|
|
3497
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3498
|
+
throw new TenderCliUsageError("app id is required. Example: tender app db capabilities <app-id> --json");
|
|
3499
|
+
}
|
|
3500
|
+
const parsed = parseDbCommonArgs(args, 1);
|
|
3501
|
+
if (parsed.remaining.length > 0) {
|
|
3502
|
+
throw new TenderCliUsageError(`Unknown app db capabilities option: ${parsed.remaining[0]}`);
|
|
3503
|
+
}
|
|
3504
|
+
return { command: "app db capabilities", artifactId, ...parsed };
|
|
3505
|
+
}
|
|
3506
|
+
function parseDbTablesArgs(args) {
|
|
3507
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3508
|
+
return { command: "help", topic: "app db tables" };
|
|
3509
|
+
}
|
|
3510
|
+
const artifactId = args[0];
|
|
3511
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3512
|
+
throw new TenderCliUsageError("app id is required. Example: tender app db tables <app-id> --json");
|
|
3513
|
+
}
|
|
3514
|
+
const parsed = parseDbCommonArgs(args, 1);
|
|
3515
|
+
let environment = "preview";
|
|
3516
|
+
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3517
|
+
const arg = parsed.remaining[index];
|
|
3518
|
+
if (arg === "--env") {
|
|
3519
|
+
environment = parseDbEnvironment(parsed.remaining[index + 1]);
|
|
3520
|
+
index += 1;
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
throw new TenderCliUsageError(`Unknown app db tables option: ${arg}`);
|
|
3524
|
+
}
|
|
3525
|
+
return { command: "app db tables", artifactId, environment, ...parsed };
|
|
3526
|
+
}
|
|
3527
|
+
function parseDbSchemaArgs(args) {
|
|
3528
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3529
|
+
return { command: "help", topic: "app db schema" };
|
|
3530
|
+
}
|
|
3531
|
+
const artifactId = args[0];
|
|
3532
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3533
|
+
throw new TenderCliUsageError("app id is required. Example: tender app db schema <app-id> --table votes --json");
|
|
3534
|
+
}
|
|
3535
|
+
const parsed = parseDbCommonArgs(args, 1);
|
|
3536
|
+
let environment = "preview";
|
|
3537
|
+
let tableName = null;
|
|
3538
|
+
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3539
|
+
const arg = parsed.remaining[index];
|
|
3540
|
+
if (arg === "--env") {
|
|
3541
|
+
environment = parseDbEnvironment(parsed.remaining[index + 1]);
|
|
3542
|
+
index += 1;
|
|
3543
|
+
continue;
|
|
3544
|
+
}
|
|
3545
|
+
if (arg === "--table") {
|
|
3546
|
+
tableName = parseRequiredFlagValue(parsed.remaining[index + 1], "--table");
|
|
3547
|
+
index += 1;
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3550
|
+
throw new TenderCliUsageError(`Unknown app db schema option: ${arg}`);
|
|
3551
|
+
}
|
|
3552
|
+
if (!tableName) {
|
|
3553
|
+
throw new TenderCliUsageError("--table is required. Example: tender app db schema artifact_123 --table votes --json");
|
|
3554
|
+
}
|
|
3555
|
+
return {
|
|
3556
|
+
command: "app db schema",
|
|
3557
|
+
artifactId,
|
|
3558
|
+
environment,
|
|
3559
|
+
tableName,
|
|
3560
|
+
...parsed,
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
function parseDbQueryArgs(args) {
|
|
3564
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3565
|
+
return { command: "help", topic: "app db query" };
|
|
3566
|
+
}
|
|
3567
|
+
const artifactId = args[0];
|
|
3568
|
+
if (!artifactId || artifactId.startsWith("-")) {
|
|
3569
|
+
throw new TenderCliUsageError("app id is required. Example: tender app db query <app-id> --sql \"select * from votes limit 20\" --json");
|
|
3570
|
+
}
|
|
3571
|
+
const parsed = parseDbCommonArgs(args, 1);
|
|
3572
|
+
let environment = "preview";
|
|
3573
|
+
let sqlSource = null;
|
|
3574
|
+
const params = [];
|
|
3575
|
+
let limit = null;
|
|
3576
|
+
for (let index = 0; index < parsed.remaining.length; index += 1) {
|
|
3577
|
+
const arg = parsed.remaining[index];
|
|
3578
|
+
if (arg === "--env") {
|
|
3579
|
+
environment = parseDbEnvironment(parsed.remaining[index + 1]);
|
|
3580
|
+
index += 1;
|
|
3581
|
+
continue;
|
|
3582
|
+
}
|
|
3583
|
+
if (arg === "--sql") {
|
|
3584
|
+
if (sqlSource) {
|
|
3585
|
+
throw new TenderCliUsageError("--sql and --file cannot be repeated or combined.");
|
|
3586
|
+
}
|
|
3587
|
+
sqlSource = {
|
|
3588
|
+
kind: "inline",
|
|
3589
|
+
value: parseDbFlagValue(parsed.remaining[index + 1], "--sql"),
|
|
3590
|
+
};
|
|
3591
|
+
index += 1;
|
|
3592
|
+
continue;
|
|
3593
|
+
}
|
|
3594
|
+
if (arg === "--file") {
|
|
3595
|
+
if (sqlSource) {
|
|
3596
|
+
throw new TenderCliUsageError("--sql and --file cannot be repeated or combined.");
|
|
3597
|
+
}
|
|
3598
|
+
sqlSource = {
|
|
3599
|
+
kind: "file",
|
|
3600
|
+
value: parseDbFilePath(parsed.remaining[index + 1]),
|
|
3601
|
+
};
|
|
3602
|
+
index += 1;
|
|
3603
|
+
continue;
|
|
3604
|
+
}
|
|
3605
|
+
if (arg === "--param") {
|
|
3606
|
+
params.push(parseDbParameter(parsed.remaining[index + 1]));
|
|
3607
|
+
index += 1;
|
|
3608
|
+
continue;
|
|
3609
|
+
}
|
|
3610
|
+
if (arg === "--limit") {
|
|
3611
|
+
limit = parsePositiveIntegerFlag(parsed.remaining[index + 1], "--limit");
|
|
3612
|
+
index += 1;
|
|
3613
|
+
continue;
|
|
3614
|
+
}
|
|
3615
|
+
throw new TenderCliUsageError(`Unknown app db query option: ${arg}`);
|
|
3616
|
+
}
|
|
3617
|
+
if (!sqlSource) {
|
|
3618
|
+
throw new TenderCliUsageError("--sql or --file is required. Example: tender app db query artifact_123 --sql \"select * from votes limit 20\" --json");
|
|
3619
|
+
}
|
|
3620
|
+
if (params.length > DB_MAX_BOUND_PARAMETERS) {
|
|
3621
|
+
throw new TenderCliUsageError(`App database queries are limited to ${DB_MAX_BOUND_PARAMETERS} bound parameters.`);
|
|
3622
|
+
}
|
|
3623
|
+
return {
|
|
3624
|
+
command: "app db query",
|
|
3625
|
+
artifactId,
|
|
3626
|
+
environment,
|
|
3627
|
+
sqlSource,
|
|
3628
|
+
params,
|
|
3629
|
+
limit,
|
|
3630
|
+
...parsed,
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3302
3633
|
function parseTenderArgs(args) {
|
|
3303
3634
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
3304
3635
|
return { command: "help", topic: "root" };
|
|
@@ -3383,21 +3714,21 @@ function parseTenderArgs(args) {
|
|
|
3383
3714
|
return parseAnalyticsDashboardsArgs(resourceArgs.slice(3));
|
|
3384
3715
|
}
|
|
3385
3716
|
if (resourceArgs[1] === "analytics" &&
|
|
3386
|
-
resourceArgs[2] === "
|
|
3717
|
+
resourceArgs[2] === "properties" &&
|
|
3387
3718
|
(resourceArgs[3] === undefined ||
|
|
3388
3719
|
resourceArgs[3] === "--help" ||
|
|
3389
3720
|
resourceArgs[3] === "-h")) {
|
|
3390
|
-
return { command: "help", topic: "app analytics
|
|
3721
|
+
return { command: "help", topic: "app analytics properties list" };
|
|
3391
3722
|
}
|
|
3392
3723
|
if (resourceArgs[1] === "analytics" &&
|
|
3393
|
-
resourceArgs[2] === "
|
|
3724
|
+
resourceArgs[2] === "properties" &&
|
|
3394
3725
|
resourceArgs[3] === "list") {
|
|
3395
|
-
return
|
|
3726
|
+
return parseAnalyticsPropertiesListArgs(resourceArgs.slice(4));
|
|
3396
3727
|
}
|
|
3397
3728
|
if (resourceArgs[1] === "analytics" &&
|
|
3398
|
-
resourceArgs[2] === "
|
|
3729
|
+
resourceArgs[2] === "properties" &&
|
|
3399
3730
|
resourceArgs[3] === "activate") {
|
|
3400
|
-
return
|
|
3731
|
+
return parseAnalyticsPropertiesActivateArgs(resourceArgs.slice(4));
|
|
3401
3732
|
}
|
|
3402
3733
|
if (resourceArgs[1] === "analytics" &&
|
|
3403
3734
|
resourceArgs[2] === "export" &&
|
|
@@ -3448,6 +3779,27 @@ function parseTenderArgs(args) {
|
|
|
3448
3779
|
resourceArgs[3] === "delete") {
|
|
3449
3780
|
return parseAnalyticsChartsDeleteArgs(resourceArgs.slice(4));
|
|
3450
3781
|
}
|
|
3782
|
+
if (resourceArgs[1] === "db" &&
|
|
3783
|
+
(resourceArgs[2] === undefined ||
|
|
3784
|
+
resourceArgs[2] === "--help" ||
|
|
3785
|
+
resourceArgs[2] === "-h")) {
|
|
3786
|
+
return { command: "help", topic: "app db" };
|
|
3787
|
+
}
|
|
3788
|
+
if (resourceArgs[1] === "db" && resourceArgs[2] === "capabilities") {
|
|
3789
|
+
return parseDbCapabilitiesArgs(resourceArgs.slice(3));
|
|
3790
|
+
}
|
|
3791
|
+
if (resourceArgs[1] === "db" && resourceArgs[2] === "tables") {
|
|
3792
|
+
return parseDbTablesArgs(resourceArgs.slice(3));
|
|
3793
|
+
}
|
|
3794
|
+
if (resourceArgs[1] === "db" && resourceArgs[2] === "schema") {
|
|
3795
|
+
return parseDbSchemaArgs(resourceArgs.slice(3));
|
|
3796
|
+
}
|
|
3797
|
+
if (resourceArgs[1] === "db" && resourceArgs[2] === "query") {
|
|
3798
|
+
return parseDbQueryArgs(resourceArgs.slice(3));
|
|
3799
|
+
}
|
|
3800
|
+
if (resourceArgs[1] === "db") {
|
|
3801
|
+
throw new TenderCliUsageError(`Unknown app db command: ${resourceArgs[2]}`);
|
|
3802
|
+
}
|
|
3451
3803
|
if (resourceArgs[1] === "context" &&
|
|
3452
3804
|
(resourceArgs[2] === undefined ||
|
|
3453
3805
|
resourceArgs[2] === "--help" ||
|
|
@@ -3849,6 +4201,55 @@ async function requestAnalyticsApi(input) {
|
|
|
3849
4201
|
}
|
|
3850
4202
|
return payload;
|
|
3851
4203
|
}
|
|
4204
|
+
function appendDbEnvironment(params, environment) {
|
|
4205
|
+
params.set("env", environment);
|
|
4206
|
+
}
|
|
4207
|
+
async function requestDbApi(input) {
|
|
4208
|
+
const params = new URLSearchParams();
|
|
4209
|
+
if (input.environment) {
|
|
4210
|
+
appendDbEnvironment(params, input.environment);
|
|
4211
|
+
}
|
|
4212
|
+
if (input.tableName) {
|
|
4213
|
+
params.set("table", input.tableName);
|
|
4214
|
+
}
|
|
4215
|
+
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
4216
|
+
const response = await input.fetcher(`${input.baseUrl}/api/v1/artifacts/${encodeURIComponent(input.artifactId)}/db/${input.dbPath}${query}`, {
|
|
4217
|
+
method: input.method,
|
|
4218
|
+
headers: {
|
|
4219
|
+
authorization: `Bearer ${input.token}`,
|
|
4220
|
+
accept: "application/json",
|
|
4221
|
+
...(input.body === undefined ? {} : { "content-type": "application/json" }),
|
|
4222
|
+
},
|
|
4223
|
+
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
4224
|
+
});
|
|
4225
|
+
const payload = await parseJsonResponse(response);
|
|
4226
|
+
if (!response.ok || !payload?.ok) {
|
|
4227
|
+
const reasonCode = (typeof payload?.reasonCode === "string" && payload.reasonCode) ||
|
|
4228
|
+
`http_${response.status}`;
|
|
4229
|
+
const message = (typeof payload?.message === "string" && payload.message) ||
|
|
4230
|
+
(typeof payload?.reasonCode === "string" && payload.reasonCode) ||
|
|
4231
|
+
`App database request failed with HTTP ${response.status}.`;
|
|
4232
|
+
throw new TenderCliDbApiError([
|
|
4233
|
+
message,
|
|
4234
|
+
`Scope: artifact ${input.artifactId}, db/${input.dbPath}.`,
|
|
4235
|
+
`Reason: ${reasonCode}.`,
|
|
4236
|
+
`Example: ${dbCorrectedExample(input.artifactId, input.dbPath)}.`,
|
|
4237
|
+
].join("\n"), reasonCode);
|
|
4238
|
+
}
|
|
4239
|
+
return payload;
|
|
4240
|
+
}
|
|
4241
|
+
function dbCorrectedExample(artifactId, dbPath) {
|
|
4242
|
+
if (dbPath === "capabilities") {
|
|
4243
|
+
return `tender app db capabilities ${artifactId} --json`;
|
|
4244
|
+
}
|
|
4245
|
+
if (dbPath === "tables") {
|
|
4246
|
+
return `tender app db tables ${artifactId} --json`;
|
|
4247
|
+
}
|
|
4248
|
+
if (dbPath === "schema") {
|
|
4249
|
+
return `tender app db schema ${artifactId} --table <table-name> --json`;
|
|
4250
|
+
}
|
|
4251
|
+
return `tender app db query ${artifactId} --sql "select * from <table-name> limit 20" --json`;
|
|
4252
|
+
}
|
|
3852
4253
|
function analyticsCorrectedExample(artifactId, analyticsPath) {
|
|
3853
4254
|
if (analyticsPath === "capabilities") {
|
|
3854
4255
|
return `tender app analytics capabilities ${artifactId} --include-catalog --range 30d --json`;
|
|
@@ -3859,8 +4260,8 @@ function analyticsCorrectedExample(artifactId, analyticsPath) {
|
|
|
3859
4260
|
if (analyticsPath === "suggestions") {
|
|
3860
4261
|
return `tender app analytics suggestions ${artifactId} --range 30d --json`;
|
|
3861
4262
|
}
|
|
3862
|
-
if (analyticsPath === "
|
|
3863
|
-
return `tender app analytics
|
|
4263
|
+
if (analyticsPath === "properties") {
|
|
4264
|
+
return `tender app analytics properties list ${artifactId} --json`;
|
|
3864
4265
|
}
|
|
3865
4266
|
if (analyticsPath === "dashboards") {
|
|
3866
4267
|
return `tender app analytics dashboards ${artifactId} --json`;
|
|
@@ -3877,6 +4278,10 @@ function readCliStringField(record, field) {
|
|
|
3877
4278
|
const value = record[field];
|
|
3878
4279
|
return typeof value === "string" ? value : null;
|
|
3879
4280
|
}
|
|
4281
|
+
function readCliBooleanField(record, field) {
|
|
4282
|
+
const value = record[field];
|
|
4283
|
+
return typeof value === "boolean" ? value : false;
|
|
4284
|
+
}
|
|
3880
4285
|
function readAnalyticsDashboardRecords(payload) {
|
|
3881
4286
|
return Array.isArray(payload.dashboards)
|
|
3882
4287
|
? payload.dashboards.filter(isCliJsonRecord)
|
|
@@ -3912,9 +4317,9 @@ function readAnalyticsChartSpec(chart) {
|
|
|
3912
4317
|
function readAnalyticsChartLayout(chart) {
|
|
3913
4318
|
return isCliJsonRecord(chart.layout) ? chart.layout : {};
|
|
3914
4319
|
}
|
|
3915
|
-
function
|
|
3916
|
-
return Array.isArray(payload.
|
|
3917
|
-
? payload.
|
|
4320
|
+
function readAnalyticsPropertyRecords(payload) {
|
|
4321
|
+
return Array.isArray(payload.properties)
|
|
4322
|
+
? payload.properties.filter(isCliJsonRecord)
|
|
3918
4323
|
: [];
|
|
3919
4324
|
}
|
|
3920
4325
|
function readAnalyticsSpecEventNameFilter(spec) {
|
|
@@ -3940,34 +4345,34 @@ function readAnalyticsSpecGroupField(spec) {
|
|
|
3940
4345
|
const [group] = spec.groupBy.filter(isCliJsonRecord);
|
|
3941
4346
|
return group ? readCliStringField(group, "field") : null;
|
|
3942
4347
|
}
|
|
3943
|
-
function
|
|
4348
|
+
function readRequiredAnalyticsPropertiesFromSpec(spec) {
|
|
3944
4349
|
const eventName = readAnalyticsSpecEventNameFilter(spec);
|
|
3945
4350
|
const requirements = [];
|
|
3946
4351
|
const groupField = readAnalyticsSpecGroupField(spec);
|
|
3947
4352
|
if (groupField?.startsWith("property:")) {
|
|
3948
4353
|
if (!eventName) {
|
|
3949
|
-
throw new TenderCliUsageError("Property group-by chart specs require an event_name = filter so the CLI can
|
|
4354
|
+
throw new TenderCliUsageError("Property group-by chart specs require an event_name = filter so the CLI can make the property available to dashboard queries.");
|
|
3950
4355
|
}
|
|
3951
4356
|
requirements.push({
|
|
3952
4357
|
eventName,
|
|
3953
4358
|
propertyKey: groupField.slice("property:".length),
|
|
3954
|
-
|
|
4359
|
+
propertyType: "dimension",
|
|
3955
4360
|
});
|
|
3956
4361
|
}
|
|
3957
4362
|
const metricName = readAnalyticsSpecMetricName(spec);
|
|
3958
4363
|
if (metricName?.startsWith("sum:") || metricName?.startsWith("avg:")) {
|
|
3959
4364
|
if (!eventName) {
|
|
3960
|
-
throw new TenderCliUsageError("Property metric chart specs require an event_name = filter so the CLI can
|
|
4365
|
+
throw new TenderCliUsageError("Property metric chart specs require an event_name = filter so the CLI can make the property available to dashboard queries.");
|
|
3961
4366
|
}
|
|
3962
4367
|
requirements.push({
|
|
3963
4368
|
eventName,
|
|
3964
4369
|
propertyKey: metricName.slice(metricName.indexOf(":") + 1),
|
|
3965
|
-
|
|
4370
|
+
propertyType: "metric",
|
|
3966
4371
|
});
|
|
3967
4372
|
}
|
|
3968
4373
|
const seen = new Set();
|
|
3969
4374
|
return requirements.filter((requirement) => {
|
|
3970
|
-
const key = `${requirement.
|
|
4375
|
+
const key = `${requirement.propertyType}:${requirement.eventName}:${requirement.propertyKey}`;
|
|
3971
4376
|
if (seen.has(key)) {
|
|
3972
4377
|
return false;
|
|
3973
4378
|
}
|
|
@@ -3975,107 +4380,22 @@ function readRequiredAnalyticsPropertySlotsFromSpec(spec) {
|
|
|
3975
4380
|
return true;
|
|
3976
4381
|
});
|
|
3977
4382
|
}
|
|
3978
|
-
function
|
|
3979
|
-
return slotType === "dimension"
|
|
3980
|
-
? Array.from({ length: 13 }, (_, index) => `blob${index + 8}`)
|
|
3981
|
-
: Array.from({ length: 19 }, (_, index) => `double${index + 2}`);
|
|
3982
|
-
}
|
|
3983
|
-
function isValidAnalyticsPropertySlotName(input) {
|
|
3984
|
-
return analyticsPropertySlotNames(input.slotType).includes(input.slotName);
|
|
3985
|
-
}
|
|
3986
|
-
function findExistingAnalyticsPropertySlot(input) {
|
|
3987
|
-
return (input.propertySlots.find((slot) => readCliStringField(slot, "status") === "active" &&
|
|
3988
|
-
readCliStringField(slot, "eventName") === input.requirement.eventName &&
|
|
3989
|
-
readCliStringField(slot, "propertyKey") === input.requirement.propertyKey &&
|
|
3990
|
-
readCliStringField(slot, "slotType") === input.requirement.slotType) ?? null);
|
|
3991
|
-
}
|
|
3992
|
-
function chooseAnalyticsPropertySlotName(input) {
|
|
3993
|
-
if (input.requestedSlotName) {
|
|
3994
|
-
if (!isValidAnalyticsPropertySlotName({
|
|
3995
|
-
slotType: input.requirement.slotType,
|
|
3996
|
-
slotName: input.requestedSlotName,
|
|
3997
|
-
})) {
|
|
3998
|
-
throw new TenderCliUsageError(`--slot must be a valid ${input.requirement.slotType} analytics slot.`);
|
|
3999
|
-
}
|
|
4000
|
-
const conflictingSlot = input.propertySlots.find((slot) => readCliStringField(slot, "status") === "active" &&
|
|
4001
|
-
readCliStringField(slot, "eventName") === input.requirement.eventName &&
|
|
4002
|
-
readCliStringField(slot, "slotType") === input.requirement.slotType &&
|
|
4003
|
-
readCliStringField(slot, "slotName") === input.requestedSlotName &&
|
|
4004
|
-
readCliStringField(slot, "propertyKey") !== input.requirement.propertyKey);
|
|
4005
|
-
if (conflictingSlot) {
|
|
4006
|
-
throw new TenderCliUsageError(`--slot ${input.requestedSlotName} is already active for another ${input.requirement.slotType} property on ${input.requirement.eventName}.`);
|
|
4007
|
-
}
|
|
4008
|
-
return input.requestedSlotName;
|
|
4009
|
-
}
|
|
4010
|
-
const usedSlots = new Set(input.propertySlots
|
|
4011
|
-
.filter((slot) => readCliStringField(slot, "status") === "active" &&
|
|
4012
|
-
readCliStringField(slot, "eventName") === input.requirement.eventName &&
|
|
4013
|
-
readCliStringField(slot, "slotType") === input.requirement.slotType)
|
|
4014
|
-
.map((slot) => readCliStringField(slot, "slotName"))
|
|
4015
|
-
.filter((slotName) => Boolean(slotName)));
|
|
4016
|
-
const slotName = analyticsPropertySlotNames(input.requirement.slotType).find((candidate) => !usedSlots.has(candidate));
|
|
4017
|
-
if (!slotName) {
|
|
4018
|
-
throw new Error("analytics_property_slots_full");
|
|
4019
|
-
}
|
|
4020
|
-
return slotName;
|
|
4021
|
-
}
|
|
4022
|
-
async function fetchAnalyticsPropertySlots(input) {
|
|
4023
|
-
const payload = await requestAnalyticsApi({
|
|
4024
|
-
artifactId: input.command.artifactId,
|
|
4025
|
-
analyticsPath: "property-slots",
|
|
4026
|
-
method: "GET",
|
|
4027
|
-
baseUrl: input.credentials.baseUrl,
|
|
4028
|
-
token: input.credentials.token,
|
|
4029
|
-
fetcher: input.runtime.fetcher ?? fetch,
|
|
4030
|
-
});
|
|
4031
|
-
return { payload, propertySlots: readAnalyticsPropertySlotRecords(payload) };
|
|
4032
|
-
}
|
|
4033
|
-
async function ensureAnalyticsPropertySlots(input) {
|
|
4383
|
+
async function ensureAnalyticsProperties(input) {
|
|
4034
4384
|
if (input.requirements.length === 0) {
|
|
4035
4385
|
return [];
|
|
4036
4386
|
}
|
|
4037
|
-
const { propertySlots } = await fetchAnalyticsPropertySlots({
|
|
4038
|
-
command: input.command,
|
|
4039
|
-
credentials: input.credentials,
|
|
4040
|
-
runtime: input.runtime,
|
|
4041
|
-
});
|
|
4042
4387
|
const plan = [];
|
|
4043
4388
|
for (const requirement of input.requirements) {
|
|
4044
|
-
const existing = findExistingAnalyticsPropertySlot({
|
|
4045
|
-
propertySlots,
|
|
4046
|
-
requirement,
|
|
4047
|
-
});
|
|
4048
|
-
if (existing) {
|
|
4049
|
-
plan.push({
|
|
4050
|
-
...requirement,
|
|
4051
|
-
slotName: readCliStringField(existing, "slotName") ?? "",
|
|
4052
|
-
label: readCliStringField(existing, "label") ?? requirement.propertyKey,
|
|
4053
|
-
status: "existing",
|
|
4054
|
-
propertySlot: existing,
|
|
4055
|
-
});
|
|
4056
|
-
continue;
|
|
4057
|
-
}
|
|
4058
|
-
const slotName = chooseAnalyticsPropertySlotName({
|
|
4059
|
-
propertySlots,
|
|
4060
|
-
requirement,
|
|
4061
|
-
});
|
|
4062
4389
|
const body = {
|
|
4063
4390
|
eventName: requirement.eventName,
|
|
4064
4391
|
propertyKey: requirement.propertyKey,
|
|
4065
|
-
|
|
4066
|
-
slotName,
|
|
4392
|
+
propertyType: requirement.propertyType,
|
|
4067
4393
|
label: requirement.propertyKey,
|
|
4394
|
+
dryRun: input.dryRun,
|
|
4068
4395
|
};
|
|
4069
|
-
if (input.dryRun) {
|
|
4070
|
-
plan.push({
|
|
4071
|
-
...body,
|
|
4072
|
-
status: "planned",
|
|
4073
|
-
});
|
|
4074
|
-
continue;
|
|
4075
|
-
}
|
|
4076
4396
|
const payload = await requestAnalyticsApi({
|
|
4077
4397
|
artifactId: input.command.artifactId,
|
|
4078
|
-
analyticsPath: "
|
|
4398
|
+
analyticsPath: "properties",
|
|
4079
4399
|
method: "POST",
|
|
4080
4400
|
baseUrl: input.credentials.baseUrl,
|
|
4081
4401
|
token: input.credentials.token,
|
|
@@ -4083,17 +4403,16 @@ async function ensureAnalyticsPropertySlots(input) {
|
|
|
4083
4403
|
body,
|
|
4084
4404
|
});
|
|
4085
4405
|
plan.push({
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
status: "active",
|
|
4406
|
+
eventName: requirement.eventName,
|
|
4407
|
+
propertyKey: requirement.propertyKey,
|
|
4408
|
+
propertyType: requirement.propertyType,
|
|
4409
|
+
label: requirement.propertyKey,
|
|
4410
|
+
status: readCliBooleanField(payload, "alreadyActive")
|
|
4411
|
+
? "existing"
|
|
4412
|
+
: input.dryRun
|
|
4413
|
+
? "planned"
|
|
4414
|
+
: "activated",
|
|
4415
|
+
property: payload.property,
|
|
4097
4416
|
});
|
|
4098
4417
|
}
|
|
4099
4418
|
return plan;
|
|
@@ -4108,12 +4427,12 @@ function analyticsCommandAuthFlags(command) {
|
|
|
4108
4427
|
}
|
|
4109
4428
|
return flags;
|
|
4110
4429
|
}
|
|
4111
|
-
function
|
|
4430
|
+
function analyticsPropertyActivationCommand(input) {
|
|
4112
4431
|
return [
|
|
4113
4432
|
"tender",
|
|
4114
4433
|
"app",
|
|
4115
4434
|
"analytics",
|
|
4116
|
-
"
|
|
4435
|
+
"properties",
|
|
4117
4436
|
"activate",
|
|
4118
4437
|
shellQuote(input.command.artifactId),
|
|
4119
4438
|
"--event",
|
|
@@ -4121,16 +4440,16 @@ function analyticsPropertySlotActivationCommand(input) {
|
|
|
4121
4440
|
"--property",
|
|
4122
4441
|
shellQuote(input.requirement.propertyKey),
|
|
4123
4442
|
"--type",
|
|
4124
|
-
input.requirement.
|
|
4443
|
+
input.requirement.propertyType,
|
|
4125
4444
|
...(input.dryRun ? ["--dry-run"] : []),
|
|
4126
4445
|
...analyticsCommandAuthFlags(input.command),
|
|
4127
4446
|
"--json",
|
|
4128
4447
|
].join(" ");
|
|
4129
4448
|
}
|
|
4130
|
-
function
|
|
4449
|
+
function createAnalyticsPropertyGuidance(input) {
|
|
4131
4450
|
let requirements;
|
|
4132
4451
|
try {
|
|
4133
|
-
requirements =
|
|
4452
|
+
requirements = readRequiredAnalyticsPropertiesFromSpec(input.spec);
|
|
4134
4453
|
}
|
|
4135
4454
|
catch {
|
|
4136
4455
|
return null;
|
|
@@ -4142,11 +4461,11 @@ function createAnalyticsPropertySlotGuidance(input) {
|
|
|
4142
4461
|
"To make this chart available for dashboard queries, activate the custom event property first:",
|
|
4143
4462
|
];
|
|
4144
4463
|
for (const requirement of requirements) {
|
|
4145
|
-
lines.push(
|
|
4464
|
+
lines.push(analyticsPropertyActivationCommand({
|
|
4146
4465
|
command: input.command,
|
|
4147
4466
|
requirement,
|
|
4148
4467
|
dryRun: true,
|
|
4149
|
-
}),
|
|
4468
|
+
}), analyticsPropertyActivationCommand({
|
|
4150
4469
|
command: input.command,
|
|
4151
4470
|
requirement,
|
|
4152
4471
|
dryRun: false,
|
|
@@ -4172,8 +4491,8 @@ async function requestAnalyticsQueryWithPropertyGuidance(input) {
|
|
|
4172
4491
|
}
|
|
4173
4492
|
catch (error) {
|
|
4174
4493
|
if (error instanceof TenderCliAnalyticsApiError &&
|
|
4175
|
-
error.reasonCode === "
|
|
4176
|
-
const guidance =
|
|
4494
|
+
error.reasonCode === "analytics_property_not_queryable") {
|
|
4495
|
+
const guidance = createAnalyticsPropertyGuidance({
|
|
4177
4496
|
command: input.command,
|
|
4178
4497
|
spec: input.spec,
|
|
4179
4498
|
});
|
|
@@ -6216,6 +6535,131 @@ async function resolveAnalyticsCredentials(command, runtime) {
|
|
|
6216
6535
|
env: runtime.env ?? process.env,
|
|
6217
6536
|
});
|
|
6218
6537
|
}
|
|
6538
|
+
async function resolveDbCredentials(command, runtime) {
|
|
6539
|
+
return await resolveApiCredentials({
|
|
6540
|
+
baseUrl: command.baseUrl,
|
|
6541
|
+
flagToken: command.flagToken,
|
|
6542
|
+
profileName: command.profileName,
|
|
6543
|
+
env: runtime.env ?? process.env,
|
|
6544
|
+
});
|
|
6545
|
+
}
|
|
6546
|
+
function validateDbSql(sql) {
|
|
6547
|
+
if (!sql.trim()) {
|
|
6548
|
+
throw new TenderCliUsageError("SQL statement is required.");
|
|
6549
|
+
}
|
|
6550
|
+
const byteLength = new TextEncoder().encode(sql).byteLength;
|
|
6551
|
+
if (byteLength > DB_SQL_STATEMENT_MAX_BYTES) {
|
|
6552
|
+
throw new TenderCliUsageError(`SQL statement is limited to ${DB_SQL_STATEMENT_MAX_BYTES} bytes.`);
|
|
6553
|
+
}
|
|
6554
|
+
}
|
|
6555
|
+
async function readDbSql(command, runtime) {
|
|
6556
|
+
const sql = command.sqlSource.kind === "inline"
|
|
6557
|
+
? command.sqlSource.value
|
|
6558
|
+
: command.sqlSource.value === "-"
|
|
6559
|
+
? runtime.stdin
|
|
6560
|
+
: await readFile(path.resolve(command.sqlSource.value), "utf8");
|
|
6561
|
+
if (!sql) {
|
|
6562
|
+
throw new TenderCliUsageError("--file - requires SQL on stdin.");
|
|
6563
|
+
}
|
|
6564
|
+
validateDbSql(sql);
|
|
6565
|
+
return sql;
|
|
6566
|
+
}
|
|
6567
|
+
function printDbPayload(command, io, payload, fallbackLines) {
|
|
6568
|
+
if (command.json) {
|
|
6569
|
+
io.stdout(JSON.stringify(payload, null, 2));
|
|
6570
|
+
}
|
|
6571
|
+
else {
|
|
6572
|
+
io.stdout(fallbackLines.join("\n"));
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
async function runDbCapabilities(command, io, runtime) {
|
|
6576
|
+
const credentials = await resolveDbCredentials(command, runtime);
|
|
6577
|
+
const payload = await requestDbApi({
|
|
6578
|
+
artifactId: command.artifactId,
|
|
6579
|
+
dbPath: "capabilities",
|
|
6580
|
+
method: "GET",
|
|
6581
|
+
baseUrl: credentials.baseUrl,
|
|
6582
|
+
token: credentials.token,
|
|
6583
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
6584
|
+
});
|
|
6585
|
+
const limits = isCliJsonRecord(payload.limits) ? payload.limits : {};
|
|
6586
|
+
const runtimeInfo = isCliJsonRecord(payload.runtime) ? payload.runtime : {};
|
|
6587
|
+
printDbPayload(command, io, payload, [
|
|
6588
|
+
`app_id: ${command.artifactId}`,
|
|
6589
|
+
`backend: ${String(runtimeInfo.backend ?? "app-database")}`,
|
|
6590
|
+
`default_limit: ${String(limits.defaultLimit ?? "")}`,
|
|
6591
|
+
`max_limit: ${String(limits.maxInteractiveLimit ?? "")}`,
|
|
6592
|
+
]);
|
|
6593
|
+
return 0;
|
|
6594
|
+
}
|
|
6595
|
+
async function runDbTables(command, io, runtime) {
|
|
6596
|
+
const credentials = await resolveDbCredentials(command, runtime);
|
|
6597
|
+
const payload = await requestDbApi({
|
|
6598
|
+
artifactId: command.artifactId,
|
|
6599
|
+
dbPath: "tables",
|
|
6600
|
+
method: "GET",
|
|
6601
|
+
baseUrl: credentials.baseUrl,
|
|
6602
|
+
token: credentials.token,
|
|
6603
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
6604
|
+
environment: command.environment,
|
|
6605
|
+
});
|
|
6606
|
+
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
|
6607
|
+
printDbPayload(command, io, payload, [
|
|
6608
|
+
`app_id: ${command.artifactId}`,
|
|
6609
|
+
`environment: ${command.environment}`,
|
|
6610
|
+
`tables: ${rows.length}`,
|
|
6611
|
+
]);
|
|
6612
|
+
return 0;
|
|
6613
|
+
}
|
|
6614
|
+
async function runDbSchema(command, io, runtime) {
|
|
6615
|
+
const credentials = await resolveDbCredentials(command, runtime);
|
|
6616
|
+
const payload = await requestDbApi({
|
|
6617
|
+
artifactId: command.artifactId,
|
|
6618
|
+
dbPath: "schema",
|
|
6619
|
+
method: "GET",
|
|
6620
|
+
baseUrl: credentials.baseUrl,
|
|
6621
|
+
token: credentials.token,
|
|
6622
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
6623
|
+
environment: command.environment,
|
|
6624
|
+
tableName: command.tableName,
|
|
6625
|
+
});
|
|
6626
|
+
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
|
6627
|
+
printDbPayload(command, io, payload, [
|
|
6628
|
+
`app_id: ${command.artifactId}`,
|
|
6629
|
+
`environment: ${command.environment}`,
|
|
6630
|
+
`table: ${command.tableName}`,
|
|
6631
|
+
`schema_rows: ${rows.length}`,
|
|
6632
|
+
]);
|
|
6633
|
+
return 0;
|
|
6634
|
+
}
|
|
6635
|
+
async function runDbQuery(command, io, runtime) {
|
|
6636
|
+
const credentials = await resolveDbCredentials(command, runtime);
|
|
6637
|
+
const sql = await readDbSql(command, runtime);
|
|
6638
|
+
const body = {
|
|
6639
|
+
environment: command.environment,
|
|
6640
|
+
mode: "read",
|
|
6641
|
+
sql,
|
|
6642
|
+
params: command.params,
|
|
6643
|
+
...(command.limit === null ? {} : { limit: command.limit }),
|
|
6644
|
+
};
|
|
6645
|
+
const payload = await requestDbApi({
|
|
6646
|
+
artifactId: command.artifactId,
|
|
6647
|
+
dbPath: "query",
|
|
6648
|
+
method: "POST",
|
|
6649
|
+
baseUrl: credentials.baseUrl,
|
|
6650
|
+
token: credentials.token,
|
|
6651
|
+
fetcher: runtime.fetcher ?? fetch,
|
|
6652
|
+
body,
|
|
6653
|
+
});
|
|
6654
|
+
const rows = Array.isArray(payload.rows) ? payload.rows : [];
|
|
6655
|
+
printDbPayload(command, io, payload, [
|
|
6656
|
+
`app_id: ${command.artifactId}`,
|
|
6657
|
+
`environment: ${command.environment}`,
|
|
6658
|
+
`rows: ${rows.length}`,
|
|
6659
|
+
`truncated: ${String(payload.truncated ?? false)}`,
|
|
6660
|
+
]);
|
|
6661
|
+
return 0;
|
|
6662
|
+
}
|
|
6219
6663
|
async function runAnalyticsSummary(command, io, runtime) {
|
|
6220
6664
|
const credentials = await resolveAnalyticsCredentials(command, runtime);
|
|
6221
6665
|
const payload = await requestAnalyticsApi({
|
|
@@ -6380,93 +6824,43 @@ async function runAnalyticsExportQuery(command, io, runtime) {
|
|
|
6380
6824
|
io.stdout(JSON.stringify(payload, null, 2));
|
|
6381
6825
|
return 0;
|
|
6382
6826
|
}
|
|
6383
|
-
async function
|
|
6827
|
+
async function runAnalyticsPropertiesList(command, io, runtime) {
|
|
6384
6828
|
const credentials = await resolveAnalyticsCredentials(command, runtime);
|
|
6385
6829
|
const payload = await requestAnalyticsApi({
|
|
6386
6830
|
artifactId: command.artifactId,
|
|
6387
|
-
analyticsPath: "
|
|
6831
|
+
analyticsPath: "properties",
|
|
6388
6832
|
method: "GET",
|
|
6389
6833
|
baseUrl: credentials.baseUrl,
|
|
6390
6834
|
token: credentials.token,
|
|
6391
6835
|
fetcher: runtime.fetcher ?? fetch,
|
|
6392
6836
|
});
|
|
6393
|
-
const
|
|
6394
|
-
readCliStringField(
|
|
6837
|
+
const properties = readAnalyticsPropertyRecords(payload).filter((property) => !command.eventName ||
|
|
6838
|
+
readCliStringField(property, "eventName") === command.eventName);
|
|
6395
6839
|
printAnalyticsPayload(command, io, {
|
|
6396
6840
|
ok: true,
|
|
6397
6841
|
command: command.command,
|
|
6398
6842
|
artifactId: command.artifactId,
|
|
6399
6843
|
eventName: command.eventName,
|
|
6400
|
-
|
|
6844
|
+
properties,
|
|
6401
6845
|
}, [
|
|
6402
6846
|
`app_id: ${command.artifactId}`,
|
|
6403
6847
|
command.eventName ? `event: ${command.eventName}` : "event: all",
|
|
6404
|
-
`
|
|
6848
|
+
`properties: ${properties.length}`,
|
|
6405
6849
|
]);
|
|
6406
6850
|
return 0;
|
|
6407
6851
|
}
|
|
6408
|
-
async function
|
|
6852
|
+
async function runAnalyticsPropertiesActivate(command, io, runtime) {
|
|
6409
6853
|
const credentials = await resolveAnalyticsCredentials(command, runtime);
|
|
6410
|
-
const requirement = {
|
|
6411
|
-
eventName: command.eventName,
|
|
6412
|
-
propertyKey: command.propertyKey,
|
|
6413
|
-
slotType: command.slotType,
|
|
6414
|
-
};
|
|
6415
|
-
const { propertySlots } = await fetchAnalyticsPropertySlots({
|
|
6416
|
-
command,
|
|
6417
|
-
credentials,
|
|
6418
|
-
runtime,
|
|
6419
|
-
});
|
|
6420
|
-
const existing = findExistingAnalyticsPropertySlot({
|
|
6421
|
-
propertySlots,
|
|
6422
|
-
requirement,
|
|
6423
|
-
});
|
|
6424
|
-
if (existing) {
|
|
6425
|
-
const payload = {
|
|
6426
|
-
ok: true,
|
|
6427
|
-
command: command.command,
|
|
6428
|
-
artifactId: command.artifactId,
|
|
6429
|
-
alreadyActive: true,
|
|
6430
|
-
propertySlot: existing,
|
|
6431
|
-
};
|
|
6432
|
-
printAnalyticsPayload(command, io, payload, [
|
|
6433
|
-
`app_id: ${command.artifactId}`,
|
|
6434
|
-
"already_active: true",
|
|
6435
|
-
`slot_name: ${readCliStringField(existing, "slotName") ?? ""}`,
|
|
6436
|
-
]);
|
|
6437
|
-
return 0;
|
|
6438
|
-
}
|
|
6439
|
-
const slotName = chooseAnalyticsPropertySlotName({
|
|
6440
|
-
propertySlots,
|
|
6441
|
-
requirement,
|
|
6442
|
-
requestedSlotName: command.slotName,
|
|
6443
|
-
});
|
|
6444
6854
|
const request = {
|
|
6445
6855
|
eventName: command.eventName,
|
|
6446
6856
|
propertyKey: command.propertyKey,
|
|
6447
|
-
|
|
6448
|
-
slotName,
|
|
6857
|
+
propertyType: command.propertyType,
|
|
6449
6858
|
label: command.label ?? command.propertyKey,
|
|
6859
|
+
dryRun: command.dryRun,
|
|
6450
6860
|
};
|
|
6451
|
-
if (command.dryRun) {
|
|
6452
|
-
printAnalyticsPayload(command, io, {
|
|
6453
|
-
ok: true,
|
|
6454
|
-
command: command.command,
|
|
6455
|
-
dryRun: true,
|
|
6456
|
-
artifactId: command.artifactId,
|
|
6457
|
-
request,
|
|
6458
|
-
}, [
|
|
6459
|
-
`app_id: ${command.artifactId}`,
|
|
6460
|
-
`event: ${command.eventName}`,
|
|
6461
|
-
`property: ${command.propertyKey}`,
|
|
6462
|
-
`slot_name: ${slotName}`,
|
|
6463
|
-
"dry_run: true",
|
|
6464
|
-
]);
|
|
6465
|
-
return 0;
|
|
6466
|
-
}
|
|
6467
6861
|
const payload = await requestAnalyticsApi({
|
|
6468
6862
|
artifactId: command.artifactId,
|
|
6469
|
-
analyticsPath: "
|
|
6863
|
+
analyticsPath: "properties",
|
|
6470
6864
|
method: "POST",
|
|
6471
6865
|
baseUrl: credentials.baseUrl,
|
|
6472
6866
|
token: credentials.token,
|
|
@@ -6477,7 +6871,8 @@ async function runAnalyticsPropertySlotsActivate(command, io, runtime) {
|
|
|
6477
6871
|
`app_id: ${command.artifactId}`,
|
|
6478
6872
|
`event: ${command.eventName}`,
|
|
6479
6873
|
`property: ${command.propertyKey}`,
|
|
6480
|
-
`
|
|
6874
|
+
`type: ${command.propertyType}`,
|
|
6875
|
+
...(command.dryRun ? ["dry_run: true"] : []),
|
|
6481
6876
|
]);
|
|
6482
6877
|
return 0;
|
|
6483
6878
|
}
|
|
@@ -6492,16 +6887,16 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
|
|
|
6492
6887
|
spec,
|
|
6493
6888
|
layout: {},
|
|
6494
6889
|
};
|
|
6495
|
-
const
|
|
6496
|
-
const credentials = !command.dryRun ||
|
|
6890
|
+
const requiredProperties = readRequiredAnalyticsPropertiesFromSpec(spec);
|
|
6891
|
+
const credentials = !command.dryRun || requiredProperties.length > 0
|
|
6497
6892
|
? await resolveAnalyticsCredentials(command, runtime)
|
|
6498
6893
|
: null;
|
|
6499
|
-
const
|
|
6500
|
-
? await
|
|
6894
|
+
const properties = credentials
|
|
6895
|
+
? await ensureAnalyticsProperties({
|
|
6501
6896
|
command,
|
|
6502
6897
|
credentials,
|
|
6503
6898
|
runtime,
|
|
6504
|
-
requirements:
|
|
6899
|
+
requirements: requiredProperties,
|
|
6505
6900
|
dryRun: command.dryRun,
|
|
6506
6901
|
})
|
|
6507
6902
|
: [];
|
|
@@ -6510,7 +6905,7 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
|
|
|
6510
6905
|
ok: true,
|
|
6511
6906
|
dryRun: true,
|
|
6512
6907
|
artifactId: command.artifactId,
|
|
6513
|
-
|
|
6908
|
+
properties,
|
|
6514
6909
|
request: body,
|
|
6515
6910
|
};
|
|
6516
6911
|
printAnalyticsPayload(command, io, payload, [
|
|
@@ -6533,10 +6928,10 @@ async function runAnalyticsChartsCreate(command, io, runtime) {
|
|
|
6533
6928
|
fetcher: runtime.fetcher ?? fetch,
|
|
6534
6929
|
body,
|
|
6535
6930
|
});
|
|
6536
|
-
printAnalyticsPayload(command, io, { ...payload,
|
|
6931
|
+
printAnalyticsPayload(command, io, { ...payload, properties }, [
|
|
6537
6932
|
`app_id: ${command.artifactId}`,
|
|
6538
6933
|
`chart_id: ${String(payload.chart?.id ?? "")}`,
|
|
6539
|
-
`
|
|
6934
|
+
`properties: ${properties.length}`,
|
|
6540
6935
|
]);
|
|
6541
6936
|
return 0;
|
|
6542
6937
|
}
|
|
@@ -6583,11 +6978,11 @@ async function runAnalyticsChartsUpdate(command, io, runtime) {
|
|
|
6583
6978
|
spec,
|
|
6584
6979
|
layout: readAnalyticsChartLayout(existing.chart),
|
|
6585
6980
|
};
|
|
6586
|
-
const
|
|
6981
|
+
const properties = await ensureAnalyticsProperties({
|
|
6587
6982
|
command,
|
|
6588
6983
|
credentials,
|
|
6589
6984
|
runtime,
|
|
6590
|
-
requirements:
|
|
6985
|
+
requirements: readRequiredAnalyticsPropertiesFromSpec(spec),
|
|
6591
6986
|
dryRun: command.dryRun,
|
|
6592
6987
|
});
|
|
6593
6988
|
const dashboardId = readCliStringField(existing.dashboard, "id");
|
|
@@ -6599,7 +6994,7 @@ async function runAnalyticsChartsUpdate(command, io, runtime) {
|
|
|
6599
6994
|
chartId: command.chartId,
|
|
6600
6995
|
dashboardId,
|
|
6601
6996
|
currentChart: existing.chart,
|
|
6602
|
-
|
|
6997
|
+
properties,
|
|
6603
6998
|
request: body,
|
|
6604
6999
|
}, [
|
|
6605
7000
|
`app_id: ${command.artifactId}`,
|
|
@@ -6619,11 +7014,11 @@ async function runAnalyticsChartsUpdate(command, io, runtime) {
|
|
|
6619
7014
|
fetcher: runtime.fetcher ?? fetch,
|
|
6620
7015
|
body,
|
|
6621
7016
|
});
|
|
6622
|
-
printAnalyticsPayload(command, io, { ...payload,
|
|
7017
|
+
printAnalyticsPayload(command, io, { ...payload, properties }, [
|
|
6623
7018
|
`app_id: ${command.artifactId}`,
|
|
6624
7019
|
`chart_id: ${command.chartId}`,
|
|
6625
7020
|
`title: ${String(payload.chart?.title ?? title)}`,
|
|
6626
|
-
`
|
|
7021
|
+
`properties: ${properties.length}`,
|
|
6627
7022
|
]);
|
|
6628
7023
|
return 0;
|
|
6629
7024
|
}
|
|
@@ -6792,11 +7187,26 @@ export async function runTenderCli(args, io = {
|
|
|
6792
7187
|
else if (command.topic === "app analytics charts delete") {
|
|
6793
7188
|
io.stdout(analyticsChartsDeleteHelp());
|
|
6794
7189
|
}
|
|
6795
|
-
else if (command.topic === "app analytics
|
|
6796
|
-
io.stdout(
|
|
7190
|
+
else if (command.topic === "app analytics properties list") {
|
|
7191
|
+
io.stdout(analyticsPropertiesListHelp());
|
|
6797
7192
|
}
|
|
6798
|
-
else if (command.topic === "app analytics
|
|
6799
|
-
io.stdout(
|
|
7193
|
+
else if (command.topic === "app analytics properties activate") {
|
|
7194
|
+
io.stdout(analyticsPropertiesActivateHelp());
|
|
7195
|
+
}
|
|
7196
|
+
else if (command.topic === "app db") {
|
|
7197
|
+
io.stdout(dbHelp());
|
|
7198
|
+
}
|
|
7199
|
+
else if (command.topic === "app db capabilities") {
|
|
7200
|
+
io.stdout(dbCapabilitiesHelp());
|
|
7201
|
+
}
|
|
7202
|
+
else if (command.topic === "app db tables") {
|
|
7203
|
+
io.stdout(dbTablesHelp());
|
|
7204
|
+
}
|
|
7205
|
+
else if (command.topic === "app db schema") {
|
|
7206
|
+
io.stdout(dbSchemaHelp());
|
|
7207
|
+
}
|
|
7208
|
+
else if (command.topic === "app db query") {
|
|
7209
|
+
io.stdout(dbQueryHelp());
|
|
6800
7210
|
}
|
|
6801
7211
|
else if (command.topic === "app") {
|
|
6802
7212
|
io.stdout(appHelp());
|
|
@@ -6908,11 +7318,23 @@ export async function runTenderCli(args, io = {
|
|
|
6908
7318
|
if (command.command === "app analytics charts delete") {
|
|
6909
7319
|
return await runAnalyticsChartsDelete(command, io, runtime);
|
|
6910
7320
|
}
|
|
6911
|
-
if (command.command === "app analytics
|
|
6912
|
-
return await
|
|
7321
|
+
if (command.command === "app analytics properties list") {
|
|
7322
|
+
return await runAnalyticsPropertiesList(command, io, runtime);
|
|
6913
7323
|
}
|
|
6914
|
-
if (command.command === "app analytics
|
|
6915
|
-
return await
|
|
7324
|
+
if (command.command === "app analytics properties activate") {
|
|
7325
|
+
return await runAnalyticsPropertiesActivate(command, io, runtime);
|
|
7326
|
+
}
|
|
7327
|
+
if (command.command === "app db capabilities") {
|
|
7328
|
+
return await runDbCapabilities(command, io, runtime);
|
|
7329
|
+
}
|
|
7330
|
+
if (command.command === "app db tables") {
|
|
7331
|
+
return await runDbTables(command, io, runtime);
|
|
7332
|
+
}
|
|
7333
|
+
if (command.command === "app db schema") {
|
|
7334
|
+
return await runDbSchema(command, io, runtime);
|
|
7335
|
+
}
|
|
7336
|
+
if (command.command === "app db query") {
|
|
7337
|
+
return await runDbQuery(command, io, runtime);
|
|
6916
7338
|
}
|
|
6917
7339
|
if (command.command === "app git remote") {
|
|
6918
7340
|
return await runGitRemote(command, io, runtime);
|
|
@@ -6926,10 +7348,16 @@ export async function runTenderCli(args, io = {
|
|
|
6926
7348
|
}
|
|
6927
7349
|
}
|
|
6928
7350
|
const currentFile = fileURLToPath(import.meta.url);
|
|
7351
|
+
function hasFlagValue(args, flag, value) {
|
|
7352
|
+
return args.some((arg, index) => arg === flag && args[index + 1] === value);
|
|
7353
|
+
}
|
|
6929
7354
|
function shouldReadEntrypointStdin(args) {
|
|
6930
7355
|
return (args[0] === "app" &&
|
|
6931
7356
|
((args[1] === "git" && args[2] === "credential") ||
|
|
6932
|
-
(args[1] === "analytics" && args
|
|
7357
|
+
(args[1] === "analytics" && hasFlagValue(args, "--spec", "-")) ||
|
|
7358
|
+
(args[1] === "db" &&
|
|
7359
|
+
args[2] === "query" &&
|
|
7360
|
+
hasFlagValue(args, "--file", "-"))));
|
|
6933
7361
|
}
|
|
6934
7362
|
async function readEntrypointStdin() {
|
|
6935
7363
|
if (process.stdin.isTTY) {
|