@formo/cli 1.1.0 → 1.2.0
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 +92 -31
- package/dist/commands/alerts.d.ts +14 -22
- package/dist/commands/alerts.js +48 -49
- package/dist/commands/analytics.d.ts +1 -1
- package/dist/commands/analytics.js +51 -11
- package/dist/commands/boards.d.ts +7 -9
- package/dist/commands/boards.js +4 -14
- package/dist/commands/charts.d.ts +11 -13
- package/dist/commands/charts.js +12 -16
- package/dist/commands/contracts.d.ts +9 -11
- package/dist/commands/contracts.js +7 -17
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +6 -5
- package/dist/commands/import.d.ts +1 -1
- package/dist/commands/import.js +3 -0
- package/dist/commands/profiles.d.ts +13 -13
- package/dist/commands/profiles.js +189 -49
- package/dist/commands/query.d.ts +1 -1
- package/dist/commands/segments.d.ts +7 -9
- package/dist/commands/segments.js +45 -25
- package/dist/index.js +39 -26
- package/dist/lib/client.d.ts +18 -3
- package/dist/lib/client.js +11 -2
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +32 -18
- package/dist/lib/filters.d.ts +6 -0
- package/dist/lib/filters.js +45 -0
- package/dist/lib/pagination.d.ts +14 -0
- package/dist/lib/pagination.js +31 -0
- package/dist/lib/ui.d.ts +6 -5
- package/dist/lib/ui.js +10 -12
- package/package.json +8 -12
package/README.md
CHANGED
|
@@ -104,23 +104,38 @@ Search wallet profiles with filters, sorting, and pagination. Returns a `Paginat
|
|
|
104
104
|
| Option | Description |
|
|
105
105
|
|---|---|
|
|
106
106
|
| `--address` | Filter by wallet address |
|
|
107
|
+
| `--search` | Free-text search across address and identity fields |
|
|
107
108
|
| `--page` | Page number (1-indexed, default `1`) |
|
|
108
109
|
| `--size` | Page size (default `100`, max `1000`) |
|
|
109
110
|
| `--order-by` | `last_onchain`, `first_onchain`, `net_worth_usd`, `updated_at`, `tx_count`, `first_seen`, `last_seen`, `num_sessions`, `revenue`, `volume`, `points` |
|
|
110
111
|
| `--order-dir` | `asc` or `desc` |
|
|
111
112
|
| `--expand` | Comma-separated fields to expand |
|
|
112
|
-
| `--
|
|
113
|
-
| `--logic` | Combine
|
|
113
|
+
| `--filters` | JSON array of canonical `{field,op,value}` filter objects (see below) |
|
|
114
|
+
| `--logic` | Combine filters with `and` (default) or `or` |
|
|
114
115
|
|
|
115
116
|
```bash
|
|
116
117
|
formo profiles search --size 10
|
|
117
118
|
formo profiles search --order-by net_worth_usd --order-dir desc --size 5
|
|
118
119
|
formo profiles search --page 2 --size 20
|
|
119
|
-
formo profiles search --
|
|
120
|
-
formo profiles search --
|
|
121
|
-
formo profiles search --
|
|
120
|
+
formo profiles search --filters '[{"field":"users.net_worth_usd","op":"gt","value":10000}]' --size 20
|
|
121
|
+
formo profiles search --filters '[{"field":"users.net_worth_usd","op":"gt","value":10000},{"field":"users.volume","op":"gt","value":1000}]' --logic or --size 20
|
|
122
|
+
formo profiles search --filters '[{"field":"chains.balance","op":"gt","value":1000,"chain_id":"1"}]' --size 20
|
|
122
123
|
```
|
|
123
124
|
|
|
125
|
+
### Lifecycle tuning (advanced)
|
|
126
|
+
|
|
127
|
+
Both `profiles get` and `profiles search` accept optional flags to override the lifecycle stage thresholds used when computing `lifecycle`:
|
|
128
|
+
|
|
129
|
+
| Option | Description |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `--new-window-days` | Override lifecycle new-user window in days |
|
|
132
|
+
| `--churn-window-days` | Override lifecycle churn window in days |
|
|
133
|
+
| `--power-user-min-active-days` | Override lifecycle power-user minimum active days |
|
|
134
|
+
| `--power-user-window-days` | Override lifecycle power-user window in days |
|
|
135
|
+
| `--resurrected-gap-days` | Override lifecycle resurrected gap in days |
|
|
136
|
+
| `--at-risk-min-days-inactive` | Override lifecycle at-risk minimum inactive days |
|
|
137
|
+
| `--at-risk-prior-active-days-threshold` | Override lifecycle at-risk prior active days threshold |
|
|
138
|
+
|
|
124
139
|
### `profiles update <address>`
|
|
125
140
|
|
|
126
141
|
Merge-update identity properties on a wallet profile.
|
|
@@ -205,15 +220,16 @@ Get a single alert by ID.
|
|
|
205
220
|
| `--trigger-filters` | JSON array of trigger filter objects |
|
|
206
221
|
| `--recipient` | JSON array of recipient objects |
|
|
207
222
|
| `--secret` | Webhook secret |
|
|
223
|
+
| `--slack-property-keys` | JSON array of event/user property keys to include in Slack alerts |
|
|
208
224
|
|
|
209
225
|
```bash
|
|
210
226
|
formo alerts create --name "High value tx" --trigger-type event \
|
|
211
|
-
--trigger-filters '[{"
|
|
227
|
+
--trigger-filters '[{"field":"event","op":"eq","value":"transaction"}]' \
|
|
212
228
|
--recipient '[{"type":"email","value":["alerts@myapp.com"]}]'
|
|
213
229
|
```
|
|
214
230
|
|
|
215
231
|
### `alerts update <alertId>`
|
|
216
|
-
Same options as `create`. Replaces the alert configuration.
|
|
232
|
+
Same options as `create`. Replaces the alert configuration in full — omitted options are reset to their defaults (e.g. leaving out `--trigger-filters` clears the existing trigger filters).
|
|
217
233
|
|
|
218
234
|
### `alerts delete <alertId>`
|
|
219
235
|
Delete an alert.
|
|
@@ -271,7 +287,7 @@ Delete a board.
|
|
|
271
287
|
|
|
272
288
|
## `formo charts`
|
|
273
289
|
|
|
274
|
-
Chart commands. Charts live inside a board. Requires `
|
|
290
|
+
Chart commands. Charts live inside a board. Requires `boards:read` / `boards:write`.
|
|
275
291
|
|
|
276
292
|
### `charts list --board-id <boardId>`
|
|
277
293
|
List all charts in a board.
|
|
@@ -292,10 +308,18 @@ formo charts create --board-id brd_123 --title "Daily Active Users" \
|
|
|
292
308
|
--x-axis date --y-axis users
|
|
293
309
|
|
|
294
310
|
formo charts create --board-id brd_123 --body '{"title":"Recent Events","chart_type":"table","query":"SELECT * FROM events LIMIT 10"}'
|
|
311
|
+
|
|
312
|
+
formo charts create --board-id brd_123 --title "Post-connect paths" \
|
|
313
|
+
--chart-type user_paths \
|
|
314
|
+
--settings '{"anchors":[{"type":"event","event":"connect"}],"maxSteps":5,"nodesPerStep":3}'
|
|
295
315
|
```
|
|
296
316
|
|
|
297
|
-
### `charts update <chartId> --board-id <boardId>
|
|
298
|
-
Update a chart.
|
|
317
|
+
### `charts update <chartId> --board-id <boardId> [options]`
|
|
318
|
+
Update a chart. Accepts the same options as `create`: a raw `--body '<json>'` and/or typed flags (`--title`, `--chart-type`, `--query`, `--description`, `--x-axis`, `--y-axis`, `--group-by`, `--steps`, `--settings`). `--steps` is for funnel steps. User Paths use `--settings` with `anchors` and optional `maxSteps` / `nodesPerStep`; retention settings require an `entryFilter` key. Typed flags override matching `--body` keys.
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
formo charts update chart_abc123 --board-id brd_123 --title "Renamed chart"
|
|
322
|
+
```
|
|
299
323
|
|
|
300
324
|
### `charts query <chartId> --board-id <boardId> --date-from <YYYY-MM-DD> --date-to <YYYY-MM-DD>`
|
|
301
325
|
Execute a saved chart that uses `{{date_from}}` / `{{date_to}}` variables.
|
|
@@ -369,7 +393,7 @@ List all user segments.
|
|
|
369
393
|
| Option | Description |
|
|
370
394
|
|---|---|
|
|
371
395
|
| `--title` | Segment title |
|
|
372
|
-
| `--
|
|
396
|
+
| `--filters` | JSON array of canonical `{field,op,value}` filter objects. Array string members cannot contain `|` |
|
|
373
397
|
|
|
374
398
|
### `segments delete <segmentId>`
|
|
375
399
|
Delete a user segment.
|
|
@@ -401,7 +425,7 @@ Pre-built analytics pipes — the same data that powers the Formo dashboard —
|
|
|
401
425
|
|---|---|
|
|
402
426
|
| `--date-from` | Inclusive start date `YYYY-MM-DD` (default: 7 days before `--date-to`) |
|
|
403
427
|
| `--date-to` | Inclusive end date `YYYY-MM-DD` (default: today) |
|
|
404
|
-
| `--filters` | JSON array of `[{field,op,value}]`.
|
|
428
|
+
| `--filters` | JSON array of `[{field,op,value}]`. For `in`/`nin`, array values are preferred; pipe-delimited strings are also accepted. Array string members cannot contain `|` |
|
|
405
429
|
| `--params` | JSON object of pipe-specific params merged into the query (e.g. `{"limit":10,"group_by":"device"}`) |
|
|
406
430
|
|
|
407
431
|
```bash
|
|
@@ -414,13 +438,17 @@ formo analytics retention --filters '[{"field":"location","op":"eq","value":"US"
|
|
|
414
438
|
|
|
415
439
|
> Requires `query:read` scope. Run `formo analytics <pipe> --help` for the pipe-specific params accepted via `--params`.
|
|
416
440
|
|
|
441
|
+
On `kpis`, `top_*`, `revenue_*` and `volume_by_metric`, `--params '{"page_scope":"session"}'` widens a `page` filter from page-scoped metrics (the default) to the legacy session scope.
|
|
442
|
+
|
|
443
|
+
All user-attribute, profile, social, lifecycle and resource predicates go in the single `--filters` array, using the canonical envelope with named qualifiers (`chain_id`, `app_id`, `token_address`, `scope`, `tag_id`). The retired per-family params — `socials`, `chain_filters`, `app_filters`, `token_filters`, `label_filters`, `profile_filters`, `lifecycle_filter` — are rejected with a `400` if passed through `--params`.
|
|
444
|
+
|
|
417
445
|
---
|
|
418
446
|
|
|
419
447
|
## `formo import`
|
|
420
448
|
|
|
421
449
|
### `import wallets`
|
|
422
450
|
|
|
423
|
-
Bulk-import wallet addresses into the project via the
|
|
451
|
+
Bulk-import wallet addresses into the project via the main Formo API, authenticated with your workspace API key.
|
|
424
452
|
|
|
425
453
|
| Option | Description |
|
|
426
454
|
|---|---|
|
|
@@ -432,53 +460,86 @@ formo import wallets --addresses '["0xabc...","0xdef..."]'
|
|
|
432
460
|
formo import wallets --rows '[{"address":"0xabc...","properties":{"display_name":"Alice"}}]'
|
|
433
461
|
```
|
|
434
462
|
|
|
463
|
+
> Requires `profiles:write` scope. Only available on Scale and Enterprise plans.
|
|
464
|
+
|
|
435
465
|
---
|
|
436
466
|
|
|
437
467
|
## `formo events`
|
|
438
468
|
|
|
439
469
|
### `events ingest`
|
|
440
470
|
|
|
441
|
-
Send raw analytics events to `events.formo.so`. This command uses a project SDK write key, not the workspace API key.
|
|
471
|
+
Send raw analytics events to `events.formo.so`. This command uses a project SDK write key, not the workspace API key — pass it via `--write-key` or the `FORMO_WRITE_KEY` environment variable.
|
|
472
|
+
|
|
473
|
+
| Option | Description |
|
|
474
|
+
|---|---|
|
|
475
|
+
| `--event` | Single event as a JSON object; wrapped in an array before sending |
|
|
476
|
+
| `--events` | JSON array of event objects to send as a batch |
|
|
477
|
+
| `--write-key` | Project SDK write key (defaults to `FORMO_WRITE_KEY`) |
|
|
442
478
|
|
|
443
479
|
```bash
|
|
444
480
|
export FORMO_WRITE_KEY=formo_write_key_xxx
|
|
445
481
|
formo events ingest --event '{"type":"track","channel":"cli","version":"1","anonymous_id":"anon_123","event":"CLI Test","context":{},"properties":{},"original_timestamp":"2026-04-27T23:05:38.000Z","sent_at":"2026-04-27T23:05:42.000Z","message_id":"cli-test-1"}'
|
|
482
|
+
formo events ingest --events '[{"type":"track","event":"First"},{"type":"track","event":"Second"}]'
|
|
446
483
|
```
|
|
447
484
|
|
|
448
485
|
---
|
|
449
486
|
|
|
450
487
|
## FilterCondition reference
|
|
451
488
|
|
|
452
|
-
`profiles search --
|
|
489
|
+
`profiles search --filters` accepts a JSON array of canonical filter objects:
|
|
453
490
|
|
|
454
491
|
```json
|
|
455
492
|
[
|
|
456
493
|
{ "field": "users.net_worth_usd", "op": "gt", "value": 10000 },
|
|
457
|
-
{ "field": "chains.
|
|
494
|
+
{ "field": "chains.balance", "op": "gte", "value": 1000, "chain_id": "1" }
|
|
458
495
|
]
|
|
459
496
|
```
|
|
460
497
|
|
|
461
|
-
> **The `field` must be a
|
|
498
|
+
> **The `field` must be a canonical path.** A bare name like `net_worth_usd` is
|
|
462
499
|
> silently ignored by the API (no error, no filtering — the search returns
|
|
463
|
-
> everything).
|
|
500
|
+
> everything). Resource identity goes in the named qualifiers below, never in
|
|
501
|
+
> the field path — identifier-in-path fields such as `chains.1.balance` are
|
|
502
|
+
> rejected with a `400`.
|
|
464
503
|
|
|
465
504
|
| Field | Type | Description |
|
|
466
505
|
|---|---|---|
|
|
467
|
-
| `field` | `string` |
|
|
468
|
-
| `op` | `string` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`
|
|
469
|
-
| `value` | `any` | Value to compare against |
|
|
470
|
-
| `
|
|
471
|
-
| `
|
|
506
|
+
| `field` | `string` | `users.{attribute}` or one of the four resource paths (see below) |
|
|
507
|
+
| `op` | `string` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `startsWith`, `endsWith`, `notEmpty` / `isEmpty` (value-less existence checks). Support is per field — see [operator support](#operator-support). Long-form spellings (`equals`, `greater`, `includes`, …) are retired — the API rejects them with a `400` naming the token |
|
|
508
|
+
| `value` | `any` | Value to compare against; must be a number on the `.balance` fields |
|
|
509
|
+
| `chain_id` | `string` | _(optional on any resource filter)_ restrict to one chain; omit to match any |
|
|
510
|
+
| `app_id` | `string` | _(required by `apps.balance`, and by `tokens.balance` with `scope: protocol`)_ e.g. `aave-v3` |
|
|
511
|
+
| `token_address` | `string` | _(required by `tokens.balance`)_ |
|
|
512
|
+
| `tag_id` | `string` | _(required by `labels.value`)_ e.g. `coinbase.verified_account` |
|
|
513
|
+
| `scope` | `string` | _(required by `tokens.balance`)_ `any` or `protocol` |
|
|
514
|
+
|
|
515
|
+
| Field | Required qualifiers | Example |
|
|
516
|
+
|---|---|---|
|
|
517
|
+
| `users.{attribute}` | none | `{"field":"users.net_worth_usd","op":"gt","value":10000}` |
|
|
518
|
+
| `chains.balance` | none (`chain_id` optional) | `{"field":"chains.balance","op":"gte","value":1000,"chain_id":"1"}` |
|
|
519
|
+
| `apps.balance` | `app_id` | `{"field":"apps.balance","op":"gt","value":500,"app_id":"uniswap-v3"}` |
|
|
520
|
+
| `tokens.balance` | `token_address`, `scope` | `{"field":"tokens.balance","op":"gt","value":0,"token_address":"0xA0b8…48","scope":"any"}` |
|
|
521
|
+
| `labels.value` | `tag_id` | `{"field":"labels.value","op":"eq","value":"true","tag_id":"coinbase.verified_account"}` |
|
|
472
522
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
| `tokens.` | `tokens.0xA0b8…48.balance` |
|
|
479
|
-
| `labels.` | `labels.coinbase.verified_account` |
|
|
523
|
+
User attributes for `users.{attribute}`: `net_worth_usd`, `volume`, `revenue`,
|
|
524
|
+
`points`, `device`, `location`, `lifecycle`, `ens`, `farcaster`, and the other
|
|
525
|
+
social handles.
|
|
526
|
+
|
|
527
|
+
### Operator support
|
|
480
528
|
|
|
481
|
-
|
|
529
|
+
The canonical vocabulary is shared, but each field implements a subset. The API
|
|
530
|
+
rejects an unsupported pairing with a `400`:
|
|
531
|
+
|
|
532
|
+
| Field class | Supported operators |
|
|
533
|
+
|---|---|
|
|
534
|
+
| `chains.balance`, `apps.balance`, `tokens.balance` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` (value must be a JSON number) |
|
|
535
|
+
| `labels.value` | comparison operators plus `contains` (case-insensitive) |
|
|
536
|
+
| Numeric profile metrics (`users.net_worth_usd`, `users.volume`, `users.revenue`, `users.points`) | comparison operators |
|
|
537
|
+
| Routable string attributes (`users.device`, `users.os`, `users.referrer`, `users.utm_*`, `users.click_id`, and the `first_*`/`last_*` attribution variants) | full vocabulary; `contains`/`startsWith`/`endsWith` match case-sensitively |
|
|
538
|
+
| Social fields (`users.twitter`, `users.email`, `users.farcaster`, …) | `contains` (case-insensitive) and `notEmpty`; `startsWith`/`endsWith`/`isEmpty` are rejected |
|
|
539
|
+
| `users.paid_source` (and `first_`/`last_` variants) | `eq`, `neq`, `in`, `nin`, `notEmpty`, `isEmpty` — it is a fixed ad-network enum |
|
|
540
|
+
| `users.lifecycle` | `eq` (one stage) and `in` (a list of stages) |
|
|
541
|
+
|
|
542
|
+
Combine multiple filters with `--logic and` (default) or `--logic or`.
|
|
482
543
|
|
|
483
544
|
---
|
|
484
545
|
|
|
@@ -1,36 +1,28 @@
|
|
|
1
1
|
import { Cli } from 'incur';
|
|
2
|
+
import { type PaginationOptions } from '../lib/pagination';
|
|
3
|
+
export type { PaginationOptions };
|
|
2
4
|
export declare const alerts: Cli.Cli<{}, undefined, undefined>;
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
export declare function listAlertsRun(options?: PaginationOptions): Promise<import("axios").AxiosResponse<any, any, {}>>;
|
|
8
|
-
export declare function getAlertRun(alertId: string): Promise<import("axios").AxiosResponse<any, any, {}>>;
|
|
9
|
-
export interface CreateAlertOptions {
|
|
10
|
-
name: string;
|
|
11
|
-
triggerType: 'event' | 'user' | string;
|
|
12
|
-
triggerFilters?: string;
|
|
13
|
-
recipient?: string;
|
|
14
|
-
secret?: string;
|
|
15
|
-
slackPropertyKeys?: string;
|
|
16
|
-
}
|
|
17
|
-
export declare function buildAlertBody(options: CreateAlertOptions | UpdateAlertOptions): Record<string, unknown>;
|
|
18
|
-
export declare function createAlertRun(options: CreateAlertOptions): Promise<import("axios").AxiosResponse<any, any, {}>>;
|
|
19
|
-
export interface UpdateAlertOptions {
|
|
5
|
+
export declare function listAlertsRun(options?: PaginationOptions): Promise<unknown>;
|
|
6
|
+
export declare function getAlertRun(alertId: string): Promise<unknown>;
|
|
7
|
+
export interface AlertBodyOptions {
|
|
20
8
|
name: string;
|
|
21
|
-
triggerType:
|
|
9
|
+
triggerType: string;
|
|
22
10
|
triggerFilters?: string;
|
|
23
11
|
recipient?: string;
|
|
24
12
|
secret?: string;
|
|
25
13
|
slackPropertyKeys?: string;
|
|
26
14
|
}
|
|
27
|
-
export
|
|
28
|
-
export
|
|
29
|
-
export declare function
|
|
15
|
+
export type CreateAlertOptions = AlertBodyOptions;
|
|
16
|
+
export type UpdateAlertOptions = AlertBodyOptions;
|
|
17
|
+
export declare function buildAlertBody(options: AlertBodyOptions): Record<string, unknown>;
|
|
18
|
+
export declare function createAlertRun(options: CreateAlertOptions): Promise<unknown>;
|
|
19
|
+
export declare function updateAlertRun(alertId: string, options: AlertBodyOptions): Promise<unknown>;
|
|
20
|
+
export declare function deleteAlertRun(alertId: string): Promise<unknown>;
|
|
21
|
+
export declare function toggleAlertRun(alertId: string, status: string): Promise<unknown>;
|
|
30
22
|
export interface TestAlertOptions {
|
|
31
23
|
sampleEvent?: string;
|
|
32
24
|
sampleUser?: string;
|
|
33
25
|
recipientOverrides?: string;
|
|
34
26
|
}
|
|
35
27
|
export declare function buildTestAlertBody(options: TestAlertOptions): Record<string, unknown> | undefined;
|
|
36
|
-
export declare function testAlertRun(alertId: string, options?: TestAlertOptions): Promise<
|
|
28
|
+
export declare function testAlertRun(alertId: string, options?: TestAlertOptions): Promise<unknown>;
|
package/dist/commands/alerts.js
CHANGED
|
@@ -12,29 +12,21 @@ exports.buildTestAlertBody = buildTestAlertBody;
|
|
|
12
12
|
exports.testAlertRun = testAlertRun;
|
|
13
13
|
const incur_1 = require("incur");
|
|
14
14
|
const client_1 = require("../lib/client");
|
|
15
|
+
const filters_1 = require("../lib/filters");
|
|
15
16
|
const json_1 = require("../lib/json");
|
|
17
|
+
const pagination_1 = require("../lib/pagination");
|
|
16
18
|
exports.alerts = incur_1.Cli.create('alerts', {
|
|
17
19
|
description: 'Project alert commands — create, list, update, and delete alerts',
|
|
18
20
|
});
|
|
19
|
-
|
|
20
|
-
const params = {};
|
|
21
|
-
if (options.page !== undefined)
|
|
22
|
-
params.page = options.page;
|
|
23
|
-
if (options.size !== undefined)
|
|
24
|
-
params.size = options.size;
|
|
25
|
-
return params;
|
|
26
|
-
}
|
|
21
|
+
// ── List alerts ──
|
|
27
22
|
function listAlertsRun(options = {}) {
|
|
28
23
|
(0, client_1.requireApiKey)();
|
|
29
24
|
const client = (0, client_1.createClient)();
|
|
30
|
-
return client.get('/v0/alerts/', { params: buildPaginationParams(options) });
|
|
25
|
+
return client.get('/v0/alerts/', { params: (0, pagination_1.buildPaginationParams)(options) });
|
|
31
26
|
}
|
|
32
27
|
exports.alerts.command('list', {
|
|
33
28
|
description: 'List all alerts for the project',
|
|
34
|
-
options: incur_1.z.object(
|
|
35
|
-
page: incur_1.z.coerce.number().optional().describe('Page number (1-indexed, default 1)'),
|
|
36
|
-
size: incur_1.z.coerce.number().optional().describe('Page size (default 100, max 200)'),
|
|
37
|
-
}),
|
|
29
|
+
options: incur_1.z.object(pagination_1.paginationOptionsSchema),
|
|
38
30
|
examples: [{ description: 'List all project alerts' }],
|
|
39
31
|
hint: 'Requires alerts:read scope on your API key.',
|
|
40
32
|
run({ options }) {
|
|
@@ -60,6 +52,24 @@ exports.alerts.command('get', {
|
|
|
60
52
|
return getAlertRun(args.alertId);
|
|
61
53
|
},
|
|
62
54
|
});
|
|
55
|
+
// Shared option fragment for `create` and `update` (same PUT/POST body).
|
|
56
|
+
const alertBodyOptionsSchema = {
|
|
57
|
+
name: incur_1.z.string().describe('Alert name'),
|
|
58
|
+
triggerType: incur_1.z.enum(['event', 'user']).describe('Trigger type'),
|
|
59
|
+
triggerFilters: incur_1.z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('JSON array of trigger filter objects'),
|
|
63
|
+
recipient: incur_1.z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('JSON array of recipient objects'),
|
|
67
|
+
secret: incur_1.z.string().optional().describe('Webhook secret for the alert'),
|
|
68
|
+
slackPropertyKeys: incur_1.z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('JSON array of event/user property keys to include in Slack alerts'),
|
|
72
|
+
};
|
|
63
73
|
function buildAlertBody(options) {
|
|
64
74
|
const body = {
|
|
65
75
|
name: options.name,
|
|
@@ -67,7 +77,27 @@ function buildAlertBody(options) {
|
|
|
67
77
|
trigger_filters: [],
|
|
68
78
|
};
|
|
69
79
|
if (options.triggerFilters) {
|
|
70
|
-
|
|
80
|
+
const triggerFilters = (0, json_1.parseJsonArray)(options.triggerFilters, '--trigger-filters');
|
|
81
|
+
for (const filter of triggerFilters) {
|
|
82
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
|
|
83
|
+
throw new Error('--trigger-filters must be a JSON array of {field, op, value} objects');
|
|
84
|
+
}
|
|
85
|
+
const record = filter;
|
|
86
|
+
if (typeof record.field !== 'string' ||
|
|
87
|
+
record.field.length === 0 ||
|
|
88
|
+
typeof record.value !== 'string') {
|
|
89
|
+
throw new Error('--trigger-filters: each entry requires string "field" and "value"');
|
|
90
|
+
}
|
|
91
|
+
if (record.op !== undefined &&
|
|
92
|
+
record.op !== '' &&
|
|
93
|
+
!(0, filters_1.isCanonicalFilterOperator)(record.op)) {
|
|
94
|
+
throw new Error('--trigger-filters: "op" must use the canonical filter vocabulary');
|
|
95
|
+
}
|
|
96
|
+
if (Object.keys(record).some((key) => !['field', 'op', 'value', 'numericThreshold'].includes(key))) {
|
|
97
|
+
throw new Error('--trigger-filters entries may only contain field, op, value, and numericThreshold');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
body.trigger_filters = triggerFilters;
|
|
71
101
|
}
|
|
72
102
|
if (options.recipient) {
|
|
73
103
|
body.recipient = (0, json_1.parseJsonArray)(options.recipient, '--recipient');
|
|
@@ -87,23 +117,7 @@ function createAlertRun(options) {
|
|
|
87
117
|
}
|
|
88
118
|
exports.alerts.command('create', {
|
|
89
119
|
description: 'Create a new project alert',
|
|
90
|
-
options: incur_1.z.object(
|
|
91
|
-
name: incur_1.z.string().describe('Alert name'),
|
|
92
|
-
triggerType: incur_1.z.enum(['event', 'user']).describe('Trigger type'),
|
|
93
|
-
triggerFilters: incur_1.z
|
|
94
|
-
.string()
|
|
95
|
-
.optional()
|
|
96
|
-
.describe('JSON array of trigger filter objects'),
|
|
97
|
-
recipient: incur_1.z
|
|
98
|
-
.string()
|
|
99
|
-
.optional()
|
|
100
|
-
.describe('JSON array of recipient objects'),
|
|
101
|
-
secret: incur_1.z.string().optional().describe('Webhook secret for the alert'),
|
|
102
|
-
slackPropertyKeys: incur_1.z
|
|
103
|
-
.string()
|
|
104
|
-
.optional()
|
|
105
|
-
.describe('JSON array of event/user property keys to include in Slack alerts'),
|
|
106
|
-
}),
|
|
120
|
+
options: incur_1.z.object(alertBodyOptionsSchema),
|
|
107
121
|
examples: [
|
|
108
122
|
{
|
|
109
123
|
options: { name: 'High value tx', triggerType: 'event' },
|
|
@@ -115,33 +129,18 @@ exports.alerts.command('create', {
|
|
|
115
129
|
return createAlertRun(options);
|
|
116
130
|
},
|
|
117
131
|
});
|
|
132
|
+
// ── Update an alert ──
|
|
118
133
|
function updateAlertRun(alertId, options) {
|
|
119
134
|
(0, client_1.requireApiKey)();
|
|
120
135
|
const client = (0, client_1.createClient)();
|
|
121
136
|
return client.put(`/v0/alerts/${encodeURIComponent(alertId)}`, buildAlertBody(options));
|
|
122
137
|
}
|
|
123
138
|
exports.alerts.command('update', {
|
|
124
|
-
description: 'Update an existing alert',
|
|
139
|
+
description: 'Update an existing alert (full replace — omitted options reset to defaults)',
|
|
125
140
|
args: incur_1.z.object({
|
|
126
141
|
alertId: incur_1.z.string().describe('Alert ID to update'),
|
|
127
142
|
}),
|
|
128
|
-
options: incur_1.z.object(
|
|
129
|
-
name: incur_1.z.string().describe('Alert name'),
|
|
130
|
-
triggerType: incur_1.z.enum(['event', 'user']).describe('Trigger type'),
|
|
131
|
-
triggerFilters: incur_1.z
|
|
132
|
-
.string()
|
|
133
|
-
.optional()
|
|
134
|
-
.describe('JSON array of trigger filter objects'),
|
|
135
|
-
recipient: incur_1.z
|
|
136
|
-
.string()
|
|
137
|
-
.optional()
|
|
138
|
-
.describe('JSON array of recipient objects'),
|
|
139
|
-
secret: incur_1.z.string().optional().describe('Webhook secret for the alert'),
|
|
140
|
-
slackPropertyKeys: incur_1.z
|
|
141
|
-
.string()
|
|
142
|
-
.optional()
|
|
143
|
-
.describe('JSON array of event/user property keys to include in Slack alerts'),
|
|
144
|
-
}),
|
|
143
|
+
options: incur_1.z.object(alertBodyOptionsSchema),
|
|
145
144
|
examples: [
|
|
146
145
|
{
|
|
147
146
|
args: { alertId: 'alert_abc123' },
|
|
@@ -22,4 +22,4 @@ export interface AnalyticsOptions {
|
|
|
22
22
|
* Exported for unit testing.
|
|
23
23
|
*/
|
|
24
24
|
export declare function buildAnalyticsParams(options: AnalyticsOptions): Record<string, string | number | boolean>;
|
|
25
|
-
export declare function runAnalytics(pipe: string, options: AnalyticsOptions): Promise<
|
|
25
|
+
export declare function runAnalytics(pipe: string, options: AnalyticsOptions): Promise<unknown>;
|
|
@@ -5,6 +5,8 @@ exports.buildAnalyticsParams = buildAnalyticsParams;
|
|
|
5
5
|
exports.runAnalytics = runAnalytics;
|
|
6
6
|
const incur_1 = require("incur");
|
|
7
7
|
const client_1 = require("../lib/client");
|
|
8
|
+
const filters_1 = require("../lib/filters");
|
|
9
|
+
const json_1 = require("../lib/json");
|
|
8
10
|
exports.analytics = incur_1.Cli.create('analytics', {
|
|
9
11
|
description: 'Pre-built analytics query commands — KPIs, funnels, retention, revenue, and top-N breakdowns',
|
|
10
12
|
});
|
|
@@ -43,6 +45,52 @@ const RESERVED_PARAM_KEYS = new Set([
|
|
|
43
45
|
'dateTo',
|
|
44
46
|
'filters',
|
|
45
47
|
]);
|
|
48
|
+
const ANALYTICS_FILTER_KEYS = new Set(['field', 'op', 'value', 'filters']);
|
|
49
|
+
const ANALYTICS_NESTED_FILTER_KEYS = new Set(['field', 'op', 'value']);
|
|
50
|
+
function validateAnalyticsFilter(filter, path, allowNested) {
|
|
51
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
|
|
52
|
+
throw new Error(`${path} must be a {field, op, value} object`);
|
|
53
|
+
}
|
|
54
|
+
const record = filter;
|
|
55
|
+
if (!allowNested && record.filters !== undefined) {
|
|
56
|
+
throw new Error(`${path}.filters must be a one-level array of leaf filters`);
|
|
57
|
+
}
|
|
58
|
+
const allowedKeys = allowNested
|
|
59
|
+
? ANALYTICS_FILTER_KEYS
|
|
60
|
+
: ANALYTICS_NESTED_FILTER_KEYS;
|
|
61
|
+
if (Object.keys(record).some((key) => !allowedKeys.has(key))) {
|
|
62
|
+
throw new Error(`${path} may only contain field, op, value${allowNested ? ', and filters' : ''}`);
|
|
63
|
+
}
|
|
64
|
+
if (typeof record.field !== 'string' || record.field.length === 0) {
|
|
65
|
+
throw new Error(`${path} requires a non-empty string "field"`);
|
|
66
|
+
}
|
|
67
|
+
if (!(0, filters_1.isCanonicalFilterOperator)(record.op)) {
|
|
68
|
+
throw new Error(`${path} requires a canonical "op"`);
|
|
69
|
+
}
|
|
70
|
+
if ((0, filters_1.isEmptyMembershipArray)(record.value)) {
|
|
71
|
+
throw new Error(`${path}: membership arrays cannot be empty`);
|
|
72
|
+
}
|
|
73
|
+
if (!(0, filters_1.isValuelessFilterOperator)(record.op) &&
|
|
74
|
+
(record.value === undefined ||
|
|
75
|
+
record.value === null ||
|
|
76
|
+
!(0, filters_1.isCanonicalFilterValue)(record.value))) {
|
|
77
|
+
throw new Error(`${path}: "value" is required for every operator except notEmpty/isEmpty`);
|
|
78
|
+
}
|
|
79
|
+
if (record.value !== undefined &&
|
|
80
|
+
record.value !== null &&
|
|
81
|
+
!(0, filters_1.isCanonicalFilterValue)(record.value)) {
|
|
82
|
+
throw new Error(`${path}: "value" must be a string, number, boolean, or string/number array`);
|
|
83
|
+
}
|
|
84
|
+
if ((0, filters_1.hasTinybirdMembershipDelimiter)(record.value)) {
|
|
85
|
+
throw new Error(`${path}: array string members cannot contain "|" because it is the Tinybird membership separator`);
|
|
86
|
+
}
|
|
87
|
+
if (record.filters !== undefined) {
|
|
88
|
+
if (!allowNested || !Array.isArray(record.filters)) {
|
|
89
|
+
throw new Error(`${path}.filters must be a one-level array of leaf filters`);
|
|
90
|
+
}
|
|
91
|
+
record.filters.forEach((nested, index) => validateAnalyticsFilter(nested, `${path}.filters[${index}]`, false));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
46
94
|
/**
|
|
47
95
|
* Build the query-string params for an analytics pipe request.
|
|
48
96
|
*
|
|
@@ -62,16 +110,7 @@ function buildAnalyticsParams(options) {
|
|
|
62
110
|
const out = {};
|
|
63
111
|
// --params first, so the validated flags below override it.
|
|
64
112
|
if (options.params) {
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
parsed = JSON.parse(options.params);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
throw new Error('--params must be a valid JSON object');
|
|
71
|
-
}
|
|
72
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
73
|
-
throw new Error('--params must be a valid JSON object');
|
|
74
|
-
}
|
|
113
|
+
const parsed = (0, json_1.parseJsonObject)(options.params, '--params');
|
|
75
114
|
for (const [key, value] of Object.entries(parsed)) {
|
|
76
115
|
if (RESERVED_PARAM_KEYS.has(key)) {
|
|
77
116
|
throw new Error(`--params may not set "${key}" — use the --date-from/--date-to/--filters flags instead`);
|
|
@@ -101,6 +140,7 @@ function buildAnalyticsParams(options) {
|
|
|
101
140
|
if (!Array.isArray(parsed)) {
|
|
102
141
|
throw new Error('--filters must be a valid JSON array of {field,op,value} objects');
|
|
103
142
|
}
|
|
143
|
+
parsed.forEach((filter, index) => validateAnalyticsFilter(filter, `--filters[${index}]`, true));
|
|
104
144
|
out.filters = JSON.stringify(parsed);
|
|
105
145
|
}
|
|
106
146
|
return out;
|
|
@@ -123,7 +163,7 @@ const sharedOptions = incur_1.z.object({
|
|
|
123
163
|
.string()
|
|
124
164
|
.optional()
|
|
125
165
|
.describe('JSON array of filter conditions: [{"field","op","value"}]. ' +
|
|
126
|
-
'Use op "in"/"nin" with
|
|
166
|
+
'Use op "in"/"nin" with an array value (e.g. ["chrome","firefox"]); pipe-delimited strings are also accepted. Array string members cannot contain "|".'),
|
|
127
167
|
params: incur_1.z
|
|
128
168
|
.string()
|
|
129
169
|
.optional()
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { Cli } from 'incur';
|
|
2
|
+
import { type PaginationOptions } from '../lib/pagination';
|
|
3
|
+
export type { PaginationOptions };
|
|
2
4
|
export declare const boards: Cli.Cli<{}, undefined, undefined>;
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
size?: number;
|
|
6
|
-
}
|
|
7
|
-
export declare function listBoardsRun(options?: PaginationOptions): Promise<import("axios").AxiosResponse<any, any, {}>>;
|
|
8
|
-
export declare function getBoardRun(boardId: string): Promise<import("axios").AxiosResponse<any, any, {}>>;
|
|
5
|
+
export declare function listBoardsRun(options?: PaginationOptions): Promise<unknown>;
|
|
6
|
+
export declare function getBoardRun(boardId: string): Promise<unknown>;
|
|
9
7
|
export interface CreateBoardOptions {
|
|
10
8
|
title?: string;
|
|
11
9
|
name?: string;
|
|
@@ -13,12 +11,12 @@ export interface CreateBoardOptions {
|
|
|
13
11
|
isPublic?: boolean;
|
|
14
12
|
}
|
|
15
13
|
export declare function buildBoardBody(options: CreateBoardOptions | UpdateBoardOptions): Record<string, unknown>;
|
|
16
|
-
export declare function createBoardRun(options: CreateBoardOptions): Promise<
|
|
14
|
+
export declare function createBoardRun(options: CreateBoardOptions): Promise<unknown>;
|
|
17
15
|
export interface UpdateBoardOptions {
|
|
18
16
|
title?: string;
|
|
19
17
|
name?: string;
|
|
20
18
|
description?: string;
|
|
21
19
|
isPublic?: boolean;
|
|
22
20
|
}
|
|
23
|
-
export declare function updateBoardRun(boardId: string, options: UpdateBoardOptions): Promise<
|
|
24
|
-
export declare function deleteBoardRun(boardId: string): Promise<
|
|
21
|
+
export declare function updateBoardRun(boardId: string, options: UpdateBoardOptions): Promise<unknown>;
|
|
22
|
+
export declare function deleteBoardRun(boardId: string): Promise<unknown>;
|
package/dist/commands/boards.js
CHANGED
|
@@ -9,29 +9,19 @@ exports.updateBoardRun = updateBoardRun;
|
|
|
9
9
|
exports.deleteBoardRun = deleteBoardRun;
|
|
10
10
|
const incur_1 = require("incur");
|
|
11
11
|
const client_1 = require("../lib/client");
|
|
12
|
+
const pagination_1 = require("../lib/pagination");
|
|
12
13
|
exports.boards = incur_1.Cli.create('boards', {
|
|
13
14
|
description: 'Dashboard board commands — create, list, update, and delete boards',
|
|
14
15
|
});
|
|
15
|
-
function buildPaginationParams(options = {}) {
|
|
16
|
-
const params = {};
|
|
17
|
-
if (options.page !== undefined)
|
|
18
|
-
params.page = options.page;
|
|
19
|
-
if (options.size !== undefined)
|
|
20
|
-
params.size = options.size;
|
|
21
|
-
return params;
|
|
22
|
-
}
|
|
23
16
|
// ── List boards ──
|
|
24
17
|
function listBoardsRun(options = {}) {
|
|
25
18
|
(0, client_1.requireApiKey)();
|
|
26
19
|
const client = (0, client_1.createClient)();
|
|
27
|
-
return client.get('/v0/boards/', { params: buildPaginationParams(options) });
|
|
20
|
+
return client.get('/v0/boards/', { params: (0, pagination_1.buildPaginationParams)(options) });
|
|
28
21
|
}
|
|
29
22
|
exports.boards.command('list', {
|
|
30
23
|
description: 'List all boards for the project',
|
|
31
|
-
options: incur_1.z.object(
|
|
32
|
-
page: incur_1.z.coerce.number().optional().describe('Page number (1-indexed, default 1)'),
|
|
33
|
-
size: incur_1.z.coerce.number().optional().describe('Page size (default 100, max 200)'),
|
|
34
|
-
}),
|
|
24
|
+
options: incur_1.z.object(pagination_1.paginationOptionsSchema),
|
|
35
25
|
examples: [{ description: 'List all dashboard boards' }],
|
|
36
26
|
hint: 'Requires boards:read scope on your API key.',
|
|
37
27
|
run({ options }) {
|
|
@@ -62,7 +52,7 @@ function buildBoardBody(options) {
|
|
|
62
52
|
const body = {};
|
|
63
53
|
if (title !== undefined) {
|
|
64
54
|
if (!title)
|
|
65
|
-
throw new Error('--title must not be empty');
|
|
55
|
+
throw new Error('--title (or its deprecated alias --name) must not be empty');
|
|
66
56
|
body.title = title;
|
|
67
57
|
}
|
|
68
58
|
if (options.description !== undefined) {
|