neonctl 1.11.2 → 1.11.3

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,3 +1,27 @@
1
1
  import { createApiClient } from '@neondatabase/api-client';
2
+ import { isAxiosError } from 'axios';
3
+ import { log } from './log.js';
2
4
  export const getApiClient = ({ apiKey, apiHost }) => createApiClient({ apiKey, baseURL: apiHost, timeout: 10000 });
3
- export const isApiError = (err) => err instanceof Error && 'response' in err;
5
+ const RETRY_COUNT = 5;
6
+ const RETRY_DELAY = 3000;
7
+ export const retryOnLock = async (fn) => {
8
+ let attempt = 0;
9
+ let errOut;
10
+ while (attempt < RETRY_COUNT) {
11
+ try {
12
+ return await fn();
13
+ }
14
+ catch (err) {
15
+ errOut = err;
16
+ if (isAxiosError(err) && err.response?.status === 423) {
17
+ attempt++;
18
+ log.info(`Resource is locked. Waiting ${RETRY_DELAY}ms before retrying...`);
19
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
20
+ }
21
+ else {
22
+ throw err;
23
+ }
24
+ }
25
+ }
26
+ throw errOut;
27
+ };
@@ -1,6 +1,7 @@
1
1
  import { writer } from '../writer.js';
2
2
  import { branchCreateRequest, branchCreateRequestEndpointOptions, branchUpdateRequest, } from '../parameters.gen.js';
3
3
  import { commandFailHandler } from '../utils.js';
4
+ import { retryOnLock } from '../api.js';
4
5
  const BRANCH_FIELDS = ['id', 'name', 'created_at'];
5
6
  export const command = 'branches';
6
7
  export const describe = 'Manage branches';
@@ -46,10 +47,10 @@ const list = async (props) => {
46
47
  });
47
48
  };
48
49
  const create = async (props) => {
49
- const { data } = await props.apiClient.createProjectBranch(props.project.id, {
50
+ const { data } = await retryOnLock(() => props.apiClient.createProjectBranch(props.project.id, {
50
51
  branch: props.branch,
51
52
  endpoints: props.endpoint ? [props.endpoint] : undefined,
52
- });
53
+ }));
53
54
  const out = writer(props);
54
55
  out.write(data.branch, {
55
56
  fields: BRANCH_FIELDS,
@@ -70,15 +71,15 @@ const create = async (props) => {
70
71
  out.end();
71
72
  };
72
73
  const update = async (props) => {
73
- const { data } = await props.apiClient.updateProjectBranch(props.project.id, props.branch.id, {
74
+ const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.project.id, props.branch.id, {
74
75
  branch: props.branch,
75
- });
76
+ }));
76
77
  writer(props).end(data.branch, {
77
78
  fields: BRANCH_FIELDS,
78
79
  });
79
80
  };
80
81
  const deleteBranch = async (props) => {
81
- const { data } = await props.apiClient.deleteProjectBranch(props.project.id, props.branch.id);
82
+ const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranch(props.project.id, props.branch.id));
82
83
  writer(props).end(data.branch, {
83
84
  fields: BRANCH_FIELDS,
84
85
  });
@@ -1,3 +1,4 @@
1
+ import { retryOnLock } from '../api.js';
1
2
  import { databaseCreateRequest } from '../parameters.gen.js';
2
3
  import { commandFailHandler } from '../utils.js';
3
4
  import { writer } from '../writer.js';
@@ -40,15 +41,15 @@ export const list = async (props) => {
40
41
  });
41
42
  };
42
43
  export const create = async (props) => {
43
- const { data } = await props.apiClient.createProjectBranchDatabase(props.project.id, props.branch.id, {
44
+ const { data } = await retryOnLock(() => props.apiClient.createProjectBranchDatabase(props.project.id, props.branch.id, {
44
45
  database: props.database,
45
- });
46
+ }));
46
47
  writer(props).end(data.database, {
47
48
  fields: DATABASE_FIELDS,
48
49
  });
49
50
  };
50
51
  export const deleteDb = async (props) => {
51
- const { data } = await props.apiClient.deleteProjectBranchDatabase(props.project.id, props.branch.id, props.database.name);
52
+ const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchDatabase(props.project.id, props.branch.id, props.database.name));
52
53
  writer(props).end(data.database, {
53
54
  fields: DATABASE_FIELDS,
54
55
  });
@@ -1,3 +1,4 @@
1
+ import { retryOnLock } from '../api.js';
1
2
  import { endpointCreateRequest, endpointUpdateRequest, } from '../parameters.gen.js';
2
3
  import { commandFailHandler } from '../utils.js';
3
4
  import { writer } from '../writer.js';
@@ -62,19 +63,23 @@ const list = async (props) => {
62
63
  });
63
64
  };
