@tenderprompt/cli 0.1.12 → 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 +60 -0
- package/dist/index.js +575 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ session:
|
|
|
34
34
|
- build and preview a commit through the Tender API
|
|
35
35
|
- publish only after a dry run or explicit `--confirm publish`
|
|
36
36
|
- inspect artifact-scoped analytics without SQL or platform credentials
|
|
37
|
+
- inspect artifact-scoped app database tables, schema, and bounded read-only
|
|
38
|
+
query output
|
|
37
39
|
- run server-validated chart specs and export aggregate rows
|
|
38
40
|
- generate starter analytics suggestions
|
|
39
41
|
- create, list, update, and delete saved analytics dashboards and charts, with
|
|
@@ -87,6 +89,8 @@ TTL values are intentionally bounded:
|
|
|
87
89
|
|
|
88
90
|
Use `--profile publish` only when the token should be allowed to publish.
|
|
89
91
|
Publish still requires explicit confirmation with `--confirm publish`.
|
|
92
|
+
Use `--profile db-read --artifact <app-id>` for read-only app database
|
|
93
|
+
inspection.
|
|
90
94
|
|
|
91
95
|
Credential precedence is explicit-first:
|
|
92
96
|
|
|
@@ -128,6 +132,10 @@ tender app validate artifact_123 --json
|
|
|
128
132
|
tender app build artifact_123 --commit "$(git rev-parse HEAD)" --json
|
|
129
133
|
tender app preview artifact_123 --commit "$(git rev-parse HEAD)" --stream --json
|
|
130
134
|
tender app publish artifact_123 --dry-run --json
|
|
135
|
+
tender app db capabilities artifact_123 --json
|
|
136
|
+
tender app db tables artifact_123 --json
|
|
137
|
+
tender app db schema artifact_123 --table votes --json
|
|
138
|
+
tender app db query artifact_123 --sql "select * from votes limit 20" --json
|
|
131
139
|
```
|
|
132
140
|
|
|
133
141
|
## Analytics
|
|
@@ -226,6 +234,56 @@ filter. `analytics query` is read-only, so use
|
|
|
226
234
|
query fails for a missing dashboard-query property, the CLI prints the exact
|
|
227
235
|
activation commands to run and does not perform that write automatically.
|
|
228
236
|
|
|
237
|
+
Event payload properties can be tracked by the app, observed in bounded catalog
|
|
238
|
+
samples, and still not be queryable in fast dashboard charts. Register intended
|
|
239
|
+
dashboard dimensions and metrics before meaningful traffic starts. Use
|
|
240
|
+
dimensions for grouping values such as variant, plan, step, source, or outcome,
|
|
241
|
+
and metrics for numeric values such as score, amount, duration, or quantity.
|
|
242
|
+
Making a property queryable does not require an app redeploy, but older
|
|
243
|
+
dashboard-query rows may not fully populate grouped charts until new traffic
|
|
244
|
+
arrives. Do not make every property queryable; avoid PII, secrets, free text,
|
|
245
|
+
ids, URLs, or high-cardinality values unless the chart has a clear purpose.
|
|
246
|
+
|
|
247
|
+
## App Database
|
|
248
|
+
|
|
249
|
+
App database commands are artifact-scoped and go through Tender's server-side
|
|
250
|
+
authorization and read-only query checks. The CLI only accepts an app id; it
|
|
251
|
+
does not accept tenant ids, runner names, provider account ids, or runtime
|
|
252
|
+
storage identifiers.
|
|
253
|
+
|
|
254
|
+
Start with capabilities:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db --json
|
|
258
|
+
|
|
259
|
+
tender app db capabilities artifact_123 --profile db --json
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Common read-only commands:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
tender app db tables artifact_123 --profile db --json
|
|
266
|
+
|
|
267
|
+
tender app db schema artifact_123 --table votes --profile db --json
|
|
268
|
+
|
|
269
|
+
tender app db query artifact_123 --sql "select * from votes limit 20" --profile db --json
|
|
270
|
+
|
|
271
|
+
tender app db query artifact_123 \
|
|
272
|
+
--sql "select * from votes where choice = ?" \
|
|
273
|
+
--param coffee \
|
|
274
|
+
--profile db \
|
|
275
|
+
--json
|
|
276
|
+
|
|
277
|
+
tender app db query artifact_123 --file ./debug.sql --limit 50 --profile db --json
|
|
278
|
+
|
|
279
|
+
tender app db query artifact_123 --file - --profile db --json < debug.sql
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The query command sends one read-only statement with positional bindings.
|
|
283
|
+
Client and server limits include a 100 KB statement cap and at most 100 bound
|
|
284
|
+
parameters. The server enforces the final policy, row limits, response limits,
|
|
285
|
+
artifact scope, and audit logging.
|
|
286
|
+
|
|
229
287
|
## Safety Model
|
|
230
288
|
|
|
231
289
|
- Local agents run in the user's environment, not in Tender.
|
|
@@ -242,6 +300,8 @@ activation commands to run and does not perform that write automatically.
|
|
|
242
300
|
tenant/artifact-scoped.
|
|
243
301
|
- Raw analytics event export is intentionally not available; exports are bounded
|
|
244
302
|
catalog metadata or validated aggregate query rows.
|
|
303
|
+
- App database commands require `db:read`, should use artifact-scoped tokens,
|
|
304
|
+
and never require provider account ids or runtime storage identifiers.
|
|
245
305
|
|
|
246
306
|
## Related Package
|
|
247
307
|
|
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]
|
|
@@ -217,6 +236,14 @@ Commands:
|
|
|
217
236
|
List properties available to dashboard queries
|
|
218
237
|
tender app analytics properties activate <app-id>
|
|
219
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>
|
|
@@ -257,6 +284,10 @@ Examples:
|
|
|
257
284
|
tender app analytics charts delete artifact_123 --chart analytics_chart_123 --dry-run --json
|
|
258
285
|
tender app analytics properties list artifact_123 --json
|
|
259
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
|
|
@@ -435,6 +466,57 @@ Examples:
|
|
|
435
466
|
tender app analytics properties activate artifact_123 --event signup_started --property plan --type dimension --dry-run --json
|
|
436
467
|
tender app analytics properties activate artifact_123 --event score_saved --property score --type metric --json`;
|
|
437
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]
|
|
500
|
+
|
|
501
|
+
Examples:
|
|
502
|
+
tender app db schema artifact_123 --table votes --json
|
|
503
|
+
tender app db schema artifact_123 --table preference_votes --env preview --json`;
|
|
504
|
+
}
|
|
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.
|
|
513
|
+
|
|
514
|
+
Examples:
|
|
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`;
|
|
519
|
+
}
|
|
438
520
|
function artifactsListHelp() {
|
|
439
521
|
return `Usage: tender app list [--base-url <url>] [--token <token>] [--profile <name>] [--json]
|
|
440
522
|
|
|
@@ -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
|
},
|
|
@@ -832,6 +915,23 @@ function tenderCliCapabilities() {
|
|
|
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.",
|
|
@@ -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,
|
|
@@ -2941,10 +3070,12 @@ function analyticsChartTemplates() {
|
|
|
2941
3070
|
purpose: "Compare a queryable dimension property observed in events.",
|
|
2942
3071
|
usableAsIs: false,
|
|
2943
3072
|
placeholders: {
|
|
3073
|
+
eventName: "<event_name>",
|
|
2944
3074
|
property: "<queryable_dimension_property>",
|
|
2945
3075
|
},
|
|
2946
3076
|
adaptWith: [
|
|
2947
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: {
|
|
@@ -2953,7 +3084,7 @@ function analyticsChartTemplates() {
|
|
|
2953
3084
|
visualization: "bar",
|
|
2954
3085
|
metric: { name: "custom_event_count" },
|
|
2955
3086
|
groupBy: [{ field: "property:<queryable_dimension_property>" }],
|
|
2956
|
-
filters: [],
|
|
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,10 +3097,12 @@ function analyticsChartTemplates() {
|
|
|
2966
3097
|
purpose: "Track an observed numeric event property over time.",
|
|
2967
3098
|
usableAsIs: false,
|
|
2968
3099
|
placeholders: {
|
|
3100
|
+
eventName: "<event_name>",
|
|
2969
3101
|
metric: "avg:<queryable_metric_property>",
|
|
2970
3102
|
},
|
|
2971
3103
|
adaptWith: [
|
|
2972
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: {
|
|
@@ -2978,7 +3111,7 @@ function analyticsChartTemplates() {
|
|
|
2978
3111
|
visualization: "line",
|
|
2979
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,
|
|
@@ -3292,6 +3425,211 @@ function parseAnalyticsPropertiesActivateArgs(args) {
|
|
|
3292
3425
|
...parsed,
|
|
3293
3426
|
};
|
|
3294
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
|
+
}
|
|
3295
3633
|
function parseTenderArgs(args) {
|
|
3296
3634
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
3297
3635
|
return { command: "help", topic: "root" };
|
|
@@ -3441,6 +3779,27 @@ function parseTenderArgs(args) {
|
|
|
3441
3779
|
resourceArgs[3] === "delete") {
|
|
3442
3780
|
return parseAnalyticsChartsDeleteArgs(resourceArgs.slice(4));
|
|
3443
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
|
+
}
|
|
3444
3803
|
if (resourceArgs[1] === "context" &&
|
|
3445
3804
|
(resourceArgs[2] === undefined ||
|
|
3446
3805
|
resourceArgs[2] === "--help" ||
|
|
@@ -3842,6 +4201,55 @@ async function requestAnalyticsApi(input) {
|
|
|
3842
4201
|
}
|
|
3843
4202
|
return payload;
|
|
3844
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
|
+
}
|
|
3845
4253
|
function analyticsCorrectedExample(artifactId, analyticsPath) {
|
|
3846
4254
|
if (analyticsPath === "capabilities") {
|
|
3847
4255
|
return `tender app analytics capabilities ${artifactId} --include-catalog --range 30d --json`;
|
|
@@ -6127,6 +6535,131 @@ async function resolveAnalyticsCredentials(command, runtime) {
|
|
|
6127
6535
|
env: runtime.env ?? process.env,
|
|
6128
6536
|
});
|
|
6129
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
|
+
}
|
|
6130
6663
|
async function runAnalyticsSummary(command, io, runtime) {
|
|
6131
6664
|
const credentials = await resolveAnalyticsCredentials(command, runtime);
|
|
6132
6665
|
const payload = await requestAnalyticsApi({
|
|
@@ -6660,6 +7193,21 @@ export async function runTenderCli(args, io = {
|
|
|
6660
7193
|
else if (command.topic === "app analytics properties activate") {
|
|
6661
7194
|
io.stdout(analyticsPropertiesActivateHelp());
|
|
6662
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());
|
|
7210
|
+
}
|
|
6663
7211
|
else if (command.topic === "app") {
|
|
6664
7212
|
io.stdout(appHelp());
|
|
6665
7213
|
}
|
|
@@ -6776,6 +7324,18 @@ export async function runTenderCli(args, io = {
|
|
|
6776
7324
|
if (command.command === "app analytics properties activate") {
|
|
6777
7325
|
return await runAnalyticsPropertiesActivate(command, io, runtime);
|
|
6778
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);
|
|
7338
|
+
}
|
|
6779
7339
|
if (command.command === "app git remote") {
|
|
6780
7340
|
return await runGitRemote(command, io, runtime);
|
|
6781
7341
|
}
|
|
@@ -6788,10 +7348,16 @@ export async function runTenderCli(args, io = {
|
|
|
6788
7348
|
}
|
|
6789
7349
|
}
|
|
6790
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
|
+
}
|
|
6791
7354
|
function shouldReadEntrypointStdin(args) {
|
|
6792
7355
|
return (args[0] === "app" &&
|
|
6793
7356
|
((args[1] === "git" && args[2] === "credential") ||
|
|
6794
|
-
(args[1] === "analytics" && args
|
|
7357
|
+
(args[1] === "analytics" && hasFlagValue(args, "--spec", "-")) ||
|
|
7358
|
+
(args[1] === "db" &&
|
|
7359
|
+
args[2] === "query" &&
|
|
7360
|
+
hasFlagValue(args, "--file", "-"))));
|
|
6795
7361
|
}
|
|
6796
7362
|
async function readEntrypointStdin() {
|
|
6797
7363
|
if (process.stdin.isTTY) {
|