@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.
- package/README.md +15 -0
- package/dist/index.js +78 -10
- 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
|
-
).
|
|
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
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
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({
|
|
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);
|