avo-cli 4.2.0.beta.5 → 4.3.0.beta.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 573e2761181c8f7805b67e3559693db7d95ae070b56e3dc430d1d8e8b5d15886
4
- data.tar.gz: afbde3679980b2a958d903a76f40911d69c0e484a32bf3b65eb04cb52be51909
3
+ metadata.gz: e16520471450b18019b924ef115ead5d10506a57b20604bf6c5c2e001a86ce06
4
+ data.tar.gz: 1cefc1c773b788d3a82694971d5b045440c7887faf8149b3a3d248c44de343af
5
5
  SHA512:
6
- metadata.gz: 992210ec32bc04da8d8d2d81efd26e5f5c16fe71469781ccd50ee71629d30fab56ec566be8bcd6da0b1f9d118ce6f3e6ff4ff2827caec75a8fbec7f6bea4bf94
7
- data.tar.gz: d635d45e4f5540e324ed41e3bf8a2f392d6bc9163324db8343cbab7e92267755c8ca0de791311cb8bc1ecf00c27c4d9806b015d723f4f16d38a72e688ae16acb
6
+ metadata.gz: 58ea6fc978c6505b0812271ab03a937ad3145c15121314fa14c254e671b31fa1a7173e88842ae3b0bbdd82dbd5b185fd4b3dc5fd142c23fcf2fc7056b3604827
7
+ data.tar.gz: e649adb3ca3dc4c03c703172afe49ba56bde22d5e070ab1c9e5b4a944188a168db22a398a52ba1abcd4239fec5616e79fa29677694e0a5395f2166b1c8e97701
data/README.md CHANGED
@@ -61,6 +61,18 @@ avo delete users 5 # no confirmation
61
61
  Field formats, errors and exit codes are in the
