neonctl 2.17.1 → 2.18.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.
@@ -10,16 +10,15 @@ import { log } from '../log.js';
10
10
  import { parseSchemaDiffParams, schemaDiff } from './schema_diff.js';
11
11
  import { getComputeUnits } from '../utils/compute_units.js';
12
12
  export const BRANCH_FIELDS = [
13
- 'id',
14
13
  'name',
15
- 'default',
14
+ 'id',
16
15
  'current_state',
17
16
  'created_at',
18
17
  'expires_at',
19
18
  ];
20
19
  const BRANCH_FIELDS_RESET = [
21
- 'id',
22
20
  'name',
21
+ 'id',
23
22
  'default',
24
23
  'current_state',
25
24
  'created_at',
@@ -211,11 +210,30 @@ export const handler = (args) => {
211
210
  return args;
212
211
  };
213
212
  const list = async (props) => {
214
- const { data } = await props.apiClient.listProjectBranches({
213
+ const { data: { branches, annotations }, } = await props.apiClient.listProjectBranches({
215
214
  projectId: props.projectId,
216
215
  });
217
- writer(props).end(data.branches, {
216
+ writer(props).end(branches, {
218
217
  fields: BRANCH_FIELDS,
218
+ renderColumns: {
219
+ expires_at: (br) => br.expires_at || 'never',
220
+ name: (br) => {
221
+ const annotation = annotations[br.id];
222
+ const isAnon = annotation?.value.anonymized;
223
+ const result = [];
224
+ if (br.default) {
225
+ result.push('✱');
226
+ }
227
+ if (br.protected) {
228
+ result.push('⛨');
229
+ }
230
+ if (isAnon) {
231
+ result.push('[anon]');
232
+ }
233
+ result.push(br.name);
234
+ return result.join(' ');
235
+ },
236
+ },
219
237
  });
220
238
  };
221
239
  const create = async (props) => {
@@ -2,9 +2,14 @@ import { describe } from 'vitest';
2
2
  import { test } from '../test_utils/fixtures';
3
3
  describe('branches', () => {
4
4
  /* list */
5
- test('list', async ({ testCliCommand }) => {
5
+ test('list/yaml', async ({ testCliCommand }) => {
6
6
  await testCliCommand(['branches', 'list', '--project-id', 'test']);
7
7
  });
8
+ test('list/table output', async ({ testCliCommand }) => {
9
+ await testCliCommand(['branches', 'list', '--project-id', 'test'], {
10
+ outputTable: true,
11
+ });
12
+ });
8
13
  /* create */
9
14
  test('create by default with r/w endpoint', async ({ testCliCommand }) => {
10
15
  await testCliCommand([
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "git+ssh://git@github.com/neondatabase/neonctl.git"
6
6
  },
7
7
  "type": "module",
8
- "version": "2.17.1",
8
+ "version": "2.18.0",
9
9
  "description": "CLI tool for NeonDB Cloud management",
10
10
  "main": "index.js",
11
11
  "author": "NeonDB",
@@ -41,7 +41,7 @@ export const test = originalTest.extend({
41
41
  '--api-host',
42
42
  `http://localhost:${server.address().port}`,
43
43
  '--output',
44
- 'yaml',
44
+ options.outputTable ? 'table' : 'yaml',
45
45
  '--api-key',
46
46
  'test-key',
47
47
  '--no-analytics',
package/writer.js CHANGED
@@ -20,25 +20,28 @@ const writeJson = (chunks) => {
20
20
  ])), null, 2);
21
21
  };
22
22
  const writeTable = (chunks, out) => {
23
- chunks.forEach(({ data, config }) => {
23
+ chunks.forEach(({ data, config: { emptyMessage, fields, title, renderColumns = {} } }) => {
24
24
  const arrayData = Array.isArray(data) ? data : [data];
25
- if (!arrayData.length && config.emptyMessage) {
26
- out.write('\n' + config.emptyMessage + '\n');
25
+ if (!arrayData.length && emptyMessage) {
26
+ out.write('\n' + emptyMessage + '\n');
27
27
  return;
28
28
  }
29
- const fields = config.fields.filter((field) => arrayData.some((item) => item[field] !== undefined && item[field] !== ''));
29
+ const fieldsFiltered = fields.filter((field) => arrayData.some((item) => item[field] !== undefined && item[field] !== ''));
30
30
  const table = new Table({
31
31
  style: {
32
32
  head: ['green'],
33
33
  },
34
- head: fields.map((field) => field
34
+ head: fieldsFiltered.map((field) => field
35
35
  .split('_')
36
36
  .map((word) => word[0].toUpperCase() + word.slice(1))
37
37
  .join(' ')),
38
38
  });
39
39
  arrayData.forEach((item) => {
40
- table.push(fields.map((field) => {
40
+ table.push(fieldsFiltered.map((field) => {
41
41
  const value = item[field];
42
+ if (renderColumns[field]) {
43
+ return renderColumns[field]?.(item);
44
+ }
42
45
  return Array.isArray(value)
43
46
  ? value.join('\n')
44
47
  : isObject(value)
@@ -46,8 +49,8 @@ const writeTable = (chunks, out) => {
46
49
  : value;
47
50
  }));
48
51
  });
49
- if (config.title) {
50
- out.write((isCi() ? config.title : chalk.bold(config.title)) + '\n');
52
+ if (title) {
53
+ out.write((isCi() ? title : chalk.bold(title)) + '\n');
51
54
  }
52
55
  out.write(table.toString());
53
56
  out.write('\n');
package/writer.test.js CHANGED
@@ -1,86 +1,104 @@
1
- import { Writable } from 'node:stream';
1
+ import { PassThrough } from 'node:stream';
2
2
  import { describe, it, expect } from 'vitest';
3
3
  import { writer } from './writer.js';
4
- class MockWritable extends Writable {
5
- constructor() {
6
- super(...arguments);
7
- this._data = [];
8
- }
9
- get data() {
10
- return this._data.map((chunk) => chunk.toString()).join('');
11
- }
12
- _write(chunk) {
13
- this._data.push(chunk);
14
- }
15
- }
4
+ const getMockWritable = () => {
5
+ const chunks = [];
6
+ const stream = new PassThrough();
7
+ stream.on('data', (chunk) => {
8
+ chunks.push(chunk.toString());
9
+ });
10
+ return {
11
+ stream,
12
+ getData: () => {
13
+ return chunks.join('');
14
+ },
15
+ };
16
+ };
16
17
  describe('writer', () => {
17
18
  describe('outputs yaml', () => {
18
19
  it('outputs single data', () => {
19
- const stream = new MockWritable();
20
+ const { stream, getData } = getMockWritable();
20
21
  const out = writer({ output: 'yaml', out: stream });
21
22
  out.end({ foo: 'bar' }, { fields: ['foo'] });
22
- expect(stream.data).toMatchSnapshot();
23
+ expect(getData()).toMatchSnapshot();
23
24
  });
24
25
  it('outputs single data with title', () => {
25
- const stream = new MockWritable();
26
+ const { stream, getData } = getMockWritable();
26
27
  const out = writer({ output: 'yaml', out: stream });
27
28
  out.end({ foo: 'bar' }, { fields: ['foo'], title: 'baz' });
28
- expect(stream.data).toMatchSnapshot();
29
+ expect(getData()).toMatchSnapshot();
29
30
  });
30
31
  it('outputs multiple data', () => {
31
- const stream = new MockWritable();
32
+ const { stream, getData } = getMockWritable();
32
33
  const out = writer({ output: 'yaml', out: stream });
33
34
  out
34
35
  .write({ foo: 'bar' }, { fields: ['foo'], title: 'T1' })
35
36
  .write({ baz: 'xyz' }, { fields: ['baz'], title: 'T2' })
36
37
  .end();
37
- expect(stream.data).toMatchSnapshot();
38
+ expect(getData()).toMatchSnapshot();
38
39
  });
39
40
  });
40
41
  describe('outputs json', () => {
41
42
  it('outputs single data', () => {
42
- const stream = new MockWritable();
43
+ const { stream, getData } = getMockWritable();
43
44
  const out = writer({ output: 'json', out: stream });
44
45
  out.end({ foo: 'bar' }, { fields: ['foo'] });
45
- expect(stream.data).toMatchSnapshot();
46
+ expect(getData()).toMatchSnapshot();
46
47
  });
47
48
  it('outputs single data with title', () => {
48
- const stream = new MockWritable();
49
+ const { stream, getData } = getMockWritable();
49
50
  const out = writer({ output: 'json', out: stream });
50
51
  out.end({ foo: 'bar' }, { fields: ['foo'], title: 'baz' });
51
- expect(stream.data).toMatchSnapshot();
52
+ expect(getData()).toMatchSnapshot();
52
53
  });
53
54
  it('outputs multiple data', () => {
54
- const stream = new MockWritable();
55
+ const { stream, getData } = getMockWritable();
55
56
  const out = writer({ output: 'json', out: stream });
56
57
  out
57
58
  .write({ foo: 'bar' }, { fields: ['foo'], title: 'T1' })
58
59
  .write({ baz: 'xyz' }, { fields: ['baz'], title: 'T2' })
59
60
  .end();
60
- expect(stream.data).toMatchSnapshot();
61
+ expect(getData()).toMatchSnapshot();
61
62
  });
62
63
  });
63
64
  describe('outputs table', () => {
64
65
  it('outputs single data', () => {
65
- const stream = new MockWritable();
66
+ const { stream, getData } = getMockWritable();
66
67
  const out = writer({ output: 'table', out: stream });
67
68
  out.end({ foo: 'bar', extra: 'extra' }, { fields: ['foo'] });
68
- expect(stream.data).toMatchSnapshot();
69
+ expect(getData()).toMatchSnapshot();
69
70
  });
70
71
  it('outputs single data with title', () => {
71
- const stream = new MockWritable();
72
+ const { stream, getData } = getMockWritable();
72
73
  const out = writer({ output: 'table', out: stream });
73
74
  out.end({ foo: 'bar', extra: 'extra' }, { fields: ['foo'], title: 'baz' });
74
- expect(stream.data).toMatchSnapshot();
75
+ expect(getData()).toMatchSnapshot();
75
76
  });
76
77
  it('outputs multiple data', () => {
77
- const stream = new MockWritable();
78
+ const { stream, getData } = getMockWritable();
78
79
  const out = writer({ output: 'table', out: stream });
79
80
  out
80
81
  .write({ foo: 'bar', extra: 'extra' }, { fields: ['foo'], title: 'T1' })
81
82
  .write({ baz: 'xyz', extra: 'extra' }, { fields: ['baz'], title: 'T2' })
82
83
  .end();
83
- expect(stream.data).toMatchSnapshot();
84
+ expect(getData()).toMatchSnapshot();
85
+ });
86
+ it('outputs table with custom renderer', () => {
87
+ const { stream, getData } = getMockWritable();
88
+ const out = writer({
89
+ output: 'table',
90
+ out: stream,
91
+ });
92
+ out
93
+ .write({ foo: 'bar' }, {
94
+ fields: ['foo'],
95
+ title: 'T1',
96
+ renderColumns: {
97
+ foo: ({ foo }) => `Here is: ${foo}`,
98
+ },
99
+ })
100
+ .end();
101
+ expect(getData()).toMatchSnapshot();
84
102
  });
85
103
  });
86
104
  });