neonctl 1.14.0 → 1.15.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/api.js CHANGED
@@ -1,7 +1,15 @@
1
1
  import { createApiClient } from '@neondatabase/api-client';
2
2
  import { isAxiosError } from 'axios';
3
3
  import { log } from './log.js';
4
- export const getApiClient = ({ apiKey, apiHost }) => createApiClient({ apiKey, baseURL: apiHost, timeout: 10000 });
4
+ import pkg from './pkg.js';
5
+ export const getApiClient = ({ apiKey, apiHost }) => createApiClient({
6
+ apiKey,
7
+ baseURL: apiHost,
8
+ timeout: 10000,
9
+ headers: {
10
+ 'User-Agent': `neonctl v${pkg.version}`,
11
+ },
12
+ });
5
13
  const RETRY_COUNT = 5;
6
14
  const RETRY_DELAY = 3000;
7
15
  export const retryOnLock = async (fn) => {
package/auth.js CHANGED
@@ -23,7 +23,7 @@ custom.setHttpOptionsDefaults({
23
23
  timeout: SERVER_TIMEOUT,
24
24
  });
25
25
  export const refreshToken = async ({ oauthHost, clientId }, tokenSet) => {
26
- log.info('Discovering oauth server');
26
+ log.debug('Discovering oauth server');
27
27
  const issuer = await Issuer.discover(oauthHost);
28
28
  const neonOAuthClient = new issuer.Client({
29
29
  token_endpoint_auth_method: 'none',
@@ -33,7 +33,7 @@ export const refreshToken = async ({ oauthHost, clientId }, tokenSet) => {
33
33
  return await neonOAuthClient.refresh(tokenSet);
34
34
  };
35
35
  export const auth = async ({ oauthHost, clientId }) => {
36
- log.info('Discovering oauth server');
36
+ log.debug('Discovering oauth server');
37
37
  const issuer = await Issuer.discover(oauthHost);
38
38
  //
39
39
  // Start HTTP server and wait till /callback is hit
@@ -64,7 +64,7 @@ export const auth = async ({ oauthHost, clientId }) => {
64
64
  response.end();
65
65
  return;
66
66
  }
67
- log.info(`Callback received: ${request.url}`);
67
+ log.debug(`Callback received: ${request.url}`);
68
68
  const params = neonOAuthClient.callbackParams(request);
69
69
  const tokenSet = await neonOAuthClient.callback(REDIRECT_URI(listen_port), params, {
70
70
  code_verifier: codeVerifier,
package/commands/auth.js CHANGED
@@ -7,6 +7,7 @@ import { getApiClient } from '../api.js';
7
7
  import { isCi } from '../env.js';
8
8
  const CREDENTIALS_FILE = 'credentials.json';
9
9
  export const command = 'auth';
10
+ export const aliases = ['login'];
10
11
  export const describe = 'Authenticate';
11
12
  export const builder = (yargs) => yargs;
12
13
  export const handler = async (args) => {
@@ -52,7 +53,7 @@ export const ensureAuth = async (props) => {
52
53
  const tokenSetContents = await JSON.parse(readFileSync(credentialsPath, 'utf8'));
53
54
  const tokenSet = new TokenSet(tokenSetContents);
54
55
  if (tokenSet.expired()) {
55
- log.info('using refresh token to update access token');
56
+ log.debug('using refresh token to update access token');
56
57
  const refreshedTokenSet = await refreshToken({
57
58
  oauthHost: props.oauthHost,
58
59
  clientId: props.clientId,
@@ -1,8 +1,10 @@
1
+ import { EndpointType } from '@neondatabase/api-client';
1
2
  import { writer } from '../writer.js';
2
- import { branchCreateRequest, branchCreateRequestEndpointOptions, } from '../parameters.gen.js';
3
- import { commandFailHandler } from '../utils.js';
3
+ import { branchCreateRequest } from '../parameters.gen.js';
4
+ import { commandFailHandler } from '../utils/middlewares.js';
4
5
  import { retryOnLock } from '../api.js';
5
- import { branchIdFromProps, fillSingleProject } from '../enrichers.js';
6
+ import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
7
+ import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp, } from '../utils/formats.js';
6
8
  const BRANCH_FIELDS = ['id', 'name', 'created_at', 'updated_at'];
7
9
  export const command = 'branches';
8
10
  export const describe = 'Manage branches';
@@ -12,7 +14,7 @@ export const builder = (argv) => argv
12
14
  .fail(commandFailHandler)
13
15
  .usage('usage: $0 branches <sub-command> [options]')
14
16
  .options({
15
- 'project.id': {
17
+ 'project-id': {
16
18
  describe: 'Project ID',
17
19
  type: 'string',
18
20
  },
@@ -20,31 +22,80 @@ export const builder = (argv) => argv
20
22
  .middleware(fillSingleProject)
21
23
  .command('list', 'List branches', (yargs) => yargs, async (args) => await list(args))
22
24
  .command('create', 'Create a branch', (yargs) => yargs.options({
23
- ...branchCreateRequest,
24
- ...Object.fromEntries(Object.entries(branchCreateRequestEndpointOptions).map(([key, value]) => [`endpoint.${key}`, { ...value, demandOption: false }])),
25
- }), async (args) => await create(args))
26
- .command('update <id|name>', 'Update a branch', (yargs) => yargs.options({
27
- 'branch.name': {
28
- describe: 'Branch name',
25
+ name: branchCreateRequest['branch.name'],
26
+ parent: {
27
+ describe: 'Parent branch name or id or timestamp or LSN. Defaults to the primary branch',
29
28
  type: 'string',
30
- demandOption: true,
31
29
  },
32
- }), async (args) => await update(args))
30
+ endpoint: {
31
+ describe: 'Create a branch with or without an endpoint. By default branch is created with a read-write endpoint. To create a branch without endpoint use --no-endpoint',
32
+ type: 'boolean',
33
+ default: true,
34
+ },
35
+ readonly: {
36
+ describe: 'Create a read-only branch',
37
+ type: 'boolean',
38
+ implies: 'endpoint',
39
+ },
40
+ }), async (args) => await create(args))
41
+ .command('rename <id|name> <new-name>', 'Rename a branch', (yargs) => yargs, async (args) => await rename(args))
33
42
  .command('delete <id|name>', 'Delete a branch', (yargs) => yargs, async (args) => await deleteBranch(args))
34
43
  .command('get <id|name>', 'Get a branch', (yargs) => yargs, async (args) => await get(args));
35
44
  export const handler = (args) => {
36
45
  return args;
37
46
  };
38
47
  const list = async (props) => {
39
- const { data } = await props.apiClient.listProjectBranches(props.project.id);
48
+ const { data } = await props.apiClient.listProjectBranches(props.projectId);
40
49
  writer(props).end(data.branches, {
41
50
  fields: BRANCH_FIELDS,
42
51
  });
43
52
  };
44
53
  const create = async (props) => {
45
- const { data } = await retryOnLock(() => props.apiClient.createProjectBranch(props.project.id, {
46
- branch: props.branch,
47
- endpoints: props.endpoint ? [props.endpoint] : undefined,
54
+ const parentProps = await (() => {
55
+ if (!props.parent) {
56
+ return props.apiClient
57
+ .listProjectBranches(props.projectId)
58
+ .then(({ data }) => {
59
+ const branch = data.branches.find((b) => b.primary);
60
+ if (!branch) {
61
+ throw new Error('No primary branch found');
62
+ }
63
+ return { parent_id: branch.id };
64
+ });
65
+ }
66
+ if (looksLikeLSN(props.parent)) {
67
+ return { parent_lsn: props.parent };
68
+ }
69
+ if (looksLikeTimestamp(props.parent)) {
70
+ return { parent_timestamp: props.parent };
71
+ }
72
+ if (looksLikeBranchId(props.parent)) {
73
+ return { parent_id: props.parent };
74
+ }
75
+ return props.apiClient
76
+ .listProjectBranches(props.projectId)
77
+ .then(({ data }) => {
78
+ const branch = data.branches.find((b) => b.name === props.parent);
79
+ if (!branch) {
80
+ throw new Error(`Branch ${props.parent} not found`);
81
+ }
82
+ return { parent_id: branch.id };
83
+ });
84
+ })();
85
+ const { data } = await retryOnLock(() => props.apiClient.createProjectBranch(props.projectId, {
86
+ branch: {
87
+ name: props.name,
88
+ ...parentProps,
89
+ },
90
+ endpoints: props.endpoint
91
+ ? [
92
+ {
93
+ type: props.readonly
94
+ ? EndpointType.ReadOnly
95
+ : EndpointType.ReadWrite,
96
+ },
97
+ ]
98
+ : [],
48
99
  }));
49
100
  const out = writer(props);
50
101
  out.write(data.branch, {
@@ -65,10 +116,12 @@ const create = async (props) => {
65
116
  }
66
117
  out.end();
67
118
  };
68
- const update = async (props) => {
119
+ const rename = async (props) => {
69
120
  const branchId = await branchIdFromProps(props);
70
- const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.project.id, branchId, {
71
- branch: props.branch,
121
+ const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.projectId, branchId, {
122
+ branch: {
123
+ name: props.newName,
124
+ },
72
125
  }));
73
126
  writer(props).end(data.branch, {
74
127
  fields: BRANCH_FIELDS,
@@ -76,14 +129,14 @@ const update = async (props) => {
76
129
  };
77
130
  const deleteBranch = async (props) => {
78
131
  const branchId = await branchIdFromProps(props);
79
- const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranch(props.project.id, branchId));
132
+ const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranch(props.projectId, branchId));
80
133
  writer(props).end(data.branch, {
81
134
  fields: BRANCH_FIELDS,
82
135
  });
83
136
  };
84
137
  const get = async (props) => {
85
138
  const branchId = await branchIdFromProps(props);
86
- const { data } = await props.apiClient.getProjectBranch(props.project.id, branchId);
139
+ const { data } = await props.apiClient.getProjectBranch(props.projectId, branchId);
87
140
  writer(props).end(data.branch, {
88
141
  fields: BRANCH_FIELDS,
89
142
  });
@@ -3,22 +3,35 @@ import { testCliCommand } from '../test_utils.js';
3
3
  describe('branches', () => {
4
4
  testCliCommand({
5
5
  name: 'list',
6
- args: ['branches', 'list', '--project.id', 'test'],
6
+ args: ['branches', 'list', '--project-id', 'test'],
7
7
  expected: {
8
8
  snapshot: true,
9
9
  },
10
10
  });
11
11
  testCliCommand({
12
- name: 'create with endpoint',
12
+ name: 'create by default with r/w endpoint',
13
13
  args: [
14
14
  'branches',
15
15
  'create',
16
- '--project.id',
16
+ '--project-id',
17
17
  'test',
18
- '--branch.name',
18
+ '--name',
19
19
  'test_branch',
20
- '--endpoint.type',
21
- 'read_only',
20
+ ],
21
+ expected: {
22
+ snapshot: true,
23
+ },
24
+ });
25
+ testCliCommand({
26
+ name: 'create with readonly endpoint',
27
+ args: [
28
+ 'branches',
29
+ 'create',
30
+ '--project-id',
31
+ 'test',
32
+ '--name',
33
+ 'test_branch',
34
+ '--readonly',
22
35
  ],
23
36
  expected: {
24
37
  snapshot: true,
@@ -29,10 +42,59 @@ describe('branches', () => {
29
42
  args: [
30
43
  'branches',
31
44
  'create',
32
- '--project.id',
45
+ '--project-id',
33
46
  'test',
34
- '--branch.name',
47
+ '--name',
35
48
  'test_branch',
49
+ '--no-endpoint',
50
+ ],
51
+ expected: {
52
+ snapshot: true,
53
+ },
54
+ });
55
+ testCliCommand({
56
+ name: 'create with parent by name',
57
+ args: [
58
+ 'branches',
59
+ 'create',
60
+ '--project-id',
61
+ 'test',
62
+ '--name',
63
+ 'test_branch_with_parent_name',
64
+ '--parent',
65
+ 'main',
66
+ ],
67
+ expected: {
68
+ snapshot: true,
69
+ },
70
+ });
71
+ testCliCommand({
72
+ name: 'create with parent by lsn',
73
+ args: [
74
+ 'branches',
75
+ 'create',
76
+ '--project-id',
77
+ 'test',
78
+ '--name',
79
+ 'test_branch_with_parent_lsn',
80
+ '--parent',
81
+ '0/123ABC',
82
+ ],
83
+ expected: {
84
+ snapshot: true,
85
+ },
86
+ });
87
+ testCliCommand({
88
+ name: 'create with parent by timestamp',
89
+ args: [
90
+ 'branches',
91
+ 'create',
92
+ '--project-id',
93
+ 'test',
94
+ '--name',
95
+ 'test_branch_with_parent_timestamp',
96
+ '--parent',
97
+ '2021-01-01T00:00:00.000Z',
36
98
  ],
37
99
  expected: {
38
100
  snapshot: true,
@@ -44,7 +106,7 @@ describe('branches', () => {
44
106
  'branches',
45
107
  'delete',
46
108
  'br-sunny-branch-123456',
47
- '--project.id',
109
+ '--project-id',
48
110
  'test',
49
111
  ],
50
112
  expected: {
@@ -52,15 +114,14 @@ describe('branches', () => {
52
114
  },
53
115
  });
54
116
  testCliCommand({
55
- name: 'update',
117
+ name: 'rename',
56
118
  args: [
57
119
  'branches',
58
- 'update',
120
+ 'rename',
59
121
  'test_branch',
60
- '--project.id',
61
- 'test',
62
- '--branch.name',
63
122
  'new_test_branch',
123
+ '--project-id',
124
+ 'test',
64
125
  ],
65
126
  expected: {
66
127
  snapshot: true,
@@ -68,14 +129,14 @@ describe('branches', () => {
68
129
  });
69
130
  testCliCommand({
70
131
  name: 'get by id',
71
- args: ['branches', 'get', 'br-sunny-branch-123456', '--project.id', 'test'],
132
+ args: ['branches', 'get', 'br-sunny-branch-123456', '--project-id', 'test'],
72
133
  expected: {
73
134
  snapshot: true,
74
135
  },
75
136
  });
76
137
  testCliCommand({
77
138
  name: 'get by name',
78
- args: ['branches', 'get', 'test_branch', '--project.id', 'test'],
139
+ args: ['branches', 'get', 'test_branch', '--project-id', 'test'],
79
140
  expected: {
80
141
  snapshot: true,
81
142
  },
@@ -1,5 +1,5 @@
1
1
  import { EndpointType } from '@neondatabase/api-client';
2
- import { branchIdFromProps, fillSingleProject } from '../enrichers.js';
2
+ import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
3
3
  export const command = 'connection-string [branch]';
4
4
  export const aliases = ['cs'];
5
5
  export const describe = 'Get connection string';
@@ -11,15 +11,15 @@ export const builder = (argv) => {
11
11
  type: 'string',
12
12
  })
13
13
  .options({
14
- 'project.id': {
14
+ 'project-id': {
15
15
  type: 'string',
16
16
  describe: 'Project ID',
17
17
  },
18
- 'role.name': {
18
+ 'role-name': {
19
19
  type: 'string',
20
20
  describe: 'Role name',
21
21
  },
22
- 'database.name': {
22
+ 'database-name': {
23
23
  type: 'string',
24
24
  describe: 'Database name',
25
25
  },
@@ -33,18 +33,27 @@ export const builder = (argv) => {
33
33
  describe: 'Use connection string for Prisma setup',
34
34
  default: false,
35
35
  },
36
+ 'endpoint-type': {
37
+ type: 'string',
38
+ choices: Object.values(EndpointType),
39
+ describe: 'Endpoint type',
40
+ },
36
41
  })
37
42
  .middleware(fillSingleProject);
38
43
  };
39
44
  export const handler = async (props) => {
40
- const projectId = props.project.id;
45
+ const projectId = props.projectId;
41
46
  const branchId = await branchIdFromProps(props);
42
47
  const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(projectId, branchId);
43
- const endpoint = endpoints.find((e) => e.type === EndpointType.ReadWrite);
48
+ const matchEndpointType = props.endpointType ?? EndpointType.ReadWrite;
49
+ let endpoint = endpoints.find((e) => e.type === matchEndpointType);
50
+ if (!endpoint && props.endpointType == null) {
51
+ endpoint = endpoints[0];
52
+ }
44
53
  if (!endpoint) {
45
- throw new Error(`No endpoint found for the branch: ${branchId}`);
54
+ throw new Error(`No ${props.endpointType ?? ''} endpoint found for the branch: ${branchId}`);
46
55
  }
47
- const role = props.role?.name ||
56
+ const role = props.roleName ||
48
57
  (await props.apiClient
49
58
  .listProjectBranchRoles(projectId, branchId)
50
59
  .then(({ data }) => {
@@ -58,7 +67,7 @@ export const handler = async (props) => {
58
67
  .map((r) => r.name)
59
68
  .join(', ')}`);
60
69
  }));
61
- const database = props.database?.name ||
70
+ const database = props.databaseName ||
62
71
  (await props.apiClient
63
72
  .listProjectBranchDatabases(projectId, branchId)
64
73
  .then(({ data }) => {
@@ -68,9 +77,11 @@ export const handler = async (props) => {
68
77
  if (data.databases.length === 1) {
69
78
  return data.databases[0].name;
70
79
  }
71
- throw new Error(`Multiple databases found for the branch, please provide one with the --database.name option: ${data.databases}`);
80
+ throw new Error(`Multiple databases found for the branch, please provide one with the --database.name option: ${data.databases
81
+ .map((d) => d.name)
82
+ .join(', ')}`);
72
83
  }));
73
- const { data: password } = await props.apiClient.getProjectBranchRolePassword(props.project.id, endpoint.branch_id, role);
84
+ const { data: password } = await props.apiClient.getProjectBranchRolePassword(props.projectId, endpoint.branch_id, role);
74
85
  const host = props.pooled
75
86
  ? endpoint.host.replace(endpoint.id, `${endpoint.id}-pooler`)
76
87
  : endpoint.host;
@@ -6,11 +6,11 @@ describe('connection_string', () => {
6
6
  args: [
7
7
  'connection-string',
8
8
  'test_branch',
9
- '--project.id',
9
+ '--project-id',
10
10
  'test',
11
- '--database.name',
11
+ '--database-name',
12
12
  'test_db',
13
- '--role.name',
13
+ '--role-name',
14
14
  'test_role',
15
15
  ],
16
16
  expected: {
@@ -22,11 +22,11 @@ describe('connection_string', () => {
22
22
  args: [
23
23
  'connection-string',
24
24
  'test_branch',
25
- '--project.id',
25
+ '--project-id',
26
26
  'test',
27
- '--database.name',
27
+ '--database-name',
28
28
  'test_db',
29
- '--role.name',
29
+ '--role-name',
30
30
  'test_role',
31
31
  '--pooled',
32
32
  ],
@@ -39,11 +39,11 @@ describe('connection_string', () => {
39
39
  args: [
40
40
  'connection-string',
41
41
  'test_branch',
42
- '--project.id',
42
+ '--project-id',
43
43
  'test',
44
- '--database.name',
44
+ '--database-name',
45
45
  'test_db',
46
- '--role.name',
46
+ '--role-name',
47
47
  'test_role',
48
48
  '--prisma',
49
49
  ],
@@ -56,11 +56,11 @@ describe('connection_string', () => {
56
56
  args: [
57
57
  'connection-string',
58
58
  'test_branch',
59
- '--project.id',
59
+ '--project-id',
60
60
  'test',
61
- '--database.name',
61
+ '--database-name',
62
62
  'test_db',
63
- '--role.name',
63
+ '--role-name',
64
64
  'test_role',
65
65
  '--prisma',
66
66
  '--pooled',
@@ -1,7 +1,6 @@
1
1
  import { retryOnLock } from '../api.js';
2
- import { branchIdFromProps, fillSingleProject } from '../enrichers.js';
3
- import { databaseCreateRequest } from '../parameters.gen.js';
4
- import { commandFailHandler } from '../utils.js';
2
+ import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
3
+ import { commandFailHandler } from '../utils/middlewares.js';
5
4
  import { writer } from '../writer.js';
6
5
  const DATABASE_FIELDS = ['name', 'owner_name', 'created_at'];
7
6
  export const command = 'databases';
@@ -12,7 +11,7 @@ export const builder = (argv) => argv
12
11
  .fail(commandFailHandler)
13
12
  .usage('usage: $0 databases <sub-command> [options]')
14
13
  .options({
15
- 'project.id': {
14
+ 'project-id': {
16
15
  describe: 'Project ID',
17
16
  type: 'string',
18
17
  },
@@ -23,22 +22,52 @@ export const builder = (argv) => argv
23
22
  })
24
23
  .middleware(fillSingleProject)
25
24
  .command('list', 'List databases', (yargs) => yargs, async (args) => await list(args))
26
- .command('create', 'Create a database', (yargs) => yargs.options(databaseCreateRequest), async (args) => await create(args))
25
+ .command('create', 'Create a database', (yargs) => yargs.options({
26
+ name: {
27
+ describe: 'Database name',
28
+ type: 'string',
29
+ demandOption: true,
30
+ },
31
+ 'owner-name': {
32
+ describe: 'Owner name',
33
+ type: 'string',
34
+ },
35
+ }), async (args) => await create(args))
27
36
  .command('delete <database>', 'Delete a database', (yargs) => yargs, async (args) => await deleteDb(args));
28
37
  export const handler = (args) => {
29
38
  return args;
30
39
  };
31
40
  export const list = async (props) => {
32
41
  const branchId = await branchIdFromProps(props);
33
- const { data } = await props.apiClient.listProjectBranchDatabases(props.project.id, branchId);
42
+ const { data } = await props.apiClient.listProjectBranchDatabases(props.projectId, branchId);
34
43
  writer(props).end(data.databases, {
35
44
  fields: DATABASE_FIELDS,
36
45
  });
37
46
  };
38
47
  export const create = async (props) => {
39
48
  const branchId = await branchIdFromProps(props);
40
- const { data } = await retryOnLock(() => props.apiClient.createProjectBranchDatabase(props.project.id, branchId, {
41
- database: props.database,
49
+ const owner = props.ownerName ??
50
+ (await props.apiClient
51
+ .listProjectBranchRoles(props.projectId, branchId)
52
+ .then(({ data }) => {
53
+ if (data.roles.length === 0) {
54
+ throw new Error(`No roles found in branch ${branchId}`);
55
+ }
56
+ if (data.roles.length > 1) {
57
+ throw new Error(`More than one role found in branch ${branchId}. Please specify the owner name. Roles: ${data.roles
58
+ .map((r) => r.name)
59
+ .join(', ')}`);
60
+ }
61
+ return data.roles[0].name;
62
+ }));
63
+ if (!owner) {
64
+ throw new Error('No owner found');
65
+ }
66
+ const { data } = await retryOnLock(() => props.apiClient.createProjectBranchDatabase(props.projectId, branchId, {
67
+ database: {
68
+ name: props.name,
69
+ owner_name: owner,
70
+ },
42
71
  }));
43
72
  writer(props).end(data.database, {
44
73
  fields: DATABASE_FIELDS,
@@ -46,7 +75,7 @@ export const create = async (props) => {
46
75
  };
47
76
  export const deleteDb = async (props) => {
48
77
  const branchId = await branchIdFromProps(props);
49
- const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchDatabase(props.project.id, branchId, props.database));
78
+ const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchDatabase(props.projectId, branchId, props.database));
50
79
  writer(props).end(data.database, {
51
80
  fields: DATABASE_FIELDS,
52
81
  });
@@ -6,7 +6,7 @@ describe('databases', () => {
6
6
  args: [
7
7
  'databases',
8
8
  'list',
9
- '--project.id',
9
+ '--project-id',
10
10
  'test',
11
11
  '--branch',
12
12
  'test_branch',
@@ -20,13 +20,13 @@ describe('databases', () => {
20
20
  args: [
21
21
  'databases',
22
22
  'create',
23
- '--project.id',
23
+ '--project-id',
24
24
  'test',
25
25
  '--branch',
26
26
  'test_branch',
27
- '--database.name',
27
+ '--name',
28
28
  'test_db',
29
- '--database.owner_name',
29
+ '--owner-name',
30
30
  'test_owner',
31
31
  ],
32
32
  expected: {
@@ -39,7 +39,7 @@ describe('databases', () => {
39
39
  'databases',
40
40
  'delete',
41
41
  'test_db',
42
- '--project.id',
42
+ '--project-id',
43
43
  'test',
44
44
  '--branch',
45
45
  'test_branch',
@@ -1,5 +1,5 @@
1
- import { fillSingleProject } from '../enrichers.js';
2
- import { commandFailHandler } from '../utils.js';
1
+ import { fillSingleProject } from '../utils/enrichers.js';
2
+ import { commandFailHandler } from '../utils/middlewares.js';
3
3
  import { writer } from '../writer.js';
4
4
  const OPERATIONS_FIELDS = ['id', 'action', 'status', 'created_at'];
5
5
  export const command = 'operations';
@@ -10,7 +10,7 @@ export const builder = (argv) => argv
10
10
  .fail(commandFailHandler)
11
11
  .usage('usage: $0 operations <sub-command> [options]')
12
12
  .options({
13
- 'project.id': {
13
+ 'project-id': {
14
14
  describe: 'Project ID',
15
15
  type: 'string',
16
16
  },
@@ -22,7 +22,7 @@ export const handler = (args) => {
22
22
  };
23
23
  export const list = async (props) => {
24
24
  const { data } = await props.apiClient.listProjectOperations({
25
- projectId: props.project.id,
25
+ projectId: props.projectId,
26
26
  limit: props.limit,
27
27
  });
28
28
  writer(props).end(data.operations, {
@@ -3,7 +3,7 @@ import { testCliCommand } from '../test_utils.js';
3
3
  describe('operations', () => {
4
4
  testCliCommand({
5
5
  name: 'list',
6
- args: ['operations', 'list', '--project.id', 'test'],
6
+ args: ['operations', 'list', '--project-id', 'test'],
7
7
  expected: {
8
8
  snapshot: true,
9
9
  },
@@ -1,7 +1,14 @@
1
1
  import { projectCreateRequest } from '../parameters.gen.js';
2
- import { commandFailHandler } from '../utils.js';
2
+ import { commandFailHandler } from '../utils/middlewares.js';
3
3
  import { writer } from '../writer.js';
4
4
  const PROJECT_FIELDS = ['id', 'name', 'region_id', 'created_at'];
5
+ const REGIONS = [
6
+ 'aws-us-west-2',
7
+ 'aws-ap-southeast-1',
8
+ 'aws-eu-central-1',
9
+ 'aws-us-east-2',
10
+ 'aws-us-east-1',
11
+ ];
5
12
  export const command = 'projects';
6
13
  export const describe = 'Manage projects';
7
14
  export const aliases = ['project'];
@@ -13,10 +20,24 @@ export const builder = (argv) => {
13
20
  .command('list', 'List projects', (yargs) => yargs, async (args) => {
14
21
  await list(args);
15
22
  })
16
- .command('create', 'Create a project', (yargs) => yargs.options(projectCreateRequest), async (args) => {
23
+ .command('create', 'Create a project', (yargs) => yargs.options({
24
+ name: {
25
+ describe: projectCreateRequest['project.name'].description,
26
+ type: 'string',
27
+ },
28
+ 'region-id': {
29
+ describe: `The region ID. Possible values: ${REGIONS.join(', ')}`,
30
+ type: 'string',
31
+ },
32
+ }), async (args) => {
17
33
  await create(args);
18
34
  })
19
- .command('update <id>', 'Update a project', (yargs) => yargs.options(projectCreateRequest), async (args) => {
35
+ .command('update <id>', 'Update a project', (yargs) => yargs.options({
36
+ name: {
37
+ describe: projectCreateRequest['project.name'].description,
38
+ type: 'string',
39
+ },
40
+ }), async (args) => {
20
41
  await update(args);
21
42
  })
22
43
  .command('delete <id>', 'Delete a project', (yargs) => yargs, async (args) => {
@@ -34,20 +55,20 @@ const list = async (props) => {
34
55
  writer(props).end(data.projects, { fields: PROJECT_FIELDS });
35
56
  };
36
57
  const create = async (props) => {
37
- if (props.project == null) {
38
- props.project = {};
39
- const inquirer = await import('inquirer');
40
- const answers = await inquirer.default.prompt([
41
- { name: 'name', message: 'Project name (optional)', type: 'input' },
42
- ]);
43
- if (answers.name) {
44
- props.project = answers;
45
- }
58
+ const project = {};
59
+ if (props.name) {
60
+ project.name = props.name;
61
+ }
62
+ if (props.regionId) {
63
+ project.region_id = props.regionId;
46
64
  }
47
65
  const { data } = await props.apiClient.createProject({
48
- project: props.project,
66
+ project,
49
67
  });
50
- writer(props).end(data.project, { fields: PROJECT_FIELDS });
68
+ const out = writer(props);
69
+ out.write(data.project, { fields: PROJECT_FIELDS });
70
+ out.write(data.connection_uris, { fields: ['connection_uri'] });
71
+ out.end();
51
72
  };
52
73
  const deleteProject = async (props) => {
53
74
  const { data } = await props.apiClient.deleteProject(props.id);
@@ -57,7 +78,9 @@ const deleteProject = async (props) => {
57
78
  };
58
79
  const update = async (props) => {
59
80
  const { data } = await props.apiClient.updateProject(props.id, {
60
- project: props.project,
81
+ project: {
82
+ name: props.name,
83
+ },
61
84
  });
62
85
  writer(props).end(data.project, { fields: PROJECT_FIELDS });
63
86
  };
@@ -10,7 +10,7 @@ describe('projects', () => {
10
10
  });
11
11
  testCliCommand({
12
12
  name: 'create',
13
- args: ['projects', 'create', '--project.name', 'test_project'],
13
+ args: ['projects', 'create', '--name', 'test_project'],
14
14
  expected: {
15
15
  snapshot: true,
16
16
  },
@@ -24,7 +24,7 @@ describe('projects', () => {
24
24
  });
25
25
  testCliCommand({
26
26
  name: 'update',
27
- args: ['projects', 'update', 'test', '--project.name', 'test_project'],
27
+ args: ['projects', 'update', 'test', '--name', 'test_project'],
28
28
  expected: {
29
29
  snapshot: true,
30
30
  },
package/commands/roles.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { retryOnLock } from '../api.js';
2
- import { branchIdFromProps, fillSingleProject } from '../enrichers.js';
3
- import { roleCreateRequest } from '../parameters.gen.js';
4
- import { commandFailHandler } from '../utils.js';
2
+ import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
3
+ import { commandFailHandler } from '../utils/middlewares.js';
5
4
  import { writer } from '../writer.js';
6
5
  const ROLES_FIELDS = ['name', 'created_at'];
7
6
  export const command = 'roles';
@@ -12,7 +11,7 @@ export const builder = (argv) => argv
12
11
  .fail(commandFailHandler)
13
12
  .usage('usage: $0 roles <sub-command> [options]')
14
13
  .options({
15
- 'project.id': {
14
+ 'project-id': {
16
15
  describe: 'Project ID',
17
16
  type: 'string',
18
17
  },
@@ -23,22 +22,30 @@ export const builder = (argv) => argv
23
22
  })
24
23
  .middleware(fillSingleProject)
25
24
  .command('list', 'List roles', (yargs) => yargs, async (args) => await list(args))
26
- .command('create', 'Create a role', (yargs) => yargs.options(roleCreateRequest), async (args) => await create(args))
25
+ .command('create', 'Create a role', (yargs) => yargs.options({
26
+ name: {
27
+ describe: 'Role name',
28
+ type: 'string',
29
+ demandOption: true,
30
+ },
31
+ }), async (args) => await create(args))
27
32
  .command('delete <role>', 'Delete a role', (yargs) => yargs, async (args) => await deleteRole(args));
28
33
  export const handler = (args) => {
29
34
  return args;
30
35
  };
31
36
  export const list = async (props) => {
32
37
  const branchId = await branchIdFromProps(props);
33
- const { data } = await props.apiClient.listProjectBranchRoles(props.project.id, branchId);
38
+ const { data } = await props.apiClient.listProjectBranchRoles(props.projectId, branchId);
34
39
  writer(props).end(data.roles, {
35
40
  fields: ROLES_FIELDS,
36
41
  });
37
42
  };
38
43
  export const create = async (props) => {
39
44
  const branchId = await branchIdFromProps(props);
40
- const { data } = await retryOnLock(() => props.apiClient.createProjectBranchRole(props.project.id, branchId, {
41
- role: props.role,
45
+ const { data } = await retryOnLock(() => props.apiClient.createProjectBranchRole(props.projectId, branchId, {
46
+ role: {
47
+ name: props.name,
48
+ },
42
49
  }));
43
50
  writer(props).end(data.role, {
44
51
  fields: ROLES_FIELDS,
@@ -46,7 +53,7 @@ export const create = async (props) => {
46
53
  };
47
54
  export const deleteRole = async (props) => {
48
55
  const branchId = await branchIdFromProps(props);
49
- const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchRole(props.project.id, branchId, props.role));
56
+ const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchRole(props.projectId, branchId, props.role));
50
57
  writer(props).end(data.role, {
51
58
  fields: ROLES_FIELDS,
52
59
  });
@@ -3,7 +3,7 @@ import { testCliCommand } from '../test_utils.js';
3
3
  describe('roles', () => {
4
4
  testCliCommand({
5
5
  name: 'list',
6
- args: ['roles', 'list', '--project.id', 'test', '--branch', 'test_branch'],
6
+ args: ['roles', 'list', '--project-id', 'test', '--branch', 'test_branch'],
7
7
  expected: {
8
8
  snapshot: true,
9
9
  },
@@ -13,11 +13,11 @@ describe('roles', () => {
13
13
  args: [
14
14
  'roles',
15
15
  'create',
16
- '--project.id',
16
+ '--project-id',
17
17
  'test',
18
18
  '--branch',
19
19
  'test_branch',
20
- '--role.name',
20
+ '--name',
21
21
  'test_role',
22
22
  ],
23
23
  expected: {
@@ -30,7 +30,7 @@ describe('roles', () => {
30
30
  'roles',
31
31
  'delete',
32
32
  'test_role',
33
- '--project.id',
33
+ '--project-id',
34
34
  'test',
35
35
  '--branch',
36
36
  'test_branch',
package/index.js CHANGED
@@ -16,7 +16,7 @@ import { ensureAuth } from './commands/auth.js';
16
16
  import { defaultDir, ensureConfigDir } from './config.js';
17
17
  import { log } from './log.js';
18
18
  import { defaultClientID } from './auth.js';
19
- import { fillInArgs } from './utils.js';
19
+ import { fillInArgs } from './utils/middlewares.js';
20
20
  import pkg from './pkg.js';
21
21
  import commands from './commands/index.js';
22
22
  import { analyticsMiddleware } from './analytics.js';
@@ -29,6 +29,7 @@ builder = builder
29
29
  .help()
30
30
  .option('output', {
31
31
  alias: 'o',
32
+ group: 'Global options:',
32
33
  describe: 'Set output format',
33
34
  type: 'string',
34
35
  choices: ['json', 'yaml', 'table'],
@@ -42,6 +43,7 @@ builder = builder
42
43
  // Setup config directory
43
44
  .option('config-dir', {
44
45
  describe: 'Path to config directory',
46
+ group: 'Global options:',
45
47
  type: 'string',
46
48
  default: defaultDir,
47
49
  })
@@ -60,6 +62,7 @@ builder = builder
60
62
  })
61
63
  .option('api-key', {
62
64
  describe: 'API key',
65
+ group: 'Global options:',
63
66
  type: 'string',
64
67
  default: process.env.NEON_API_KEY ?? '',
65
68
  })
@@ -74,10 +77,15 @@ builder = builder
74
77
  .strictCommands()
75
78
  .option('analytics', {
76
79
  describe: 'Manage analytics. Example: --no-analytics, --analytics false',
80
+ group: 'Global options:',
77
81
  type: 'boolean',
78
82
  default: !isCi(),
79
83
  })
80
84
  .middleware(analyticsMiddleware)
85
+ .group('version', 'Global options:')
86
+ .alias('version', 'v')
87
+ .group('help', 'Global options:')
88
+ .alias('help', 'h')
81
89
  .completion()
82
90
  .fail(async (msg, err) => {
83
91
  if (isAxiosError(err)) {
@@ -94,7 +102,7 @@ builder = builder
94
102
  else {
95
103
  log.error(msg || err?.message);
96
104
  }
97
- err.stack && log.degug('Stack: %s', err.stack);
105
+ err?.stack && log.debug('Stack: %s', err.stack);
98
106
  process.exit(1);
99
107
  });
100
108
  (async () => {
package/log.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { format } from 'node:util';
2
2
  import { isDebug } from './env.js';
3
3
  export const log = {
4
- degug: (...args) => {
4
+ debug: (...args) => {
5
5
  if (isDebug()) {
6
6
  process.stderr.write(`DEBUG: ${format(...args)}\n`);
7
7
  }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "git@github.com:neondatabase/neonctl.git"
6
6
  },
7
7
  "type": "module",
8
- "version": "1.14.0",
8
+ "version": "1.15.0",
9
9
  "description": "CLI tool for NeonDB Cloud management",
10
10
  "main": "index.js",
11
11
  "author": "NeonDB",
@@ -1,6 +1,6 @@
1
- const HAIKU_REGEX = /^[a-z]+-[a-z]+-\d{6}$/;
1
+ import { looksLikeBranchId } from './formats.js';
2
2
  export const branchIdResolve = async ({ branch, apiClient, projectId, }) => {
3
- if (branch.startsWith('br-') && HAIKU_REGEX.test(branch.substring(3))) {
3
+ if (looksLikeBranchId(branch)) {
4
4
  return branch;
5
5
  }
6
6
  const { data } = await apiClient.listProjectBranches(projectId);
@@ -20,10 +20,10 @@ export const branchIdFromProps = async (props) => {
20
20
  return await branchIdResolve({
21
21
  branch,
22
22
  apiClient: props.apiClient,
23
- projectId: props.project.id,
23
+ projectId: props.projectId,
24
24
  });
25
25
  }
26
- const { data } = await props.apiClient.listProjectBranches(props.project.id);
26
+ const { data } = await props.apiClient.listProjectBranches(props.projectId);
27
27
  const primaryBranch = data.branches.find((b) => b.primary);
28
28
  if (primaryBranch) {
29
29
  return primaryBranch.id;
@@ -31,7 +31,7 @@ export const branchIdFromProps = async (props) => {
31
31
  throw new Error('No primary branch found');
32
32
  };
33
33
  export const fillSingleProject = async (props) => {
34
- if (props.project) {
34
+ if (props.projectId) {
35
35
  return props;
36
36
  }
37
37
  const { data } = await props.apiClient.listProjects({});
@@ -43,6 +43,6 @@ export const fillSingleProject = async (props) => {
43
43
  }
44
44
  return {
45
45
  ...props,
46
- project: { id: data.projects[0].id },
46
+ projectId: data.projects[0].id,
47
47
  };
48
48
  };
@@ -0,0 +1,5 @@
1
+ const HAIKU_REGEX = /^[a-z]+-[a-z]+-\d{6}$/;
2
+ export const looksLikeBranchId = (branch) => branch.startsWith('br-') && HAIKU_REGEX.test(branch.substring(3));
3
+ const LSN_REGEX = /^[a-fA-F0-9]{1,8}\/[a-fA-F0-9]{1,8}$/;
4
+ export const looksLikeLSN = (lsn) => LSN_REGEX.test(lsn);
5
+ export const looksLikeTimestamp = (timestamp) => !isNaN(Date.parse(timestamp));
File without changes