62
62
  [docs](https://docs.avohq.io/4.0/cli.html#send-the-right-value-for-a-field).
63
63
 
64
+ To describe the API to another tool, export what the token may call as an
65
+ OpenAPI 3.1 document and import the file into Swagger UI, Postman, Redocly or
66
+ a client generator:
67
+
68
+ ```sh
69
+ avo schema --format openapi > openapi.json
70
+ ```
71
+
72
+ The document is built for the token that asked: a read-only token gets one
73
+ with no create, update or delete operations. See
74
+ [OpenAPI document](https://docs.avohq.io/4.0/rest-api.html#openapi-document).
75
+
64
76
  <!-- commands -->
65
77
  * [`avo create RESOURCE`](#avo-create-resource)
66
78
  * [`avo delete RESOURCE ID`](#avo-delete-resource-id)
@@ -284,15 +296,14 @@ EXAMPLES
284
296
 
285
297
  ## `avo logout`
286
298
 
287
- Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep working
299
+ Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION set in the environment keep working
288
300
 
289
301
  ```
290
302
  USAGE
291
303
  $ avo logout
292
304
 
293
305
  DESCRIPTION
294
- Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep
295
- working
306
+ Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION set in the environment keep working
296
307
 
297
308
  EXAMPLES
298
309
  $ avo logout
@@ -305,7 +316,7 @@ List the resources the API serves, or the fields of one resource on one view
305
316
  ```
306
317
  USAGE
307
318
  $ avo schema [RESOURCE] [--api-version <value>] [--host <value>] [--token <value>] [--verbose] [--format
308
- table|json] [--view <value>]
319
+ table|json|openapi] [--view <value>]
309
320
 
310
321
  ARGUMENTS
311
322
  [RESOURCE] A resource by its route key; with none, the resources the API serves are listed
@@ -322,8 +333,8 @@ CONNECTION FLAGS
322
333
 
323
334
  OUTPUT FLAGS
324
335
  --format=<option> [default: table] Output format; `json` prints the body exactly as the server sent it, indented on a
325
- terminal
326
- <options: table|json>
336
+ terminal; `openapi` prints the OpenAPI document of what the token may call
337
+ <options: table|json|openapi>
327
338
  --verbose Log each request and response line to stderr
328
339
 
329
340
  DESCRIPTION
@@ -334,6 +345,8 @@ EXAMPLES
334
345
 
335
346
  $ avo schema --format json
336
347
 
348
+ $ avo schema --format openapi > openapi.json
349
+
337
350
  $ avo schema users
338
351
 
339
352
  $ avo schema users --view show
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Avo
4
4
  module Cli
5
- VERSION = "4.2.0.beta.5"
5
+ VERSION = "4.3.0.beta.5"
6
6
  end
7
7
  end
data/payload/README.md CHANGED
@@ -61,6 +61,18 @@ avo delete users 5 # no confirmation
61
61
  Field formats, errors and exit codes are in the
62
62
  [docs](https://docs.avohq.io/4.0/cli.html#send-the-right-value-for-a-field).
63
63
 
64
+ To describe the API to another tool, export what the token may call as an
65
+ OpenAPI 3.1 document and import the file into Swagger UI, Postman, Redocly or
66
+ a client generator:
67
+
68
+ ```sh
69
+ avo schema --format openapi > openapi.json
70
+ ```
71
+
72
+ The document is built for the token that asked: a read-only token gets one
73
+ with no create, update or delete operations. See
74
+ [OpenAPI document](https://docs.avohq.io/4.0/rest-api.html#openapi-document).
75
+
64
76
  <!-- commands -->
65
77
  * [`avo create RESOURCE`](#avo-create-resource)
66
78
  * [`avo delete RESOURCE ID`](#avo-delete-resource-id)
@@ -284,15 +296,14 @@ EXAMPLES
284
296
 
285
297
  ## `avo logout`
286
298
 
287
- Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep working
299
+ Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION set in the environment keep working
288
300
 
289
301
  ```
290
302
  USAGE
291
303
  $ avo logout
292
304
 
293
305
  DESCRIPTION
294
- Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep
295
- working
306
+ Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION set in the environment keep working
296
307
 
297
308
  EXAMPLES
298
309
  $ avo logout
@@ -305,7 +316,7 @@ List the resources the API serves, or the fields of one resource on one view
305
316
  ```
306
317
  USAGE
307
318
  $ avo schema [RESOURCE] [--api-version <value>] [--host <value>] [--token <value>] [--verbose] [--format
308
- table|json] [--view <value>]
319
+ table|json|openapi] [--view <value>]
309
320
 
310
321
  ARGUMENTS
311
322
  [RESOURCE] A resource by its route key; with none, the resources the API serves are listed
@@ -322,8 +333,8 @@ CONNECTION FLAGS
322
333
 
323
334
  OUTPUT FLAGS
324
335
  --format=<option> [default: table] Output format; `json` prints the body exactly as the server sent it, indented on a
325
- terminal
326
- <options: table|json>
336
+ terminal; `openapi` prints the OpenAPI document of what the token may call
337
+ <options: table|json|openapi>
327
338
  --verbose Log each request and response line to stderr
328
339
 
329
340
  DESCRIPTION
@@ -334,6 +345,8 @@ EXAMPLES
334
345
 
335
346
  $ avo schema --format json
336
347
 
348
+ $ avo schema --format openapi > openapi.json
349
+
337
350
  $ avo schema users
338
351
 
339
352
  $ avo schema users --view show
@@ -47,16 +47,20 @@ export class BaseCommand extends ConnectionCommand {
47
47
  options: ['table', 'json'],
48
48
  }),
49
49
  };
50
- // Answers whether it printed. The record commands call it ahead of their shape check, so json relays a body they
51
- // could not table; `schema` cannot, since its loaders check the shape as they fetch.
52
- printedJson(flags, response) {
53
- if (flags.format !== 'json')
54
- return false;
50
+ // The body as the server sent it, whatever the flag that asked for it.
51
+ printBody(response) {
55
52
  const body = terminal().tty && response.body !== undefined ? indented(response.body) : response.raw;
56
53
  // `this.log` always appends a newline, which would double the one a body ends with.
57
54
  process.stdout.write(body);
58
55
  if (!body.endsWith('\n'))
59
56
  process.stdout.write('\n');
57
+ }
58
+ // Answers whether it printed. The record commands call it ahead of their shape check, so json relays a body they
59
+ // could not table; `schema` cannot, since its loaders check the shape as they fetch.
60
+ printedJson(flags, response) {
61
+ if (flags.format !== 'json')
62
+ return false;
63
+ this.printBody(response);
60
64
  return true;
61
65
  }
62
66
  }
@@ -34,7 +34,7 @@ export default class List extends RecordCommand {
34
34
  if (this.printedJson(flags, response))
35
35
  return;
36
36
  const { pagination, records } = parseRecords(response);
37
- const layout = records.length === 0 ? undefined : await layoutTable(records, { columns: flags.fields });
37
+ const layout = records.length === 0 ? undefined : await layoutTable(records, { columns: flags.fields, overflow: 'wrap' });
38
38
  this.log(layout?.text ?? `No ${args.resource}.`);
39
39
  if (typeof pagination?.page !== 'number')
40
40
  return;
@@ -2,7 +2,7 @@ import { Command } from '@oclif/core';
2
2
  import { fileIn, remove } from '../store.js';
3
3
  // Not a `BaseCommand`: it makes no request, so the connection flags would be on `--help` doing nothing.
4
4
  export default class Logout extends Command {
5
- static description = 'Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep working';
5
+ static description = 'Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION set in the environment keep working';
6
6
  static examples = ['<%= config.bin %> logout'];
7
7
  async run() {
8
8
  // Nothing to read from it: parsing is what refuses a flag this command does not have, instead of ignoring it.
@@ -1,17 +1,9 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../base-command.js';
3
- import { CliError } from '../errors.js';
3
+ import { CliError, wrongShape } from '../errors.js';
4
4
  import { DEFAULT_VIEW, fetchResourceSchema, loadSchema } from '../schema.js';
5
5
  import { hiddenColumnsNote, layoutTable } from '../table.js';
6
- // `required: false` is what most fields carry, so printing it on every row buries the few that are `true`. A `null`
7
- // (the server could not tell) stays, and a row left with nothing prints `-` rather than `{}`. `--format json` keeps all.
8
- function shownOptions(options) {
9
- if (options?.required !== false)
10
- return options;
11
- const rest = { ...options };
12
- delete rest.required;
13
- return Object.keys(rest).length === 0 ? undefined : rest;
14
- }
6
+ import { paint, terminal } from '../terminal.js';
15
7
  // One verb, two answers: with no resource, the resources the API serves and
16
8
  // how far the token reaches on each; with one, its fields on one view.
17
9
  export default class Schema extends BaseCommand {
@@ -24,11 +16,18 @@ export default class Schema extends BaseCommand {
24
16
  static examples = [
25
17
  '<%= config.bin %> schema',
26
18
  '<%= config.bin %> schema --format json',
19
+ '<%= config.bin %> schema --format openapi > openapi.json',
27
20
  '<%= config.bin %> schema users',
28
21
  '<%= config.bin %> schema users --view show',
29
22
  '<%= config.bin %> schema --host http://localhost:3000/api --token avo_...',
30
23
  ];
31
24
  static flags = {
25
+ // `openapi` is this verb's alone: the document describes the whole API, so no record command could print it.
26
+ format: {
27
+ ...BaseCommand.baseFlags.format,
28
+ description: `${BaseCommand.baseFlags.format.description}; \`openapi\` prints the OpenAPI document of what the token may call`,
29
+ options: ['table', 'json', 'openapi'],
30
+ },
32
31
  // No `options`: the view names are the API's to know, not this flag's.
33
32
  view: Flags.string({
34
33
  description: `Which view's fields: create/update list what a write may send, with their field_options; index/show what a record reads back (default: ${DEFAULT_VIEW})`,
@@ -38,11 +37,23 @@ export default class Schema extends BaseCommand {
38
37
  async run() {
39
38
  const { args, flags } = await this.parse(Schema);
40
39
  const { resource } = args;
41
- // A usage error, so it comes before the host and token are looked for.
40
+ // Usage errors, so they come before the host and token are looked for.
41
+ if (flags.format === 'openapi' && (resource !== undefined || flags.view)) {
42
+ throw CliError.usage('--format openapi takes no resource and no --view: the document is the whole API.');
43
+ }
42
44
  if (resource === undefined && flags.view) {
43
45
  throw CliError.usage('--view needs a resource: `schema users --view show`.');
44
46
  }
45
47
  const client = this.client(flags);
48
+ if (flags.format === 'openapi') {
49
+ const response = await client.get(['_openapi']);
50
+ // A wrong host can answer 200 with a web page, which `> openapi.json` would save as the document.
51
+ if (response.body?.openapi === undefined) {
52
+ throw wrongShape(response.url, 'an OpenAPI document');
53
+ }
54
+ this.printBody(response);
55
+ return;
56
+ }
46
57
  if (resource === undefined) {
47
58
  await this.listResources(client, flags);
48
59
  return;
@@ -52,18 +63,21 @@ export default class Schema extends BaseCommand {
52
63
  if (this.printedJson(flags, response))
53
64
  return;
54
65
  const { fields } = schema;
66
+ const view = schema.view ?? asked;
55
67
  if (fields.length === 0) {
56
- this.log(`No fields on the ${asked} view of ${resource}.`);
68
+ this.log(`No fields on the ${view} view of ${resource}.`);
57
69
  return;
58
70
  }
59
71
  // Headed `field id`, `field type`, `field options`, as a person reads them; `--format json` keeps the keys.
60
72
  // Wrapped, never cut: the CLI checks nothing against the view (decisions.md), so a truncated cell is the only
61
73
  // place a reader could have seen the `options` before sending.
62
- const rows = fields.map((field) => {
63
- const shown = 'field_options' in field ? { ...field, field_options: shownOptions(field.field_options) } : field;
64
- return Object.fromEntries(Object.entries(shown).map(([key, value]) => [key.replaceAll('_', ' '), value]));
65
- });
74
+ const rows = fields.map((field) => Object.fromEntries(Object.entries(field).map(([key, value]) => [key.replaceAll('_', ' '), value])));
75
+ const term = terminal();
76
+ // Writes point at what a record reads back, reads at what a write may send: the view a reader most likely wants next.
77
+ const next = view === 'create' || view === 'update' ? 'show' : 'create';
78
+ this.log(`${paint(term, 'bold', resource)} ${paint(term, 'cyan', `${view} view`)}`);
66
79
  await this.printTable(rows, { overflow: 'wrap' });
80
+ this.log(paint(term, 'dim', `Try: ${this.config.bin} schema ${resource} --view ${next}`));
67
81
  }
68
82
  async listResources(client, flags) {
69
83
  const { response, schema } = await loadSchema(client);
@@ -1,6 +1,6 @@
1
1
  import { wrongShape } from './errors.js';
2
2
  // The one view name held here; every other is the server's to know, and a list would refuse a view a newer API serves.
3
- // It is sent explicitly because the answer does not echo the view, so the output names the view that was asked for.
3
+ // It is sent explicitly, so what comes back is never the server's default under another name.
4
4
  export const DEFAULT_VIEW = 'create';
5
5
  // `source` names where the payload came from, so a shape error can say what did not answer.
6
6
  function parseSchema(payload, source) {
@@ -54,7 +54,8 @@ export function formatCell(value) {
54
54
  }
55
55
  if (typeof value === 'object')
56
56
  return JSON.stringify(value);
57
- return String(value).replaceAll(/\s*\n\s*/g, ' ');
57
+ // A terminal draws a tab out to the next stop and acts on an ESC, so either one moves every border after the cell.
58
+ return String(value).replaceAll(/\s*\p{Cc}[\s\p{Cc}]*/gu, ' ').trim() || '-';
58
59
  }
59
60
  // Colour only where it carries something: a name is what the eye looks for, a boolean is scanned down a column, and
60
61
  // an empty cell should fade behind real data. Read off the value, not the cell, so a string `"true"` stays plain.
@@ -548,7 +548,7 @@
548
548
  "logout": {
549
549
  "aliases": [],
550
550
  "args": {},
551
- "description": "Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION exported in the environment keep working",
551
+ "description": "Delete the saved host and token. AVO_API_HOST, AVO_API_TOKEN and AVO_API_VERSION set in the environment keep working",
552
552
  "examples": [
553
553
  "<%= config.bin %> logout"
554
554
  ],
@@ -580,6 +580,7 @@
580
580
  "examples": [
581
581
  "<%= config.bin %> schema",
582
582
  "<%= config.bin %> schema --format json",
583
+ "<%= config.bin %> schema --format openapi > openapi.json",
583
584
  "<%= config.bin %> schema users",
584
585
  "<%= config.bin %> schema users --view show",
585
586
  "<%= config.bin %> schema --host http://localhost:3000/api --token avo_..."
@@ -620,7 +621,7 @@
620
621
  "type": "boolean"
621
622
  },
622
623
  "format": {
623
- "description": "Output format; `json` prints the body exactly as the server sent it, indented on a terminal",
624
+ "description": "Output format; `json` prints the body exactly as the server sent it, indented on a terminal; `openapi` prints the OpenAPI document of what the token may call",
624
625
  "helpGroup": "OUTPUT",
625
626
  "name": "format",
626
627
  "default": "table",
@@ -628,7 +629,8 @@
628
629
  "multiple": false,
629
630
  "options": [
630
631
  "table",
631
- "json"
632
+ "json",
633
+ "openapi"
632
634
  ],
633
635
  "type": "option"
634
636
  },
@@ -786,5 +788,5 @@
786
788
  ]
787
789
  }
788
790
  },
789
- "version": "4.2.0.beta.5"
791
+ "version": "4.3.0.beta.5"
790
792
  }
data/payload/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "avo-cli",
3
3
  "description": "Command line client for the Avo REST API",
4
- "version": "4.2.0.beta.5",
4
+ "version": "4.3.0.beta.5",
5
5
  "author": "Avo",
6
6
  "bin": {
7
7
  "avo": "./bin/run.js"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: avo-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.0.beta.5
4
+ version: 4.3.0.beta.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Avo