neonctl 1.31.0 → 1.32.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.
@@ -0,0 +1,104 @@
1
+ import { test, expect, describe, beforeAll, afterAll } from 'vitest';
2
+ import { runMockServer } from '../../test_utils/mock_server.js';
3
+ import { fork } from 'node:child_process';
4
+ import { join } from 'node:path';
5
+ import { log } from '../../log';
6
+ describe('bootstrap/create-app', () => {
7
+ let server;
8
+ const mockDir = 'main';
9
+ beforeAll(async () => {
10
+ server = await runMockServer(mockDir);
11
+ });
12
+ afterAll(async () => {
13
+ return new Promise((resolve) => {
14
+ server.close(() => {
15
+ resolve();
16
+ });
17
+ });
18
+ });
19
+ // We create an app without a schema and without deploying it, as
20
+ // a very simple check that the CLI works. Eventually, we need
21
+ // to have a much more complete test suite that actually verifies
22
+ // that launching all different app combinations works.
23
+ test.skip('very simple CLI interaction test', async () => {
24
+ // Most of this forking code is copied from `test_cli_command.ts`.
25
+ const cp = fork(join(process.cwd(), './dist/index.js'), [
26
+ '--api-host',
27
+ `http://localhost:${server.address().port}`,
28
+ '--output',
29
+ 'yaml',
30
+ '--api-key',
31
+ 'test-key',
32
+ '--no-analytics',
33
+ 'create-app',
34
+ ], {
35
+ stdio: 'pipe',
36
+ env: {
37
+ PATH: `mocks/bin:${process.env.PATH}`,
38
+ },
39
+ });
40
+ process.on('SIGINT', () => {
41
+ cp.kill();
42
+ });
43
+ let neonProjectCreated = false;
44
+ return new Promise((resolve, reject) => {
45
+ cp.stdout?.on('data', (data) => {
46
+ const stdout = data.toString();
47
+ log.info(stdout);
48
+ // For some unknown, weird reason, when we send TAB clicks (\t),
49
+ // they only affect the next question. So, we send TAB below
50
+ // in order to affect the answer to the following prompt, not the
51
+ // current one.
52
+ if (stdout.includes('What is your project named')) {
53
+ cp.stdin?.write('my-app\n');
54
+ }
55
+ else if (stdout.includes('Which package manager would you like to use')) {
56
+ cp.stdin?.write('\n');
57
+ }
58
+ else if (stdout.includes('What framework would you like to use')) {
59
+ cp.stdin?.write('\n');
60
+ }
61
+ else if (stdout.includes('What ORM would you like to use')) {
62
+ cp.stdin?.write('\t'); // change auth.js
63
+ cp.stdin?.write('\n');
64
+ }
65
+ else if (stdout.includes('What authentication framework do you want to use')) {
66
+ cp.stdin?.write('\n');
67
+ }
68
+ else if (stdout.includes('What Neon project would you like to use')) {
69
+ neonProjectCreated = true;
70
+ cp.stdin?.write('\t'); // change deployment
71
+ cp.stdin?.write('\t');
72
+ cp.stdin?.write('\n');
73
+ }
74
+ else if (stdout.includes('Where would you like to deploy')) {
75
+ cp.stdin?.write('\n');
76
+ cp.stdin?.write('\n');
77
+ }
78
+ });
79
+ cp.stderr?.on('data', (data) => {
80
+ log.error(data.toString());
81
+ });
82
+ cp.on('error', (err) => {
83
+ throw err;
84
+ });
85
+ cp.on('close', (code) => {
86
+ // If we got to the point that a Neon project was successfully
87
+ // created, we consider the test run to be a success. We can't
88
+ // currently check that the template is properly generated, and that
89
+ // the project runs. We'll have to do that with containerization in
90
+ // the future, most likely.
91
+ if (neonProjectCreated) {
92
+ resolve();
93
+ }
94
+ try {
95
+ expect(code).toBe(0);
96
+ resolve();
97
+ }
98
+ catch (err) {
99
+ reject(err);
100
+ }
101
+ });
102
+ });
103
+ }, 1000 * 60 * 5);
104
+ });
@@ -0,0 +1,56 @@
1
+ // Code copied from `create-next-app`.
2
+ import { lstatSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import chalk from 'chalk';
5
+ // `isFolderEmpty` checks if a folder is empty and ready to onboard a Next.js package into it.
6
+ // It will actually log to stdout as part of its execution.
7
+ export function isFolderEmpty(root, name, writeStdout) {
8
+ const validFiles = new Set([
9
+ '.DS_Store',
10
+ '.git',
11
+ '.gitattributes',
12
+ '.gitignore',
13
+ '.gitlab-ci.yml',
14
+ '.hg',
15
+ '.hgcheck',
16
+ '.hgignore',
17
+ '.idea',
18
+ '.npmignore',
19
+ '.travis.yml',
20
+ 'LICENSE',
21
+ 'Thumbs.db',
22
+ 'docs',
23
+ 'mkdocs.yml',
24
+ 'npm-debug.log',
25
+ 'yarn-debug.log',
26
+ 'yarn-error.log',
27
+ 'yarnrc.yml',
28
+ '.yarn',
29
+ ]);
30
+ const conflicts = readdirSync(root).filter((file) => !validFiles.has(file) &&
31
+ // Support IntelliJ IDEA-based editors
32
+ !/\.iml$/.test(file));
33
+ if (conflicts.length > 0) {
34
+ writeStdout(`The directory ${chalk.green(name)} contains files that could conflict:\n`);
35
+ writeStdout('');
36
+ for (const file of conflicts) {
37
+ try {
38
+ const stats = lstatSync(join(root, file));
39
+ if (stats.isDirectory()) {
40
+ writeStdout(` ${chalk.blue(file)}/\n`);
41
+ }
42
+ else {
43
+ writeStdout(` ${file}\n`);
44
+ }
45
+ }
46
+ catch {
47
+ writeStdout(` ${file}\n`);
48
+ }
49
+ }
50
+ writeStdout('\n');
51
+ writeStdout('Either try using a new directory name, or remove the files listed above.\n');
52
+ writeStdout('\n');
53
+ return false;
54
+ }
55
+ return true;
56
+ }
@@ -0,0 +1,15 @@
1
+ // Code copied from `create-next-app`.
2
+ import validateProjectName from 'validate-npm-package-name';
3
+ export function validateNpmName(name) {
4
+ const nameValidation = validateProjectName(name);
5
+ if (nameValidation.validForNewPackages) {
6
+ return { valid: true };
7
+ }
8
+ return {
9
+ valid: false,
10
+ problems: [
11
+ ...(nameValidation.errors || []),
12
+ ...(nameValidation.warnings || []),
13
+ ],
14
+ };
15
+ }
@@ -8,6 +8,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
10
  import { parseSchemaDiffParams, schemaDiff } from './schema_diff.js';
11
+ import { getComputeUnits } from '../utils/compute_units.js';
11
12
  const BRANCH_FIELDS = [
12
13
  'id',
13
14
  'name',
@@ -61,6 +62,11 @@ export const builder = (argv) => argv
61
62
  implies: 'compute',
62
63
  default: 0,
63
64
  },
65
+ cu: {
66
+ describe: 'The number of Compute Units. Could be a fixed size (e.g. "2") or a range delimited by a dash (e.g. "0.5-3").',
67
+ type: 'string',
68
+ implies: 'compute',
69
+ },
64
70
  psql: {
65
71
  type: 'boolean',
66
72
  describe: 'Connect to a new branch via psql',
@@ -119,6 +125,10 @@ export const builder = (argv) => argv
119
125
  describe: 'Type of compute to add',
120
126
  default: EndpointType.ReadOnly,
121
127
  },
128
+ cu: {
129
+ describe: 'The number of Compute Units. Could be a fixed size (e.g. "2") or a range delimited by a dash (e.g. "0.5-3").',
130
+ type: 'string',
131
+ },
122
132
  }), async (args) => await addCompute(args))
