neonctl 1.3.0 → 1.4.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.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ Neon offers several methods for working with your projects. Utilizing the Neon Command Line Interface (CLI), you can operate Neon directly from a terminal or via automation. The Neon CLI facilitates numerous functions, such as Neon authentication, project creation and management, and more.
2
+
3
+ ## Synopsis
4
+
5
+ The `neonctl` command can be called from command line. Without any arguments, it displays command usage and help:
6
+
7
+ ```bash
8
+ usage: neonctl <cmd> [args]
9
+
10
+ Commands:
11
+ neonctl auth Authenticate
12
+ neonctl projects [command] Manage projects
13
+ neonctl me Show current user
14
+ neonctl branches Manage branches
15
+
16
+ Options:
17
+ --version Show version number [boolean]
18
+ --help Show help [boolean]
19
+ -o, --output Set output format
20
+ [string] [choices: "json", "yaml", "table"] [default: "table"]
21
+ --api-host The API host [default: "https://console.neon.tech/api/v2"]
22
+ --config-dir Path to config directory
23
+ [string] [default: "/home/eduard/.config/neonctl"]
24
+ --oauth-host URL to Neon OAUTH host [default: "https://oauth2.neon.tech"]
25
+ --client-id OAuth client id [string] [default: "neonctl"]
26
+ --api-key API key [string] [default: ""]
27
+ ```
28
+
29
+ ## Install the Neon CLI
30
+
31
+ This topic describes how to install the `neonctl` command-line interface tool and connect to Neon.
32
+
33
+ ### Prerequisites
34
+
35
+ - Node.js 16.0 or higher. To check if you already have Node.js, run the following command:
36
+
37
+ ```shell
38
+ node -v
39
+ ```
40
+
41
+ - The `npm` package manager. To check if you already have `npm`, run the following command:
42
+
43
+ ```shell
44
+ npm -v
45
+ ```
46
+
47
+ If you need to install either `Node.js` or `npm`, refer to instructions on [official nodejs page](https://nodejs.org) or you can use [Node version manager](https://github.com/nvm-sh/nvm).
48
+
49
+ ### Install
50
+
51
+ To download and install Neon CLI, run the following command:
52
+
53
+ ```shell
54
+ npm i -g neonctl
55
+ ```
56
+
57
+ ### Connect
58
+
59
+ To authenticate to Neon, run the following command:
60
+
61
+ ```shell
62
+ neonctl auth
63
+ ```
64
+
65
+ The command launches a browser window where you can authorize the Neon CLI to access your Neon account. After granting permission, your credentials are saved locally to a credentials file.
66
+
67
+ ## Commands
68
+
69
+ ### neonctl auth
70
+
71
+ Authenticates the user or caller to Neon. See [Connect](#connect).
72
+
73
+ ### neonctl me
74
+
75
+ Returns information about the authenticated user.
76
+
77
+ ```bash
78
+ $> neonctl me
79
+ ┌────────────────┬──────────────────────────┬────────────┬────────────────┐
80
+ │ Login │ Email │ Name │ Projects Limit │
81
+ ├────────────────┼──────────────────────────┼────────────┼────────────────┤
82
+ │ user1 │ user1@example.com │ User1 │ 1 │
83
+ └────────────────┴──────────────────────────┴────────────┴────────────────┘
84
+ ```
85
+
86
+
87
+ ### neonctl projects
88
+
89
+ For creating and managing Neon projects.
90
+
91
+ ## neonctl branches
92
+ For creating and managing Neon branches.
93
+
94
+ ## Options
95
+
96
+ ### version
97
+
98
+ Shows the neonctl version number
99
+
100
+ ### help
101
+
102
+ Shows the neonctl command-line help
103
+
104
+ ### output, o
105
+
106
+ Sets the output format.
107
+
108
+ ### api-host
109
+
110
+ Shows the API host
111
+
112
+ ### config-dir
113
+
114
+ Sets the path to the `neonctl` configuration directory
115
+
116
+ ### oauth-host
117
+
118
+ Sets the URL of Neon OAuth host
119
+
120
+ ### client-id
121
+
122
+ Sets the OAuth client id
123
+
124
+ ### api-key
125
+
126
+ Sets the API key
package/commands/auth.js CHANGED
@@ -4,6 +4,7 @@ import { TokenSet } from 'openid-client';
4
4
  import { auth, refreshToken } from '../auth.js';
5
5
  import { log } from '../log.js';
6
6
  import { getApiClient } from '../api.js';
7
+ import { isCi } from '../env.js';
7
8
  const CREDENTIALS_FILE = 'credentials.json';
8
9
  export const command = 'auth';
9
10
  export const describe = 'Authenticate';
@@ -12,6 +13,9 @@ export const handler = async (args) => {
12
13
  await authFlow(args);
13
14
  };
14
15
  export const authFlow = async ({ configDir, oauthHost, clientId, }) => {
16
+ if (isCi()) {
17
+ throw new Error('Cannot run interactive auth in CI');
18
+ }
15
19
  if (!clientId) {
16
20
  throw new Error('Missing client id');
17
21
  }
@@ -1,18 +1,12 @@
1
- import yargs from 'yargs';
2
- import { hideBin } from 'yargs/helpers';
3
- import { writeOut } from '../writer.js';
1
+ import { writer } from '../writer.js';
4
2
  import { branchCreateRequest, branchCreateRequestEndpointOptions, branchUpdateRequest, } from '../parameters.gen.js';
3
+ import { commandFailHandler } from '../utils.js';
4
+ const BRANCH_FIELDS = ['id', 'name', 'created_at'];
5
5
  export const command = 'branches';
6
6
  export const describe = 'Manage branches';
7
7
  export const builder = (argv) => argv
8
8
  .demandCommand(1, '')
9
- .fail(async (_msg, _err, argv) => {
10
- const y = yargs(hideBin(process.argv));
11
- if (y.argv._.length === 1) {
12
- argv.showHelp();
13
- process.exit(1);
14
- }
15
- })
9
+ .fail(commandFailHandler)
16
10
  .usage('usage: $0 branches <cmd> [args]')
17
11
  .options({
18
12
  'project.id': {
@@ -24,7 +18,7 @@ export const builder = (argv) => argv
24
18
  .command('list', 'List branches', (yargs) => yargs, async (args) => await list(args))
25
19
  .command('create', 'Create a branch', (yargs) => yargs.options({
26
20
  ...branchCreateRequest,
27
- ...Object.fromEntries(Object.entries(branchCreateRequestEndpointOptions).map(([key, value]) => [`endpoint.${key}`, value])),
21
+ ...Object.fromEntries(Object.entries(branchCreateRequestEndpointOptions).map(([key, value]) => [`endpoint.${key}`, { ...value, demandOption: false }])),
28
22
  }), async (args) => await create(args))
29
23
  .command('update', 'Update a branch', (yargs) => yargs.options(branchUpdateRequest).option('branch.id', {
30
24
  describe: 'Branch ID',
@@ -35,14 +29,19 @@ export const builder = (argv) => argv
35
29
  describe: 'Branch ID',
36
30
  type: 'string',
37
31
  demandOption: true,
38
- }), async (args) => await deleteBranch(args));
32
+ }), async (args) => await deleteBranch(args))
33
+ .command('get', 'Get a branch', (yargs) => yargs.option('branch.id', {
34
+ describe: 'Branch ID',
35
+ type: 'string',
36
+ demandOption: true,
37
+ }), async (args) => await get(args));
39
38
  export const handler = (args) => {
40
39
  return args;
41
40
  };
42
41
  const list = async (props) => {
43
42
  const { data } = await props.apiClient.listProjectBranches(props.project.id);
44
- writeOut(props)(data.branches, {
45
- fields: ['id', 'name', 'created_at'],
43
+ writer(props).end(data.branches, {
44
+ fields: BRANCH_FIELDS,
46
45
  });
47
46
  };
48
47
  const create = async (props) => {
@@ -50,42 +49,42 @@ const create = async (props) => {
50
49
  branch: props.branch,
51
50
  endpoints: props.endpoint ? [props.endpoint] : undefined,
52
51
  });
53
- writeOut(props)({
54
- branch: {
55
- data: data.branch,
56
- config: { fields: ['id', 'name', 'created_at'] },
57
- },
58
- ...(data.endpoints?.length > 0
59
- ? {
60
- endpoints: {
61
- data: data.endpoints,
62
- config: { fields: ['id', 'created_at'] },
63
- },
64
- }
65
- : {}),
66
- ...(data.connection_uris
67
- ? {
68
- connection_uri: {
69
- data: data.connection_uris[0],
70
- config: {
71
- fields: ['connection_uri'],
72
- },
73
- },
74
- }
75
- : {}),
52
+ const out = writer(props);
53
+ out.write(data.branch, {
54
+ fields: BRANCH_FIELDS,
55
+ title: 'branch',
76
56
  });
57
+ if (data.endpoints?.length > 0) {
58
+ out.write(data.endpoints, {
59
+ fields: ['id', 'created_at'],
60
+ title: 'endpoints',
61
+ });
62
+ }
63
+ if (data.connection_uris && data.connection_uris?.length > 0) {
64
+ out.write(data.connection_uris, {
65
+ fields: ['connection_uri'],
66
+ title: 'connection_uris',
67
+ });
68
+ }
69
+ out.end();
77
70
  };
78
71
  const update = async (props) => {
79
72
  const { data } = await props.apiClient.updateProjectBranch(props.project.id, props.branch.id, {
80
73
  branch: props.branch,
81
74
  });
82
- writeOut(props)(data.branch, {
83
- fields: ['id', 'name', 'created_at'],
75
+ writer(props).end(data.branch, {
76
+ fields: BRANCH_FIELDS,
84
77
  });
85
78
  };
86
79
  const deleteBranch = async (props) => {
87
80
  const { data } = await props.apiClient.deleteProjectBranch(props.project.id, props.branch.id);
88
- writeOut(props)(data.branch, {
89
- fields: ['id', 'name', 'created_at'],
81
+ writer(props).end(data.branch, {
82
+ fields: BRANCH_FIELDS,
83
+ });
84
+ };
85
+ const get = async (props) => {
86
+ const { data } = await props.apiClient.getProjectBranch(props.project.id, props.branch.id);
87
+ writer(props).end(data.branch, {
88
+ fields: BRANCH_FIELDS,
90
89
  });
91
90
  };
@@ -0,0 +1,85 @@
1
+ import { describe } from '@jest/globals';
2
+ import { testCliCommand } from '../test_utils.js';
3
+ describe('branches', () => {
4
+ testCliCommand({
5
+ name: 'list',
6
+ args: ['branches', 'list', '--project.id', 'test'],
7
+ expected: {
8
+ snapshot: true,
9
+ },
10
+ });
11
+ testCliCommand({
12
+ name: 'create with endpoint',
13
+ args: [
14
+ 'branches',
15
+ 'create',
16
+ '--project.id',
17
+ 'test',
18
+ '--branch.name',
19
+ 'test_branch',
20
+ '--endpoint.type',
21
+ 'read_only',
22
+ ],
23
+ expected: {
24
+ snapshot: true,
25
+ },
26
+ });
27
+ testCliCommand({
28
+ name: 'create without endpoint',
29
+ args: [
30
+ 'branches',
31
+ 'create',
32
+ '--project.id',
33
+ 'test',
34
+ '--branch.name',
35
+ 'test_branch',
36
+ ],
37
+ expected: {
38
+ snapshot: true,
39
+ },
40
+ });
41
+ testCliCommand({
42
+ name: 'delete',
43
+ args: [
44
+ 'branches',
45
+ 'delete',
46
+ '--project.id',
47
+ 'test',
48
+ '--branch.id',
49
+ 'test_branch_id',
50
+ ],
51
+ expected: {
52
+ snapshot: true,
53
+ },
54
+ });
55
+ testCliCommand({
56
+ name: 'update',
57
+ args: [
58
+ 'branches',
59
+ 'update',
60
+ '--project.id',
61
+ 'test',
62
+ '--branch.id',
63
+ 'test_branch_id',
64
+ '--branch.name',
65
+ 'new_test_branch',
66
+ ],
67
+ expected: {
68
+ snapshot: true,
69
+ },
70
+ });
71
+ testCliCommand({
72
+ name: 'get',
73
+ args: [
74
+ 'branches',
75
+ 'get',
76
+ '--project.id',
77
+ 'test',
78
+ '--branch.id',
79
+ 'test_branch_id',
80
+ ],
81
+ expected: {
82
+ snapshot: true,
83
+ },
84
+ });
85
+ });
@@ -0,0 +1,54 @@
1
+ import { databaseCreateRequest } from '../parameters.gen.js';
2
+ import { commandFailHandler } from '../utils.js';
3
+ import { writer } from '../writer.js';
4
+ const DATABASE_FIELDS = ['name', 'owner_name'];
5
+ export const command = 'databases';
6
+ export const describe = 'Manage databases';
7
+ export const builder = (argv) => argv
8
+ .demandCommand(1, '')
9
+ .fail(commandFailHandler)
10
+ .usage('usage: $0 databases <cmd> [args]')
11
+ .options({
12
+ 'project.id': {
13
+ describe: 'Project ID',
14
+ type: 'string',
15
+ demandOption: true,
16
+ },
17
+ 'branch.id': {
18
+ describe: 'Branch ID',
19
+ type: 'string',
20
+ demandOption: true,
21
+ },
22
+ })
23
+ .command('list', 'List databases', (yargs) => yargs, async (args) => await list(args))
24
+ .command('create', 'Create a database', (yargs) => yargs.options(databaseCreateRequest), async (args) => await create(args))
25
+ .command('delete', 'Delete a database', (yargs) => yargs.options({
26
+ 'database.name': {
27
+ describe: 'Database name',
28
+ type: 'string',
29
+ demandOption: true,
30
+ },
31
+ }), async (args) => await deleteDb(args));
32
+ export const handler = (args) => {
33
+ return args;
34
+ };
35
+ export const list = async (props) => {
36
+ const { data } = await props.apiClient.listProjectBranchDatabases(props.project.id, props.branch.id);
37
+ writer(props).end(data.databases, {
38
+ fields: DATABASE_FIELDS,
39
+ });
40
+ };
41
+ export const create = async (props) => {
42
+ const { data } = await props.apiClient.createProjectBranchDatabase(props.project.id, props.branch.id, {
43
+ database: props.database,
44
+ });
45
+ writer(props).end(data.database, {
46
+ fields: DATABASE_FIELDS,
47
+ });
48
+ };
49
+ export const deleteDb = async (props) => {
50
+ const { data } = await props.apiClient.deleteProjectBranchDatabase(props.project.id, props.branch.id, props.database.name);
51
+ writer(props).end(data.database, {
52
+ fields: DATABASE_FIELDS,
53
+ });
54
+ };
@@ -0,0 +1,52 @@
1
+ import { describe } from '@jest/globals';
2
+ import { testCliCommand } from '../test_utils.js';
3
+ describe('databases', () => {
4
+ testCliCommand({
5
+ name: 'list',
6
+ args: [
7
+ 'databases',
8
+ 'list',
9
+ '--project.id',
10
+ 'test',
11
+ '--branch.id',
12
+ 'test_branch_id',
13
+ ],
14
+ expected: {
15
+ snapshot: true,
16
+ },
17
+ });
18
+ testCliCommand({
19
+ name: 'create',
20
+ args: [
21
+ 'databases',
22
+ 'create',
23
+ '--project.id',
24
+ 'test',
25
+ '--branch.id',
26
+ 'test_branch_id',
27
+ '--database.name',
28
+ 'test_db',
29
+ '--database.owner_name',
30
+ 'test_owner',
31
+ ],
32
+ expected: {
33
+ snapshot: true,
34
+ },
35
+ });
36
+ testCliCommand({
37
+ name: 'delete',
38
+ args: [
39
+ 'databases',
40
+ 'delete',
41
+ '--project.id',
42
+ 'test',
43
+ '--branch.id',
44
+ 'test_branch_id',
45
+ '--database.name',
46
+ 'test_db',
47
+ ],
48
+ expected: {
49
+ snapshot: true,
50
+ },
51
+ });
52
+ });
@@ -0,0 +1,86 @@
1
+ import { endpointCreateRequest, endpointUpdateRequest, } from '../parameters.gen.js';
2
+ import { commandFailHandler } from '../utils.js';
3
+ import { writer } from '../writer.js';
4
+ const ENDPOINT_FIELDS = [
5
+ 'id',
6
+ 'created_at',
7
+ 'branch_id',
8
+ 'type',
9
+ 'current_state',
10
+ ];
11
+ export const command = 'endpoints';
12
+ export const describe = 'Manage endpoints';
13
+ export const builder = (argv) => argv
14
+ .demandCommand(1, '')
15
+ .fail(commandFailHandler)
16
+ .usage('usage: $0 endpoints <cmd> [args]')
17
+ .options({
18
+ 'project.id': {
19
+ describe: 'Project ID',
20
+ type: 'string',
21
+ demandOption: true,
22
+ },
23
+ })
24
+ .command('list', 'List endpoints', (yargs) => yargs.options({
25
+ 'branch.id': {
26
+ describe: 'Branch ID',
27
+ type: 'string',
28
+ demandOption: false,
29
+ },
30
+ }), async (args) => await list(args))
31
+ .command('create', 'Create an endpoint', (yargs) => yargs.options(endpointCreateRequest), async (args) => await create(args))
32
+ .command('update', 'Update an endpoint', (yargs) => yargs.options({
33
+ 'endpoint.id': {
34
+ describe: 'Endpoint ID',
35
+ type: 'string',
36
+ demandOption: true,
37
+ },
38
+ ...endpointUpdateRequest,
39
+ }), async (args) => await update(args))
40
+ .command('delete', 'Delete an endpoint', (yargs) => yargs.options({
41
+ 'endpoint.id': {
42
+ describe: 'Endpoint ID',
43
+ type: 'string',
44
+ demandOption: true,
45
+ },
46
+ }), async (args) => await deleteEndpoint(args))
47
+ .command('get', 'Get an endpoint', (yargs) => yargs.options({
48
+ 'endpoint.id': {
49
+ describe: 'Endpoint ID',
50
+ type: 'string',
51
+ demandOption: true,
52
+ },
53
+ }), async (args) => await getEndpoint(args));
54
+ export const handler = async (args) => args;
55
+ const list = async (props) => {
56
+ const { data } = props.branch?.id
57
+ ? await props.apiClient.listProjectBranchEndpoints(props.project.id, props.branch.id)
58
+ : await props.apiClient.listProjectEndpoints(props.project.id);
59
+ writer(props).end(data.endpoints, {
60
+ fields: ENDPOINT_FIELDS,
61
+ });
62
+ };
63
+ const create = async (props) => {
64
+ const { data } = await props.apiClient.createProjectEndpoint(props.project.id, { endpoint: props.endpoint });
65
+ writer(props).end(data.endpoint, {
66
+ fields: ENDPOINT_FIELDS,
67
+ });
68
+ };
69
+ const update = async (props) => {
70
+ const { data } = await props.apiClient.updateProjectEndpoint(props.project.id, props.endpoint.id, { endpoint: props.endpoint });
71
+ writer(props).end(data.endpoint, {
72
+ fields: ENDPOINT_FIELDS,
73
+ });
74
+ };
75
+ const deleteEndpoint = async (props) => {
76
+ const { data } = await props.apiClient.deleteProjectEndpoint(props.project.id, props.endpoint.id);
77
+ writer(props).end(data.endpoint, {
78
+ fields: ENDPOINT_FIELDS,
79
+ });
80
+ };
81
+ const getEndpoint = async (props) => {
82
+ const { data } = await props.apiClient.getProjectEndpoint(props.project.id, props.endpoint.id);
83
+ writer(props).end(data.endpoint, {
84
+ fields: ENDPOINT_FIELDS,
85
+ });
86
+ };
@@ -0,0 +1,85 @@
1
+ import { describe } from '@jest/globals';
2
+ import { testCliCommand } from '../test_utils.js';
3
+ describe('endpoints', () => {
4
+ testCliCommand({
5
+ name: 'list',
6
+ args: ['endpoints', 'list', '--project.id', 'test'],
7
+ expected: {
8
+ snapshot: true,
9
+ },
10
+ });
11
+ testCliCommand({
12
+ name: 'list with branch filter',
13
+ args: [
14
+ 'endpoints',
15
+ 'list',
16
+ '--project.id',
17
+ 'test',
18
+ '--branch.id',
19
+ 'test_branch_id',
20
+ ],
21
+ expected: {
22
+ snapshot: true,
23
+ },
24
+ });
25
+ testCliCommand({
26
+ name: 'create',
27
+ args: [
28
+ 'endpoints',
29
+ 'create',
30
+ '--project.id',
31
+ 'test',
32
+ '--endpoint.branch_id',
33
+ 'test_branch_id',
34
+ '--endpoint.type',
35
+ 'read_only',
36
+ ],
37
+ expected: {
38
+ snapshot: true,
39
+ },
40
+ });
41
+ testCliCommand({
42
+ name: 'delete',
43
+ args: [
44
+ 'endpoints',
45
+ 'delete',
46
+ '--project.id',
47
+ 'test',
48
+ '--endpoint.id',
49
+ 'test_endpoint_id',
50
+ ],
51
+ expected: {
52
+ snapshot: true,
53
+ },
54
+ });
55
+ testCliCommand({
56
+ name: 'update',
57
+ args: [
58
+ 'endpoints',
59
+ 'update',
60
+ '--project.id',
61
+ 'test',
62
+ '--endpoint.id',
63
+ 'test_endpoint_id',
64
+ '--endpoint.branch_id',
65
+ 'test_branch_id',
66
+ ],
67
+ expected: {
68
+ snapshot: true,
69
+ },
70
+ });
71
+ testCliCommand({
72
+ name: 'get',
73
+ args: [
74
+ 'endpoints',
75
+ 'get',
76
+ '--project.id',
77
+ 'test',
78
+ '--endpoint.id',
79
+ 'test_endpoint_id',
80
+ ],
81
+ expected: {
82
+ snapshot: true,
83
+ },
84
+ });
85
+ });
package/commands/index.js CHANGED
@@ -2,4 +2,6 @@ import * as auth from './auth.js';
2
2
  import * as projects from './projects.js';
3
3
  import * as users from './users.js';
4
4
  import * as branches from './branches.js';
5
- export default [auth, projects, users, branches];
5
+ import * as endpoints from './endpoints.js';
6
+ import * as databases from './databases.js';
7
+ export default [auth, projects, users, branches, endpoints, databases];