Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,65 @@ confidence quickstart # Launch the interactive setup wizard
| `confidence warehouse assignment-table create` | Create an assignment table for experiment analysis |
| `confidence warehouse crypto-key create` | Create a crypto key (Snowflake) |

### Fact Tables

| Command | Description |
| ----------------------------------------------------------------- | ----------------------------------------------- |
| `confidence data fact-tables list` | List one page of fact tables |
| `confidence data fact-tables get factTables/<id>` | Get the complete resource and lifecycle state |
| `confidence data fact-tables create --from-file fact.json [--id]` | Create one fact table from a resource JSON file |

List supports `--page-size`, `--page-token`, `--filter` (Lucene query string),
`--show-deleted`, and `--order-by` (for example `displayName:desc,createTime:asc`).
Pagination is opt-in: `--all --max-pages 10 --max-items 1000` traverses only within
those explicit bounds (maximum 1000 pages / 100000 resources). If a bound is reached,
`nextPageToken` is retained; use it to continue. Repeated tokens fail instead of looping.

Create reads the **resource itself**, with camelCase proto-JSON keys, not a
`factTable` wrapper or CLI-option keys:

```json
{
"displayName": "Purchases",
"sql": "SELECT purchased_at, user_id, amount, country FROM purchases",
"timestampColumn": { "name": "purchased_at" },
"entities": [{ "column": { "name": "user_id" }, "entity": "entities/user" }],
"measures": [{ "column": { "name": "amount" }, "unit": { "currencyCode": "USD" } }],
"dimensions": [{ "name": "country" }]
}
```

`sql`, `displayName`, `timestampColumn.name`, and at least one entity mapping are
required. Optional fields include `description`, `measures` (`column`, `displayName`,
`unit`, `declaredType`), `dimensions`, `labels` (string values), `owner`
(`identities/<id>`), `factDataDeliveredUntilTime` (RFC 3339 timestamp), and
`dataDeliveredUntilUpdateStrategyConfig` (`strategy: MANUAL|AUTOMATIC`, optional
`disableEmptyPartitions`, and at most one `automaticUpdateConfig`, `dailyUpdateConfig`,
or `hourlyUpdateConfig`). Automatic config requires `commitDelay`; it also supports
`incrementDuration` and `changeThreshold: { "value": "0.01" }`. Daily/hourly config
supports `maxZeroDuration`. Durations use protobuf strings such as `3600s`. Enums use
their symbolic proto names. Columns accept only `name` and optional boolean `repeated`.
Unknown fields, output-only lifecycle fields, and column annotations are rejected,
not silently dropped. `skipSqlPreview` and `column.type` are **not supported** by
this command: omit both and let the service validate SQL and infer column types.
Use `--id purchases` to select an ID; omit it for a server-generated ID. IDs must
start with an ASCII letter or digit and contain only letters, digits, `_`, or `-`.
Do not set `name` in create input.

These commands support `--profile`, `--json` / `--output`, and `--dry-run`.
Dry-run validates input and previews the request without authentication or HTTP.
The selected profile's token determines the Metrics API region. JSON output keeps
the complete remote resource, including state, errors, and job references. A
successful GET of a resource in `TABLE_STATE_FAILED` still exits successfully;
inspect its `state` and `error` to diagnose warehouse validation.

Create is an explicit, single write, not an approval workflow. It does not retry or
poll for activation. On a transport/5xx/unreadable-response failure (including a
malformed successful response), the write outcome may be unknown: inspect list/get
before deciding whether to retry. When `--id` is set, the error includes the qualified
resource name to look up. Responses must contain resource objects with qualified
`factTables/<id>` names; malformed get/list responses are errors, not successful output.

### SDK

| Command | Description |
Expand Down
52 changes: 52 additions & 0 deletions packages/cli/__tests__/commands/data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import yargs from 'yargs';
import { prepareAuthTokens } from '@spotify-confidence/testing/auth';
import { dataCommand } from '@commands/index.js';

function run(args: string[]) {
let output = '';
const sut = yargs(args).scriptName('confidence').command(dataCommand).help().exitProcess(false);
return sut
.parseAsync(args, (_error: Error | undefined, _argv: unknown, text: string) => {
output = text;
})
.then(() => output);
}

describe('data command group', () => {
it('is registered in the real root help alongside existing commands', () => {
using _auth = prepareAuthTokens('none');
const sut = fileURLToPath(new URL('../../bin/cli.ts', import.meta.url));

const output = execFileSync(process.execPath, ['--import', 'tsx', sut, '--help'], {
encoding: 'utf8',
env: { ...process.env, CONFIDENCE_TELEMETRY: 'false' },
});

expect(output).toContain('confidence data');
expect(output).toContain('confidence warehouse');
expect(output).toContain('confidence flags');
});

it('shows help without authentication or placeholder API subcommands', async () => {
using _auth = prepareAuthTokens('none');
const sut = run;

const output = await sut(['data', '--help']);

expect(output).toContain('confidence data <command>');
expect(output).toContain('confidence data fact-tables');
expect(output).not.toMatch(/onboard|discover|proposal|workflow/iu);
});

it('shows guidance when invoked without a subcommand', async () => {
const sut = run;
expect(await sut(['data'])).toContain('confidence data --help');
});

it('rejects unimplemented subcommands', async () => {
const sut = run;
expect(await sut(['data', 'invented'])).toContain('Unknown argument: invented');
});
});
Loading
Loading