123
133
  .command('delete <id|name>', 'Delete a branch', (yargs) => yargs, async (args) => await deleteBranch(args))
124
134
  .command('get <id|name>', 'Get a branch', (yargs) => yargs, async (args) => await get(args))
@@ -213,6 +223,7 @@ const create = async (props) => {
213
223
  {
214
224
  type: props.type,
215
225
  suspend_timeout_seconds: props.suspendTimeout === 0 ? undefined : props.suspendTimeout,
226
+ ...(props.cu ? getComputeUnits(props.cu) : undefined),
216
227
  },
217
228
  ]
218
229
  : [],
@@ -282,6 +293,7 @@ const addCompute = async (props) => {
282
293
  endpoint: {
283
294
  branch_id: branchId,
284
295
  type: props.type,
296
+ ...(props.cu ? getComputeUnits(props.cu) : undefined),
285
297
  },
286
298
  }));
287
299
  writer(props).end(data.endpoint, {
@@ -1,4 +1,4 @@
1
- import { describe } from '@jest/globals';
1
+ import { describe } from 'vitest';
2
2
  import { testCliCommand } from '../test_utils/test_cli_command.js';
3
3
  describe('branches', () => {
4
4
  /* list */
@@ -152,6 +152,38 @@ describe('branches', () => {
152
152
  snapshot: true,
153
153
  },
154
154
  });
155
+ testCliCommand({
156
+ name: 'create with fixed size CU',
157
+ args: [
158
+ 'branches',
159
+ 'create',
160
+ '--project-id',
161
+ 'test',
162
+ '--name',
163
+ 'test_branch_with_fixed_cu',
164
+ '--cu',
165
+ '2',
166
+ ],
167
+ expected: {
168
+ snapshot: true,
169
+ },
170
+ });
171
+ testCliCommand({
172
+ name: 'create with autoscaled CU',
173
+ args: [
174
+ 'branches',
175
+ 'create',
176
+ '--project-id',
177
+ 'test',
178
+ '--name',
179
+ 'test_branch_with_autoscaling',
180
+ '--cu',
181
+ '0.5-2',
182
+ ],
183
+ expected: {
184
+ snapshot: true,
185
+ },
186
+ });
155
187
  /* delete */
156
188
  testCliCommand({
157
189
  name: 'delete by id',
@@ -252,6 +284,36 @@ describe('branches', () => {
252
284
  snapshot: true,
253
285
  },
254
286
  });
287
+ testCliCommand({
288
+ name: 'add compute with fixed size CU',
289
+ args: [
290
+ 'branches',
291
+ 'add-compute',
292
+ 'test_branch_with_fixed_cu',
293
+ '--project-id',
294
+ 'test',
295
+ '--cu',
296
+ '2',
297
+ ],
298
+ expected: {
299
+ snapshot: true,
300
+ },
301
+ });
302
+ testCliCommand({
303
+ name: 'add compute with autoscaled CU',
304
+ args: [
305
+ 'branches',
306
+ 'add-compute',
307
+ 'test_branch_with_autoscaling',
308
+ '--project-id',
309
+ 'test',
310
+ '--cu',
311
+ '0.5-2',
312
+ ],
313
+ expected: {
314
+ snapshot: true,
315
+ },
316
+ });
255
317
  /* reset */
256
318
  testCliCommand({
257
319
  name: 'reset branch to parent',
@@ -1,4 +1,4 @@
1
- import { describe, expect } from '@jest/globals';
1
+ import { describe, expect } from 'vitest';
2
2
  import { testCliCommand } from '../test_utils/test_cli_command';
3
3
  describe('connection_string', () => {
4
4
  testCliCommand({
@@ -1,4 +1,4 @@
1
- import { describe } from '@jest/globals';
1
+ import { describe } from 'vitest';
2
2
  import { testCliCommand } from '../test_utils/test_cli_command.js';
3
3
  describe('databases', () => {
4
4
  testCliCommand({
@@ -1,4 +1,4 @@
1
- import { describe, expect } from '@jest/globals';
1
+ import { describe, expect } from 'vitest';
2
2
  import { testCliCommand } from '../test_utils/test_cli_command.js';
3
3
  describe('help', () => {
4
4
  testCliCommand({
package/commands/index.js CHANGED
@@ -9,6 +9,7 @@ import * as roles from './roles.js';
9
9
  import * as operations from './operations.js';
10
10
  import * as cs from './connection_string.js';
11
11
  import * as setContext from './set_context.js';
12
+ import * as bootstrap from './bootstrap/index.js';
12
13
  export default [
13
14
  auth,
14
15
  users,
@@ -21,4 +22,5 @@ export default [
21
22
  operations,
22
23
  cs,
23
24
  setContext,
25
+ bootstrap,
24
26
  ];
@@ -6,7 +6,7 @@ const IP_ALLOW_FIELDS = [
6
6
  'id',
7
7
  'name',
8
8
  'IP_addresses',
9
- 'primary_branch_only',
9
+ 'protected_branches_only',
10
10
  ];
11
11
  export const command = 'ip-allow';
12
12
  export const describe = 'Manage IP Allow';
@@ -30,11 +30,18 @@ export const builder = (argv) => {
30
30
  type: 'string',
31
31
  default: [],
32
32
  array: true,
33
+ })
34
+ .options({
35
+ 'protected-only': {
36
+ describe: projectUpdateRequest['project.settings.allowed_ips.protected_branches_only'].description,
37
+ type: 'boolean',
38
+ },
33
39
  })
34
40
  .options({
35
41
  'primary-only': {
36
42
  describe: projectUpdateRequest['project.settings.allowed_ips.primary_branch_only'].description,
37
43
  type: 'boolean',
44
+ deprecated: 'See --protected-only',
38
45
  },
39
46
  }), async (args) => {
40
47
  await add(args);
@@ -77,6 +84,9 @@ const add = async (props) => {
77
84
  allowed_ips: {
78
85
  ips: [...new Set(props.ips.concat(existingAllowedIps?.ips ?? []))],
79
86
  primary_branch_only: props.primaryOnly ?? existingAllowedIps?.primary_branch_only ?? false,
87
+ protected_branches_only: props.protectedOnly ??
88
+ existingAllowedIps?.protected_branches_only ??
89
+ false,
80
90
  },
81
91
  };
82
92
  const { data: response } = await props.apiClient.updateProject(props.projectId, {
@@ -97,6 +107,7 @@ const remove = async (props) => {
97
107
  allowed_ips: {
98
108
  ips: existingAllowedIps?.ips?.filter((ip) => !props.ips.includes(ip)) ?? [],
99
109
  primary_branch_only: existingAllowedIps?.primary_branch_only ?? false,
110
+ protected_branches_only: existingAllowedIps?.protected_branches_only ?? false,
100
111
  },
101
112
  };
102
113
  const { data: response } = await props.apiClient.updateProject(props.projectId, {
@@ -112,6 +123,7 @@ const reset = async (props) => {
112
123
  allowed_ips: {
113
124
  ips: props.ips,
114
125
  primary_branch_only: false,
126
+ protected_branches_only: false,
115
127
  },
116
128
  };
117
129
  const { data } = await props.apiClient.updateProject(props.projectId, {
@@ -131,5 +143,6 @@ const parse = (project) => {
131
143
  name: project.name,
132
144
  IP_addresses: ips,
133
145
  primary_branch_only: project.settings?.allowed_ips?.primary_branch_only ?? false,
146
+ protected_branches_only: project.settings?.allowed_ips?.protected_branches_only ?? false,
134
147
  };
135
148
  };
@@ -1,4 +1,4 @@
1
- import { describe } from '@jest/globals';
1
+ import { describe } from 'vitest';
2
2
  import { testCliCommand } from '../test_utils/test_cli_command.js';
3
3
  describe('ip-allow', () => {
4
4
  testCliCommand({
@@ -26,7 +26,7 @@ describe('ip-allow', () => {
26
26
  },
27
27
  });
28
28
  testCliCommand({
29
- name: 'Add IP allow',
29
+ name: 'Add IP allow - Primary',
30
30
  args: [
31
31
  'ip-allow',
32
32
  'add',
@@ -40,6 +40,21 @@ describe('ip-allow', () => {
40
40
  snapshot: true,
41
41
  },
42
42
  });
43
+ testCliCommand({
44
+ name: 'Add IP allow - Protected',
45
+ args: [
46
+ 'ip-allow',
47
+ 'add',
48
+ '127.0.0.1',
49
+ '192.168.10.1-192.168.10.15',
50
+ '--protected-only',
51
+ '--project-id',
52
+ 'test',
53
+ ],
54
+ expected: {
55
+ snapshot: true,
56
+ },
57
+ });
43
58
  testCliCommand({
44
59
  name: 'Remove IP allow - Error',
45
60
  args: ['ip-allow', 'remove', '--project-id', 'test'],
@@ -64,6 +79,7 @@ describe('ip-allow', () => {
64
79
  name: test_project
65
80
  IP_addresses: []
66
81
  primary_branch_only: false
82
+ protected_branches_only: false
67
83
  `,
68
84
  stderr: `INFO: The IP allowlist has been reset. All databases on project "test_project" are now exposed to the internet`,
69
85
  },
@@ -1,4 +1,4 @@
1
- import { describe } from '@jest/globals';
1
+ import { describe } from 'vitest';
2
2
  import { testCliCommand } from '../test_utils/test_cli_command.js';
3
3
  describe('operations', () => {
4
4
  testCliCommand({
@@ -0,0 +1,11 @@
1
+ import { describe } from 'vitest';
2
+ import { testCliCommand } from '../test_utils/test_cli_command.js';
3
+ describe('orgs', () => {
4
+ testCliCommand({
5
+ name: 'list',
6
+ args: ['orgs', 'list'],
7
+ expected: {
8
+ snapshot: true,
9
+ },
10
+ });
11
+ });
@@ -3,7 +3,13 @@ import { projectCreateRequest, projectUpdateRequest, } from '../parameters.gen.j
3
3
  import { writer } from '../writer.js';
4
4
  import { psql } from '../utils/psql.js';
5
5
  import { updateContextFile } from '../context.js';
6
- const PROJECT_FIELDS = ['id', 'name', 'region_id', 'created_at'];
6
+ import { getComputeUnits } from '../utils/compute_units.js';
7
+ export const PROJECT_FIELDS = [
8
+ 'id',
9
+ 'name',
10
+ 'region_id',
11
+ 'created_at',
12
+ ];
7
13
  const REGIONS = [
8
14
  'aws-us-west-2',
9
15
  'aws-ap-southeast-1',
@@ -48,6 +54,10 @@ export const builder = (argv) => {
48
54
  describe: 'Set the current context to the new project',
49
55
  default: false,
50
56
  },
57
+ cu: {
58
+ describe: 'The number of Compute Units. Could be a fixed size (e.g. "2") or a range delimited by a dash (e.g. "0.5-3").',
59
+ type: 'string',
60
+ },
51
61
  }), async (args) => {
52
62
  await create(args);
53
63
  })
@@ -68,6 +78,10 @@ export const builder = (argv) => {
68
78
  type: 'boolean',
69
79
  deprecated: "Deprecated. Use 'ip-allow' command",
70
80
  },
81
+ cu: {
82
+ describe: 'The number of Compute Units. Could be a fixed size (e.g. "2") or a range delimited by a dash (e.g. "0.5-3").',
83
+ type: 'string',
84
+ },
71
85
  }), async (args) => {
72
86
  await update(args);
73
87
  })
@@ -130,13 +144,17 @@ const create = async (props) => {
130
144
  if (props.role) {
131
145
  project.branch.role_name = props.role;
132
146
  }
147
+ if (props.cu) {
148
+ project.default_endpoint_settings = props.cu
149
+ ? getComputeUnits(props.cu)
150
+ : undefined;
151
+ }
133
152
  const { data } = await props.apiClient.createProject({
134
153
  project,
135
154
  });
136
155
  if (props.setContext) {
137
156
  updateContextFile(props.contextFile, {
138
157
  projectId: data.project.id,
139
- branchId: data.branch.id,
140
158
  });
141
159
  }
142
160
  const out = writer(props);
@@ -175,6 +193,11 @@ const update = async (props) => {
175
193
  },
176
194
  };
177
195
  }
196
+ if (props.cu) {
197
+ project.default_endpoint_settings = props.cu
198
+ ? getComputeUnits(props.cu)
199
+ : undefined;
200
+ }
178
201
  const { data } = await props.apiClient.updateProject(props.id, {
179
202
  project,
180
203
  });
@@ -1,4 +1,4 @@
1
- import { afterAll, describe, expect, test } from '@jest/globals';
1
+ import { afterAll, describe, expect, test } from 'vitest';
2
2
  import { readFileSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
@@ -73,6 +73,34 @@ describe('projects', () => {
73
73
  snapshot: true,
74
74
  },
75
75
  });
76
+ testCliCommand({
77
+ name: 'create project with default fixed size CU',
78
+ args: [
79
+ 'projects',
80
+ 'create',
81
+ '--name',
82
+ 'test_project_with_fixed_cu',
83
+ '--cu',
84
+ '2',
85
+ ],
86
+ expected: {
87
+ snapshot: true,
88
+ },
89
+ });
90
+ testCliCommand({
91
+ name: 'create project with default autoscaled CU',
92
+ args: [
93
+ 'projects',
94
+ 'create',
95
+ '--name',
96
+ 'test_project_with_autoscaling',
97
+ '--cu',
98
+ '0.5-2',
99
+ ],
100
+ expected: {
101
+ snapshot: true,
102
+ },
103
+ });
76
104
  afterAll(() => {
77
105
  rmSync(CONTEXT_FILE);
78
106
  });
@@ -122,6 +150,26 @@ describe('projects', () => {
122
150
  snapshot: true,
123
151
  },
124
152
  });
153
+ testCliCommand({
154
+ name: 'update project with default fixed size CU',
155
+ args: ['projects', 'update', 'test_project_with_fixed_cu', '--cu', '2'],
156
+ expected: {
157
+ snapshot: true,
158
+ },
159
+ });
160
+ testCliCommand({
161
+ name: 'update project with default autoscaled CU',
162
+ args: [
163
+ 'projects',
164
+ 'update',
165
+ 'test_project_with_autoscaling',
166
+ '--cu',
167
+ '0.5-2',
168
+ ],
169
+ expected: {
170
+ snapshot: true,
171
+ },
172
+ });
125
173
  testCliCommand({
126
174
  name: 'get',
127
175
  args: ['projects', 'get', 'test'],
@@ -1,4 +1,4 @@
1
- import { describe } from '@jest/globals';
1
+ import { describe } from 'vitest';
2
2
  import { testCliCommand } from '../test_utils/test_cli_command.js';
3
3
  describe('roles', () => {
4
4
  testCliCommand({
@@ -1,7 +1,7 @@
1
1
  import { tmpdir } from 'node:os';
2
2
  import { join } from 'node:path';
3
3
  import { rmSync, writeFileSync } from 'node:fs';
4
- import { afterAll, describe } from '@jest/globals';
4
+ import { afterAll, describe } from 'vitest';
5
5
  import { testCliCommand } from '../test_utils/test_cli_command';
6
6
  const CONTEXT_FILE = join(tmpdir(), `neon_${Date.now()}`);
7
7
  describe('set_context', () => {
package/index.js CHANGED
@@ -26,11 +26,16 @@ import { matchErrorCode } from './errors.js';
26
26
  import { showHelp } from './help.js';
27
27
  import { currentContextFile, enrichFromContext } from './context.js';
28
28
  const NO_SUBCOMMANDS_VERBS = [
29
+ // aliases
29
30
  'auth',
30
31
  'me',
32
+ // aliases
31
33
  'cs',
32
34
  'connection-string',
33
35
  'set-context',
36
+ // aliases
37
+ 'create-app',
38
+ 'bootstrap',
34
39
  ];
35
40
  let builder = yargs(hideBin(process.argv));
36
41
  builder = builder