64
65
  const create = async (props) => {
65
- const { data } = await props.apiClient.createProjectEndpoint(props.project.id, { endpoint: props.endpoint });
66
+ const { data } = await retryOnLock(() => props.apiClient.createProjectEndpoint(props.project.id, {
67
+ endpoint: props.endpoint,
68
+ }));
66
69
  writer(props).end(data.endpoint, {
67
70
  fields: ENDPOINT_FIELDS,
68
71
  });
69
72
  };
70
73
  const update = async (props) => {
71
- const { data } = await props.apiClient.updateProjectEndpoint(props.project.id, props.endpoint.id, { endpoint: props.endpoint });
74
+ const { data } = await retryOnLock(() => props.apiClient.updateProjectEndpoint(props.project.id, props.endpoint.id, {
75
+ endpoint: props.endpoint,
76
+ }));
72
77
  writer(props).end(data.endpoint, {
73
78
  fields: ENDPOINT_FIELDS,
74
79
  });
75
80
  };
76
81
  const deleteEndpoint = async (props) => {
77
- const { data } = await props.apiClient.deleteProjectEndpoint(props.project.id, props.endpoint.id);
82
+ const { data } = await retryOnLock(() => props.apiClient.deleteProjectEndpoint(props.project.id, props.endpoint.id));
78
83
  writer(props).end(data.endpoint, {
79
84
  fields: ENDPOINT_FIELDS,
80
85
  });
@@ -1,4 +1,4 @@
1
- import { describe } from '@jest/globals';
1
+ import { describe, expect } from '@jest/globals';
2
2
  import { testCliCommand } from '../test_utils.js';
3
3
  describe('endpoints', () => {
4
4
  testCliCommand({
@@ -38,6 +38,23 @@ describe('endpoints', () => {
38
38
  snapshot: true,
39
39
  },
40
40
  });
41
+ testCliCommand({
42
+ name: 'create with retry',
43
+ args: [
44
+ 'endpoints',
45
+ 'create',
46
+ '--project.id',
47
+ 'test',
48
+ '--endpoint.branch_id',
49
+ 'test_branch_with_retry',
50
+ '--endpoint.type',
51
+ 'read_only',
52
+ ],
53
+ expected: {
54
+ stderr: expect.stringContaining('Resource is locked'),
55
+ snapshot: true,
56
+ },
57
+ });
41
58
  testCliCommand({
42
59
  name: 'delete',
43
60
  args: [
package/commands/roles.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { retryOnLock } from '../api.js';
1
2
  import { roleCreateRequest } from '../parameters.gen.js';
2
3
  import { commandFailHandler } from '../utils.js';
3
4
  import { writer } from '../writer.js';
@@ -40,15 +41,15 @@ export const list = async (props) => {
40
41
  });
41
42
  };
42
43
  export const create = async (props) => {
43
- const { data } = await props.apiClient.createProjectBranchRole(props.project.id, props.branch.id, {
44
+ const { data } = await retryOnLock(() => props.apiClient.createProjectBranchRole(props.project.id, props.branch.id, {
44
45
  role: props.role,
45
- });
46
+ }));
46
47
  writer(props).end(data.role, {
47
48
  fields: ROLES_FIELDS,
48
49
  });
49
50
  };
50
51
  export const deleteRole = async (props) => {
51
- const { data } = await props.apiClient.deleteProjectBranchRole(props.project.id, props.branch.id, props.role.name);
52
+ const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchRole(props.project.id, props.branch.id, props.role.name));
52
53
  writer(props).end(data.role, {
53
54
  fields: ROLES_FIELDS,
54
55
  });
package/index.js CHANGED
@@ -16,12 +16,12 @@ 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 { isApiError } from './api.js';
20
19
  import { fillInArgs } from './utils.js';
21
20
  import pkg from './pkg.js';
22
21
  import commands from './commands/index.js';
23
22
  import { analyticsMiddleware } from './analytics.js';
24
23
  import { isCi } from './env.js';
24
+ import { isAxiosError } from 'axios';
25
25
  let builder = yargs(hideBin(process.argv));
26
26
  builder = builder
27
27
  .scriptName(pkg.name)
@@ -79,12 +79,12 @@ builder = builder
79
79
  })
80
80
  .middleware(analyticsMiddleware)
81
81
  .fail(async (msg, err) => {
82
- if (isApiError(err)) {
83
- if (err.response.status === 401) {
82
+ if (isAxiosError(err)) {
83
+ if (err.response?.status === 401) {
84
84
  log.error('Authentication failed, please run `neonctl auth`');
85
85
  }
86
86
  else {
87
- log.error('%d: %s\n%s', err.response.status, err.response.statusText, err.response.data?.message);
87
+ log.error('%d: %s\n%s', err.response?.status, err.response?.statusText, err.response?.data?.message);
88
88
  }
89
89
  }
90
90
  else {
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.11.2",
8
+ "version": "1.11.3",
9
9
  "description": "CLI tool for NeonDB Cloud management",
10
10
  "main": "index.js",
11
11
  "author": "NeonDB",