neonctl 1.29.5 → 1.31.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 CHANGED
@@ -70,7 +70,7 @@ The Neon CLI supports autocompletion, which you can configure in a few easy step
70
70
  | [projects](https://neon.tech/docs/reference/cli-projects) | `list`, `create`, `update`, `delete`, `get` | Manage projects |
71
71
  | [ip-allow](https://neon.tech/docs/reference/cli-ip-allow) | `list`, `add`, `remove`, `reset` | Manage IP Allow |
72
72
  | [me](https://neon.tech/docs/reference/cli-me) | | Show current user |
73
- | [branches](https://neon.tech/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-primary`, `delete`, `get` | Manage branches |
73
+ | [branches](https://neon.tech/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-default`, `delete`, `get` | Manage branches |
74
74
  | [databases](https://neon.tech/docs/reference/cli-databases) | `list`, `create`, `delete` | Manage databases |
75
75
  | [roles](https://neon.tech/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
76
76
  | [operations](https://neon.tech/docs/reference/cli-operations) | `list` | Manage operations |
package/analytics.js CHANGED
@@ -9,11 +9,13 @@ import pkg from './pkg.js';
9
9
  import { getApiClient } from './api.js';
10
10
  const WRITE_KEY = '3SQXn5ejjXWLEJ8xU2PRYhAotLtTaeeV';
11
11
  let client;
12
+ let clientInitialized = false;
12
13
  let userId = '';
13
14
  export const analyticsMiddleware = async (args) => {
14
- if (!args.analytics) {
15
+ if (!args.analytics || clientInitialized) {
15
16
  return;
16
17
  }
18
+ clientInitialized = true;
17
19
  try {
18
20
  const credentialsPath = join(args.configDir, CREDENTIALS_FILE);
19
21
  const credentials = readFileSync(credentialsPath, { encoding: 'utf-8' });
@@ -39,6 +41,7 @@ export const analyticsMiddleware = async (args) => {
39
41
  writeKey: WRITE_KEY,
40
42
  host: 'https://track.neon.tech',
41
43
  });
44
+ log.debug('Initialized CLI analytics');
42
45
  client.identify({
43
46
  userId: userId?.toString() ?? 'anonymous',
44
47
  });
@@ -54,9 +57,13 @@ export const analyticsMiddleware = async (args) => {
54
57
  ci: isCi(),
55
58
  },
56
59
  });
57
- log.debug('Flushing CLI started event with userId: %s', userId);
58
- await client.closeAndFlush();
59
- log.debug('Flushed CLI started event with userId: %s', userId);
60
+ };
61
+ export const closeAnalytics = async () => {
62
+ if (client) {
63
+ log.debug('Flushing CLI analytics');
64
+ await client.closeAndFlush();
65
+ log.debug('Flushed CLI analytics');
66
+ }
60
67
  };
61
68
  export const sendError = (err, errCode) => {
62
69
  if (!client) {
@@ -73,6 +80,5 @@ export const sendError = (err, errCode) => {
73
80
  statusCode: axiosError?.response?.status,
74
81
  },
75
82
  });
76
- client.closeAndFlush();
77
83
  log.debug('Sent CLI error event: %s', errCode);
78
84
  };
@@ -7,10 +7,12 @@ import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp, } from '../utils/f
7
7
  import { psql } from '../utils/psql.js';
8
8
  import { parsePointInTime } from '../utils/point_in_time.js';
9
9
  import { log } from '../log.js';
10
+ import { parseSchemaDiffParams, schemaDiff } from './schema_diff.js';
10
11
  const BRANCH_FIELDS = [
11
12
  'id',
12
13
  'name',
13
14
  'primary',
15
+ 'default',
14
16
  'created_at',
15
17
  'updated_at',
16
18
  ];
@@ -18,6 +20,7 @@ const BRANCH_FIELDS_RESET = [
18
20
  'id',
19
21
  'name',
20
22
  'primary',
23
+ 'default',
21
24
  'created_at',
22
25
  'last_reset_at',
23
26
  ];
@@ -37,7 +40,7 @@ export const builder = (argv) => argv
37
40
  .command('create', 'Create a branch', (yargs) => yargs.options({
38
41
  name: branchCreateRequest['branch.name'],
39
42
  parent: {
40
- describe: 'Parent branch name or id or timestamp or LSN. Defaults to the primary branch',
43
+ describe: 'Parent branch name or id or timestamp or LSN. Defaults to the default branch',
41
44
  type: 'string',
42
45
  },
43
46
  compute: {
@@ -107,7 +110,8 @@ export const builder = (argv) => argv
107
110
  ],
108
111
  ]), async (args) => await restore(args))
109
112
  .command('rename <id|name> <new-name>', 'Rename a branch', (yargs) => yargs, async (args) => await rename(args))
110
- .command('set-primary <id|name>', 'Set a branch as primary', (yargs) => yargs, async (args) => await setPrimary(args))
113
+ .command('set-primary <id|name>', 'DEPRECATED: Use set-default. Set a branch as primary', (yargs) => yargs, async (args) => await setDefault(args))
114
+ .command('set-default <id|name>', 'Set a branch as default', (yargs) => yargs, async (args) => await setDefault(args))
111
115
  .command('add-compute <id|name>', 'Add a compute to a branch', (yargs) => yargs.options({
112
116
  type: {
113
117
  type: 'string',
@@ -117,7 +121,47 @@ export const builder = (argv) => argv
117
121
  },
118
122
  }), async (args) => await addCompute(args))
119
123
  .command('delete <id|name>', 'Delete a branch', (yargs) => yargs, async (args) => await deleteBranch(args))
120
- .command('get <id|name>', 'Get a branch', (yargs) => yargs, async (args) => await get(args));
124
+ .command('get <id|name>', 'Get a branch', (yargs) => yargs, async (args) => await get(args))
125
+ .command({
126
+ command: 'schema-diff [base-branch] [compare-source[@(timestamp|lsn)]]',
127
+ aliases: ['sd'],
128
+ describe: "Compare the latest schemas of any two branches, or compare a branch to its own or another branch's history.",
129
+ builder: (yargs) => {
130
+ return yargs
131
+ .middleware((args) => (args.compareSource = args['compare-source@(timestamp']))
132
+ .middleware(parseSchemaDiffParams)
133
+ .options({
134
+ database: {
135
+ alias: 'db',
136
+ type: 'string',
137
+ description: 'Name of the database for which the schema comparison is performed',
138
+ },
139
+ })
140
+ .example([
141
+ [
142
+ '$0 branches schema-diff main br-compare-branch-123456',
143
+ 'Compares the main branch to the head of the branch with ID br-compare-branch-123456',
144
+ ],
145
+ [
146
+ '$0 branches schema-diff main compare@2024-06-01T00:00:00Z',
147
+ 'Compares the main branch to the state of the compare branch at timestamp 2024-06-01T00:00:00.000Z',
148
+ ],
149
+ [
150
+ '$0 branches schema-diff my-branch ^self@0/123456',
151
+ 'Compares my-branch to LSN 0/123456 from its own history',
152
+ ],
153
+ [
154
+ '$0 branches schema-diff my-branch ^parent',
155
+ 'Compares my-branch to the head of its parent branch',
156
+ ],
157
+ [
158
+ '$0 branches schema-diff',
159
+ "If a branch is specified in 'set-context', compares this branch to its parent. Otherwise, compares the default branch to its parent.",
160
+ ],
161
+ ]);
162
+ },
163
+ handler: async (args) => schemaDiff(args),
164
+ });
121
165
  export const handler = (args) => {
122
166
  return args;
123
167
  };
@@ -133,9 +177,9 @@ const create = async (props) => {
133
177
  return props.apiClient
134
178
  .listProjectBranches(props.projectId)
135
179
  .then(({ data }) => {
136
- const branch = data.branches.find((b) => b.primary);
180
+ const branch = data.branches.find((b) => b.default);
137
181
  if (!branch) {
138
- throw new Error('No primary branch found');
182
+ throw new Error('No default branch found');
139
183
  }
140
184
  return { parent_id: branch.id };
141
185
  });
@@ -211,9 +255,9 @@ const rename = async (props) => {
211
255
  fields: BRANCH_FIELDS,
212
256
  });
213
257
  };
214
- const setPrimary = async (props) => {
258
+ const setDefault = async (props) => {
215
259
  const branchId = await branchIdFromProps(props);
216
- const { data } = await retryOnLock(() => props.apiClient.setPrimaryProjectBranch(props.projectId, branchId));
260
+ const { data } = await retryOnLock(() => props.apiClient.setDefaultProjectBranch(props.projectId, branchId));
217
261
  writer(props).end(data.branch, {
218
262
  fields: BRANCH_FIELDS,
219
263
  });
@@ -195,6 +195,20 @@ describe('branches', () => {
195
195
  snapshot: true,
196
196
  },
197
197
  });
198
+ /* set default */
199
+ testCliCommand({
200
+ name: 'set default by id',
201
+ args: [
202
+ 'branches',
203
+ 'set-default',
204
+ 'br-sunny-branch-123456',
205
+ '--project-id',
206
+ 'test',
207
+ ],
208
+ expected: {
209
+ snapshot: true,
210
+ },
211
+ });
198
212
  /* get */
199
213
  testCliCommand({
200
214
  name: 'get by id',
@@ -14,7 +14,7 @@ export const builder = (argv) => {
14
14
  .example('$0 cs main@2024-01-01T00:00:00Z', 'Get connection string for the main branch at a specific point in time')
15
15
  .example('$0 cs main@0/234235', 'Get connection string for the main branch at a specific LSN')
16
16
  .positional('branch', {
17
- describe: `Branch name or id. Defaults to the primary branch if omitted. Can be written in the point-in-time format: "branch@timestamp" or "branch@lsn"`,
17
+ describe: `Branch name or id. Defaults to the default branch if omitted. Can be written in the point-in-time format: "branch@timestamp" or "branch@lsn"`,
18
18
  type: 'string',
19
19
  })
20
20
  .options({
package/commands/index.js CHANGED
@@ -2,6 +2,7 @@ import * as auth from './auth.js';
2
2
  import * as projects from './projects.js';
3
3
  import * as ipAllow from './ip_allow.js';
4
4
  import * as users from './user.js';
5
+ import * as orgs from './orgs.js';
5
6
  import * as branches from './branches.js';
6
7
  import * as databases from './databases.js';
7
8
  import * as roles from './roles.js';
@@ -11,6 +12,7 @@ import * as setContext from './set_context.js';
11
12
  export default [
12
13
  auth,
13
14
  users,
15
+ orgs,
14
16
  projects,
15
17
  ipAllow,
16
18
  branches,
@@ -0,0 +1,23 @@
1
+ import { writer } from '../writer.js';
2
+ const ORG_FIELDS = ['id', 'name'];
3
+ export const command = 'orgs';
4
+ export const describe = 'Manage organizations';
5
+ export const aliases = ['org'];
6
+ export const builder = (argv) => {
7
+ return argv.usage('$0 orgs <sub-command> [options]').command('list', 'List organizations', (yargs) => yargs, async (args) => {
8
+ // @ts-expect-error: TODO - Assert `args` is `CommonProps`
9
+ await list(args);
10
+ });
11
+ };
12
+ export const handler = (args) => {
13
+ return args;
14
+ };
15
+ const list = async (props) => {
16
+ const out = writer(props);
17
+ const { data: { organizations }, } = await props.apiClient.getCurrentUserOrganizations();
18
+ out.write(organizations, {
19
+ fields: ORG_FIELDS,
20
+ title: 'Organizations',
21
+ });
22
+ out.end();
23
+ };
@@ -0,0 +1,150 @@
1
+ import { createPatch } from 'diff';
2
+ import chalk from 'chalk';
3
+ import { writer } from '../writer.js';
4
+ import { branchIdFromProps } from '../utils/enrichers.js';
5
+ import { parsePointInTime, } from '../utils/point_in_time.js';
6
+ import { isAxiosError } from 'axios';
7
+ import { sendError } from '../analytics.js';
8
+ import { log } from '../log.js';
9
+ const COLORS = {
10
+ added: chalk.green,
11
+ removed: chalk.red,
12
+ header: chalk.yellow,
13
+ section: chalk.magenta,
14
+ };
15
+ export const schemaDiff = async (props) => {
16
+ props.branch = props.baseBranch || props.branch;
17
+ const baseBranch = await branchIdFromProps(props);
18
+ let pointInTime = await parsePointInTime({
19
+ pointInTime: props.compareSource,
20
+ targetBranchId: baseBranch,
21
+ projectId: props.projectId,
22
+ api: props.apiClient,
23
+ });
24
+ // Swap base and compare points if comparing with parent branch
25
+ const comparingWithParent = props.compareSource.startsWith('^parent');
26
+ let baseBranchPoint = {
27
+ branchId: baseBranch,
28
+ tag: 'head',
29
+ };
30
+ [baseBranchPoint, pointInTime] = comparingWithParent
31
+ ? [pointInTime, baseBranchPoint]
32
+ : [baseBranchPoint, pointInTime];
33
+ const baseDatabases = await fetchDatabases(baseBranch, props);
34
+ if (props.database) {
35
+ const database = baseDatabases.find((db) => db.name === props.database);
36
+ if (!database) {
37
+ throw new Error(`Database ${props.database} does not exist in base branch ${baseBranch}`);
38
+ }
39
+ const patch = await createSchemaDiff(baseBranchPoint, pointInTime, database, props);
40
+ writer(props).text(colorize(patch));
41
+ return;
42
+ }
43
+ await Promise.all(baseDatabases.map(async (database) => {
44
+ const patch = await createSchemaDiff(baseBranchPoint, pointInTime, database, props);
45
+ writer(props).text(colorize(patch));
46
+ }));
47
+ };
48
+ const fetchDatabases = async (branch, props) => {
49
+ return props.apiClient
50
+ .listProjectBranchDatabases(props.projectId, branch)
51
+ .then((response) => response.data.databases);
52
+ };
53
+ const createSchemaDiff = async (baseBranch, pointInTime, database, props) => {
54
+ const [baseSchema, compareSchema] = await Promise.all([
55
+ fetchSchema(baseBranch, database, props),
56
+ fetchSchema(pointInTime, database, props),
57
+ ]);
58
+ return createPatch(`Database: ${database.name}`, baseSchema, compareSchema, generateHeader(baseBranch), generateHeader(pointInTime));
59
+ };
60
+ const fetchSchema = async (pointInTime, database, props) => {
61
+ try {
62
+ return props.apiClient
63
+ .getProjectBranchSchema({
64
+ projectId: props.projectId,
65
+ branchId: pointInTime.branchId,
66
+ db_name: database.name,
67
+ role: database.owner_name,
68
+ ...pointInTimeParams(pointInTime),
69
+ })
70
+ .then((response) => response.data.sql ?? '');
71
+ }
72
+ catch (error) {
73
+ if (isAxiosError(error)) {
74
+ const data = error.response?.data;
75
+ sendError(error, 'API_ERROR');
76
+ throw new Error(data.message ??
77
+ `Error while fetching schema for branch ${pointInTime.branchId}`);
78
+ }
79
+ throw error;
80
+ }
81
+ };
82
+ const colorize = (patch) => {
83
+ return patch
84
+ .replace(/^([^\n]+)\n([^\n]+)\n/m, '') // Remove first two lines
85
+ .replace(/^-.*/gm, colorizer('removed'))
86
+ .replace(/^\+.*/gm, colorizer('added'))
87
+ .replace(/^@@.+@@.*/gm, colorizer('section'));
88
+ };
89
+ const colorizer = (colorId) => {
90
+ const color = COLORS[colorId];
91
+ return (line) => color(line);
92
+ };
93
+ const pointInTimeParams = (pointInTime) => {
94
+ switch (pointInTime.tag) {
95
+ case 'timestamp':
96
+ return {
97
+ timestamp: pointInTime.timestamp,
98
+ };
99
+ case 'lsn':
100
+ return {
101
+ lsn: pointInTime.lsn ?? undefined,
102
+ };
103
+ default:
104
+ return {};
105
+ }
106
+ };
107
+ const generateHeader = (pointInTime) => {
108
+ const header = `(Branch: ${pointInTime.branchId}`;
109
+ switch (pointInTime.tag) {
110
+ case 'timestamp':
111
+ return `${header} at ${pointInTime.timestamp})`;
112
+ case 'lsn':
113
+ return `${header} at ${pointInTime.lsn})`;
114
+ default:
115
+ return `${header})`;
116
+ }
117
+ };
118
+ /*
119
+ The command has two positional optional arguments - [base-branch] and [compare-source]
120
+ If only one argument is specified, we should consider it as `compare-source`
121
+ and `base-branch` will be either read from context or the default branch of project.
122
+ If no branches are specified, compare the context branch with its parent
123
+ */
124
+ export const parseSchemaDiffParams = async (props) => {
125
+ if (!props.compareSource) {
126
+ if (props.baseBranch) {
127
+ props.compareSource = props.baseBranch;
128
+ props.baseBranch = props.branch;
129
+ }
130
+ else if (props.branch) {
131
+ const { data } = await props.apiClient.listProjectBranches(props.projectId);
132
+ const contextBranch = data.branches.find((b) => b.id === props.branch || b.name === props.branch);
133
+ if (contextBranch?.parent_id == undefined) {
134
+ throw new Error(`No branch specified. Your context branch (${props.branch}) has no parent, so no comparison is possible.`);
135
+ }
136
+ log.info(`No branches specified. Comparing your context branch '${props.branch}' with its parent`);
137
+ props.compareSource = '^parent';
138
+ }
139
+ else {
140
+ const { data } = await props.apiClient.listProjectBranches(props.projectId);
141
+ const defaultBranch = data.branches.find((b) => b.default);
142
+ if (defaultBranch?.parent_id == undefined) {
143
+ throw new Error('No branch specified. Include a base branch or add a set-context branch to continue. Your default branch has no parent, so no comparison is possible.');
144
+ }
145
+ log.info(`No branches specified. Comparing default branch with its parent`);
146
+ props.compareSource = '^parent';
147
+ }
148
+ }
149
+ return props;
150
+ };
@@ -1,5 +1,4 @@
1
1
  import { updateContextFile } from '../context.js';
2
- import { branchIdFromProps } from '../utils/enrichers.js';
3
2
  export const command = 'set-context';
4
3
  export const describe = 'Set the current context';
5
4
  export const builder = (argv) => argv.usage('$0 set-context [options]').options({
@@ -7,16 +6,10 @@ export const builder = (argv) => argv.usage('$0 set-context [options]').options(
7
6
  describe: 'Project ID',
8
7
  type: 'string',
9
8
  },
10
- branch: {
11
- describe: 'Branch ID or name',
12
- type: 'string',
13
- },
14
9
  });
15
10
  export const handler = async (props) => {
16
- const branchId = await branchIdFromProps(props);
17
11
  const context = {
18
12
  projectId: props.projectId,
19
- branchId,
20
13
  };
21
14
  updateContextFile(props.contextFile, context);
22
15
  };
@@ -28,24 +28,64 @@ describe('set_context', () => {
28
28
  });
29
29
  const overrideContextFile = join(tmpdir(), `neon_override_ctx_${Date.now()}`);
30
30
  testCliCommand({
31
- name: 'get branch id overrides context set branch',
31
+ name: 'get project id overrides context set project',
32
32
  before: async () => {
33
33
  writeFileSync(overrideContextFile, JSON.stringify({
34
- projectId: 'test',
35
- branchId: 'br-cloudy-branch-12345678',
34
+ projectId: 'new-project id',
36
35
  }));
37
36
  },
38
37
  after: async () => {
39
38
  rmSync(overrideContextFile);
40
39
  },
41
40
  args: [
42
- 'branches',
41
+ 'project',
43
42
  'get',
44
- 'br-sunny-branch-123456',
43
+ 'project-id-123',
44
+ '--context-file',
45
+ overrideContextFile,
46
+ ],
47
+ expected: {
48
+ snapshot: true,
49
+ },
50
+ });
51
+ testCliCommand({
52
+ name: 'set the branchId and projectId is from context',
53
+ before: async () => {
54
+ writeFileSync(overrideContextFile, JSON.stringify({
55
+ projectId: 'test',
56
+ branchId: 'test_branch',
57
+ }));
58
+ },
59
+ after: async () => {
60
+ rmSync(overrideContextFile);
61
+ },
62
+ args: ['databases', 'list', '--context-file', overrideContextFile],
63
+ expected: {
64
+ snapshot: true,
65
+ },
66
+ });
67
+ testCliCommand({
68
+ name: 'should not set branchId from context for non-context projectId',
69
+ before: async () => {
70
+ writeFileSync(overrideContextFile, JSON.stringify({
71
+ projectId: 'project-id-123',
72
+ branchId: 'test_branch',
73
+ }));
74
+ },
75
+ after: async () => {
76
+ rmSync(overrideContextFile);
77
+ },
78
+ args: [
79
+ 'databases',
80
+ 'list',
81
+ '--project-id',
82
+ 'test',
45
83
  '--context-file',
46
84
  overrideContextFile,
47
85
  ],
48
86
  expected: {
87
+ code: 1,
88
+ stderr: 'ERROR: Not Found',
49
89
  snapshot: true,
50
90
  },
51
91
  });
package/context.js CHANGED
@@ -36,12 +36,15 @@ export const enrichFromContext = (args) => {
36
36
  return;
37
37
  }
38
38
  const context = readContextFile(args.contextFile);
39
- if (!args.branch && !args.id && !args.name) {
40
- args.branch = context.branchId;
41
- }
42
39
  if (!args.projectId) {
43
40
  args.projectId = context.projectId;
44
41
  }
42
+ if (!args.branch &&
43
+ !args.id &&
44
+ !args.name &&
45
+ context.projectId === args.projectId) {
46
+ args.branch = context.branchId;
47
+ }
45
48
  };
46
49
  export const updateContextFile = (file, context) => {
47
50
  writeFileSync(file, JSON.stringify(context, null, 2));
package/index.js CHANGED
@@ -20,7 +20,7 @@ import { defaultClientID } from './auth.js';
20
20
  import { fillInArgs } from './utils/middlewares.js';
21
21
  import pkg from './pkg.js';
22
22
  import commands from './commands/index.js';
23
- import { analyticsMiddleware, sendError } from './analytics.js';
23
+ import { analyticsMiddleware, closeAnalytics, sendError } from './analytics.js';
24
24
  import { isAxiosError } from 'axios';
25
25
  import { matchErrorCode } from './errors.js';
26
26
  import { showHelp } from './help.js';
@@ -96,6 +96,12 @@ builder = builder
96
96
  type: 'string',
97
97
  default: currentContextFile,
98
98
  },
99
+ color: {
100
+ group: 'Global options:',
101
+ describe: 'Colorize the output. Example: --no-color, --color false',
102
+ type: 'boolean',
103
+ default: true,
104
+ },
99
105
  })
100
106
  .middleware((args) => fillInArgs(args), true)
101
107
  .help(false)
@@ -156,13 +162,20 @@ builder = builder
156
162
  sendError(err || new Error(msg), matchErrorCode(msg || err?.message));
157
163
  log.error(msg || err?.message);
158
164
  }
165
+ await closeAnalytics();
159
166
  err?.stack && log.debug('Stack: %s', err.stack);
160
167
  process.exit(1);
161
168
  });
162
169
  (async () => {
163
- const args = await builder.argv;
164
- if (args._.length === 0 || args.help) {
165
- await showHelp(builder);
166
- process.exit(0);
170
+ try {
171
+ const args = await builder.argv;
172
+ if (args._.length === 0 || args.help) {
173
+ await showHelp(builder);
174
+ process.exit(0);
175
+ }
176
+ await closeAnalytics();
177
+ }
178
+ catch {
179
+ // noop
167
180
  }
168
181
  })();
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": "1.29.5",
8
+ "version": "1.31.0",
9
9
  "description": "CLI tool for NeonDB Cloud management",
10
10
  "main": "index.js",
11
11
  "author": "NeonDB",
@@ -29,6 +29,7 @@
29
29
  "@semantic-release/exec": "^6.0.3",
30
30
  "@semantic-release/git": "^10.0.1",
31
31
  "@types/cli-table": "^0.3.0",
32
+ "@types/diff": "^5.2.1",
32
33
  "@types/express": "^4.17.17",
33
34
  "@types/inquirer": "^9.0.3",
34
35
  "@types/node": "^18.7.13",
@@ -53,12 +54,13 @@
53
54
  "typescript": "^4.7.4"
54
55
  },
55
56
  "dependencies": {
56
- "@neondatabase/api-client": "1.5.0",
57
+ "@neondatabase/api-client": "1.9.0",
57
58
  "@segment/analytics-node": "^1.0.0-beta.26",
58
59
  "axios": "^1.4.0",
59
60
  "axios-debug-log": "^1.0.0",
60
61
  "chalk": "^5.2.0",
61
62
  "cli-table": "^0.3.11",
63
+ "diff": "^5.2.0",
62
64
  "inquirer": "^9.2.6",
63
65
  "open": "^10.1.0",
64
66
  "openid-client": "^5.6.5",
package/parameters.gen.js CHANGED
@@ -30,10 +30,15 @@ export const projectCreateRequest = {
30
30
  description: "A list of IP addresses that are allowed to connect to the endpoint.",
31
31
  demandOption: false,
32
32
  },
33
+ 'project.settings.allowed_ips.protected_branches_only': {
34
+ type: "boolean",
35
+ description: "If true, the list will be applied only to protected branches.",
36
+ demandOption: false,
37
+ },
33
38
  'project.settings.allowed_ips.primary_branch_only': {
34
39
  type: "boolean",
35
- description: "If true, the list will be applied only to the primary branch.",
36
- demandOption: true,
40
+ description: "If true, the list will be applied only to the default branch.",
41
+ demandOption: false,
37
42
  },
38
43
  'project.settings.enable_logical_replication': {
39
44
  type: "boolean",
@@ -91,6 +96,11 @@ export const projectCreateRequest = {
91
96
  description: "The number of seconds to retain the point-in-time restore (PITR) backup history for this project.\nThe default is 604800 seconds (7 days).\n",
92
97
  demandOption: false,
93
98
  },
99
+ 'project.org_id': {
100
+ type: "string",
101
+ description: "Organization id in case the project created belongs to an organization.\nIf not present, project is owned by a user and not by org.\n",
102
+ demandOption: false,
103
+ },
94
104
  };
95
105
  export const projectUpdateRequest = {
96
106
  'project.settings.quota.active_time_seconds': {
@@ -123,10 +133,15 @@ export const projectUpdateRequest = {
123
133
  description: "A list of IP addresses that are allowed to connect to the endpoint.",
124
134
  demandOption: false,
125
135
  },
136
+ 'project.settings.allowed_ips.protected_branches_only': {
137
+ type: "boolean",
138
+ description: "If true, the list will be applied only to protected branches.",
139
+ demandOption: false,
140
+ },
126
141
  'project.settings.allowed_ips.primary_branch_only': {
127
142
  type: "boolean",
128
- description: "If true, the list will be applied only to the primary branch.",
129
- demandOption: true,
143
+ description: "If true, the list will be applied only to the default branch.",
144
+ demandOption: false,
130
145
  },
131
146
  'project.settings.enable_logical_replication': {
132
147
  type: "boolean",
@@ -157,7 +172,7 @@ export const branchCreateRequest = {
157
172
  },
158
173
  'branch.parent_id': {
159
174
  type: "string",
160
- description: "The `branch_id` of the parent branch. If omitted or empty, the branch will be created from the project's primary branch.\n",
175
+ description: "The `branch_id` of the parent branch. If omitted or empty, the branch will be created from the project's default branch.\n",
161
176
  demandOption: false,
162
177
  },
163
178
  'branch.name': {
@@ -25,11 +25,11 @@ export const branchIdFromProps = async (props) => {
25
25
  });
26
26
  }
27
27
  const { data } = await props.apiClient.listProjectBranches(props.projectId);
28
- const primaryBranch = data.branches.find((b) => b.primary);
29
- if (primaryBranch) {
30
- return primaryBranch.id;
28
+ const defaultBranch = data.branches.find((b) => b.default);
29
+ if (defaultBranch) {
30
+ return defaultBranch.id;
31
31
  }
32
- throw new Error('No primary branch found');
32
+ throw new Error('No default branch found');
33
33
  };
34
34
  export const fillSingleProject = async (props) => {
35
35
  if (props.projectId) {
@@ -18,8 +18,14 @@ export const parsePITBranch = (input) => {
18
18
  ? { tag: 'lsn', lsn: exactPIT }
19
19
  : { tag: 'timestamp', timestamp: exactPIT }),
20
20
  };
21
- if (result.tag === 'timestamp' && !looksLikeTimestamp(result.timestamp)) {
22
- throw new PointInTimeParseError('Invalid source branch format');
21
+ if (result.tag === 'timestamp') {
22
+ const timestamp = result.timestamp;
23
+ if (!looksLikeTimestamp(timestamp)) {
24
+ throw new PointInTimeParseError(`Invalid source branch format - ${input}`);
25
+ }
26
+ if (Date.parse(timestamp) > Date.now()) {
27
+ throw new PointInTimeParseError(`Timestamp can not be in future - ${input}`);
28
+ }
23
29
  }
24
30
  return result;
25
31
  };
package/writer.js CHANGED
@@ -71,6 +71,9 @@ export const writer = (props) => {
71
71
  chunks.push({ data, config });
72
72
  return this;
73
73
  },
74
+ text(data) {
75
+ return out.write(data);
76
+ },
74
77
  end: (...args) => {
75
78
  if (args.length === 2) {
76
79
  chunks.push({ data: args[0], config: args[1] });