@erdoai/cli 0.54.1 → 0.55.1

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.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +78 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -87,6 +87,21 @@ erdo --org 2200-brickell datasets query 2200-brickell.page-events \
87
87
  "which pages get the most views, and how many convert?"
88
88
  ```
89
89
 
90
+ **A file dataset keeps the versions it overwrote.** Its contents are stored, and
91
+ each write that replaces them keeps the version it replaced, so an import that
92
+ landed bad rows over good ones has not destroyed them. List the versions, then
93
+ read one back with `fetch` — the same SQL you would run against the live table.
94
+
95
+ ```bash
96
+ erdo datasets revisions acme.leads # live version first, then superseded
97
+ erdo datasets fetch acme.leads --revision <id> \
98
+ --sql "SELECT * FROM data" # query those older contents
99
+ ```
100
+
101
+ The history is read-only: nothing here rolls a dataset back. Restoring means
102
+ fetching the rows out of the old revision and writing them in again through the
103
+ normal write path.
104
+
90
105
  ```bash
91
106
  erdo datasets list # slug, type, status, class, name
92
107
  erdo datasets upload ./leads.csv # create a dataset from a file
package/dist/index.js CHANGED
@@ -713,6 +713,9 @@ var ErdoClient = class {
713
713
  // named saved filters. filter_names are additive on top of the dataset's default
714
714
  // filters — they narrow the result, never bypass a default. An unknown name is
715
715
  // rejected server-side with the available names.
716
+ //
717
+ // revision_id runs the same read against a superseded storage revision instead
718
+ // of the dataset's live contents — the ids come from listDatasetRevisions.
716
719
  fetchDatasetContents(slug, opts) {
717
720
  return this.request(
718
721
  "POST",
@@ -720,10 +723,21 @@ var ErdoClient = class {
720
723
  {
721
724
  sql_query: opts.sql_query,
722
725
  limit: opts.limit,
723
- filter_names: opts.filter_names
726
+ filter_names: opts.filter_names,
727
+ revision_id: opts.revision_id
724
728
  }
725
729
  );
726
730
  }
731
+ // A file dataset's storage is versioned: every write that replaces its contents
732
+ // keeps the version it replaced, and this lists them — the live (primary) one
733
+ // first, then superseded ones newest first. superseded_at is the moment a newer
734
+ // version took over, and is null both on the live revision and on a stored file
735
+ // that was never live (an upload merged into an existing table); file_type may be
736
+ // absent when the stored file's type was never recorded. Read one back by passing
737
+ // its id as revision_id to fetchDatasetContents.
738
+ listDatasetRevisions(slug) {
739
+ return this.request("GET", `/v1/datasets/${encodeURIComponent(slug)}/revisions`);
740
+ }
727
741
  uploadDatasetFile(body) {
728
742
  return this.request("POST", "/v1/datasets-upload", body);
729
743
  }
@@ -769,6 +783,12 @@ var ErdoClient = class {
769
783
  connectIntegration(body) {
770
784
  return this.request("POST", "/v1/integrations-connect", body);
771
785
  }
786
+ // `connected` reports whether the connection exists, which is not the same as
787
+ // whether it works: a key retired at the provider leaves status 'connected'
788
+ // here, because the connector platform keeps signing calls with it and simply
789
+ // relays the provider's refusal. `provider_auth_failed_at` is when that last
790
+ // happened, and the `note` says so in words — a connection carrying one needs
791
+ // reauthorizing even though nothing else looks wrong.
772
792
  checkIntegrationConnection(app) {
773
793
  return this.request("GET", `/v1/integrations-connect/${encodeURIComponent(app)}`);
774
794
  }
@@ -3464,14 +3484,57 @@ datasetsCmd.command("fetch <slug>").description(
3464
3484
  "opt into a saved filter by name (repeatable); additive on top of the dataset's default filters (see: erdo datasets filter list <slug>)",
3465
3485
  (v, acc) => acc.concat(v),
3466
3486
  []
3467
- ).action(async (slug, opts) => {
3487
+ ).option(
3488
+ "--revision <id>",
3489
+ "read a superseded storage revision instead of the live table \u2014 the same SQL runs against the contents this id names, so a version that was overwritten is still queryable (see: erdo datasets revisions <slug> for the ids)"
3490
+ ).action(
3491
+ async (slug, opts) => {
3492
+ try {
3493
+ print(
3494
+ await new ErdoClient().fetchDatasetContents(slug, {
3495
+ sql_query: opts.sql,
3496
+ limit: opts.limit,
3497
+ filter_names: opts.filter.length ? opts.filter : void 0,
3498
+ revision_id: opts.revision
3499
+ })
3500
+ );
3501
+ } catch (e) {
3502
+ fail(e);
3503
+ }
3504
+ }
3505
+ );
3506
+ datasetsCmd.command("revisions <slug>").description(
3507
+ "List the stored versions of a file dataset's contents. Every write that replaces a file dataset keeps the version it replaced, so an import that overwrote good rows with bad ones has not lost them: this lists the live version first and the superseded ones newest first, and each row's id is what `datasets fetch <slug> --revision <id>` reads \u2014 the same SQL, run against those older contents. History is read-only and nothing here rolls a dataset back; restoring means fetching the rows out of the old revision and writing them in again through the normal write path."
3508
+ ).option("--json", "print the raw JSON result instead of a table").action(async (slug, opts) => {
3468
3509
  try {
3469
- print(
3470
- await new ErdoClient().fetchDatasetContents(slug, {
3471
- sql_query: opts.sql,
3472
- limit: opts.limit,
3473
- filter_names: opts.filter.length ? opts.filter : void 0
3474
- })
3510
+ const res = await new ErdoClient().listDatasetRevisions(slug);
3511
+ if (opts.json) {
3512
+ print(res);
3513
+ return;
3514
+ }
3515
+ const revisions = res.revisions ?? [];
3516
+ if (revisions.length === 0) {
3517
+ console.log(
3518
+ `${res.dataset_slug} has no stored revisions \u2014 versions are kept for file datasets, whose contents Erdo stores and replaces on each write. A database or warehouse dataset is queried live at its source and keeps no history here.`
3519
+ );
3520
+ return;
3521
+ }
3522
+ printAlignedTable(
3523
+ ["id", "created", "superseded", "live", "name", "type"],
3524
+ revisions.map((r) => [
3525
+ r.id,
3526
+ r.created_at,
3527
+ r.superseded_at ?? "",
3528
+ r.is_primary ? "yes" : "",
3529
+ r.display_name,
3530
+ // file_type is the specific answer (csv, xlsx) and item_type the general
3531
+ // one; the more specific wins when it was recorded.
3532
+ r.file_type || r.item_type
3533
+ ])
3534
+ );
3535
+ console.log(
3536
+ `
3537
+ Read one with: erdo datasets fetch ${res.dataset_slug} --revision <id> --sql "SELECT * FROM data"`
3475
3538
  );
3476
3539
  } catch (e) {
3477
3540
  fail(e);
@@ -3650,7 +3713,7 @@ integrationsCmd.command("apps [query]").description("Search connectable apps (na
3650
3713
  fail(e);
3651
3714
  }
3652
3715
  });
3653
- integrationsCmd.command("connect <app>").description("Connect an app \u2014 pass its API key or other credentials with -c, or omit them to get a browser connect URL for OAuth apps").option("-c, --credential <key=value...>", "credential field, e.g. -c api_key=\u2026 (repeatable); rejected by OAuth apps, which have no credential to pass", (v, acc) => acc.concat(v), []).option("-n, --name <name>", "display name for the connection").action(async (app, opts) => {
3716
+ integrationsCmd.command("connect <app>").description("Connect an app \u2014 pass its API key or other credentials with -c, or omit them to get a browser connect URL for OAuth apps").option("-c, --credential <key=value...>", "credential field, e.g. -c api_key=\u2026 (repeatable); rejected by OAuth apps, which have no credential to pass", (v, acc) => acc.concat(v), []).option("-n, --name <name>", "display name for the connection").option("--rotate", "replace the credentials of the app's existing connection instead of adding a second one \u2014 for a key that was rotated at the provider; requires -c").action(async (app, opts) => {
3654
3717
  try {
3655
3718
  let credentials;
3656
3719
  if (opts.credential.length) {
@@ -3661,7 +3724,12 @@ integrationsCmd.command("connect <app>").description("Connect an app \u2014 pass
3661
3724
  credentials[c.slice(0, eq)] = c.slice(eq + 1);
3662
3725
  }
3663
3726
  }
3664
- const res = await new ErdoClient().connectIntegration({ app, name: opts.name, credentials });
3727
+ const res = await new ErdoClient().connectIntegration({
3728
+ app,
3729
+ name: opts.name,
3730
+ credentials,
3731
+ rotate_credentials: opts.rotate || void 0
3732
+ });
3665
3733
  print(res);
3666
3734
  const notes = [];
3667
3735
  const org2 = formatOrg(res.organization_name, res.organization_slug);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.54.1",
3
+ "version": "0.55.1",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {