neonctl 1.28.0 → 1.29.1

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.
Files changed (184) hide show
  1. package/.bump +1 -0
  2. package/.editorconfig +7 -0
  3. package/.eslintrc.cjs +15 -0
  4. package/.github/workflows/commitlint.yml +46 -0
  5. package/.github/workflows/pr.yml +25 -0
  6. package/.github/workflows/release.yml +30 -0
  7. package/.husky/commit-msg +4 -0
  8. package/.husky/pre-commit +4 -0
  9. package/.nvmrc +1 -0
  10. package/.prettierignore +3 -0
  11. package/.prettierrc.json +3 -0
  12. package/.releaserc.json +47 -0
  13. package/LICENSE +202 -0
  14. package/commitlint.config.cjs +7 -0
  15. package/generateOptionsFromSpec.ts +68 -0
  16. package/jest/setup.js +5 -0
  17. package/jest.config.ts +199 -0
  18. package/mocks/bin/psql.cjs +9 -0
  19. package/mocks/main/projects/GET.js +27 -0
  20. package/mocks/main/projects/POST.js +22 -0
  21. package/mocks/main/projects/shared/GET.js +16 -0
  22. package/mocks/main/projects/test/DELETE.json +7 -0
  23. package/mocks/main/projects/test/GET.json +13 -0
  24. package/mocks/main/projects/test/PATCH.js +18 -0
  25. package/mocks/main/projects/test/branches/GET.json +25 -0
  26. package/mocks/main/projects/test/branches/POST.js +83 -0
  27. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/DELETE.json +7 -0
  28. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/GET.json +9 -0
  29. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/PATCH.js +14 -0
  30. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/GET.json +6 -0
  31. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/POST.js +13 -0
  32. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/test_db/DELETE.json +6 -0
  33. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/GET.json +26 -0
  34. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/POST.json +6 -0
  35. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/GET.json +3 -0
  36. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/POST.js +14 -0
  37. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/DELETE.json +6 -0
  38. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/reveal_password/GET.json +3 -0
  39. package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/set_as_primary/POST.json +9 -0
  40. package/mocks/main/projects/test/branches/br-numbered-branch-123456/GET.json +10 -0
  41. package/mocks/main/projects/test/branches/br-sunny-branch-123456/DELETE.json +7 -0
  42. package/mocks/main/projects/test/branches/br-sunny-branch-123456/GET.json +10 -0
  43. package/mocks/main/projects/test/branches/br-sunny-branch-123456/PATCH.js +14 -0
  44. package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/GET.json +6 -0
  45. package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/POST.js +13 -0
  46. package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/test_db/DELETE.json +6 -0
  47. package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/GET.json +26 -0
  48. package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/POST.json +6 -0
  49. package/mocks/main/projects/test/branches/br-sunny-branch-123456/restore/POST.js +16 -0
  50. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/GET.json +3 -0
  51. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/POST.js +14 -0
  52. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/DELETE.json +6 -0
  53. package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/reveal_password/GET.json +3 -0
  54. package/mocks/main/projects/test/branches/br-sunny-branch-123456/set_as_primary/POST.json +9 -0
  55. package/mocks/main/projects/test/endpoints/GET.json +9 -0
  56. package/mocks/main/projects/test/endpoints/POST.js +32 -0
  57. package/mocks/main/projects/test/endpoints/test_endpoint_id/DELETE.json +7 -0
  58. package/mocks/main/projects/test/endpoints/test_endpoint_id/GET.json +9 -0
  59. package/mocks/main/projects/test/endpoints/test_endpoint_id/PATCH.js +17 -0
  60. package/mocks/main/projects/test/operations/GET.json +22 -0
  61. package/mocks/main/users/me/GET.json +5 -0
  62. package/mocks/restore/projects/test/branches/GET.json +21 -0
  63. package/mocks/restore/projects/test/branches/br-another-branch-123456/GET.json +6 -0
  64. package/mocks/restore/projects/test/branches/br-another-branch-123456/restore/POST.js +13 -0
  65. package/mocks/restore/projects/test/branches/br-any-branch-123456/GET.json +6 -0
  66. package/mocks/restore/projects/test/branches/br-parent-tots-123456/GET.json +7 -0
  67. package/mocks/restore/projects/test/branches/br-parent-tots-123456/restore/POST.js +14 -0
  68. package/mocks/restore/projects/test/branches/br-self-tolsn-123456/GET.json +6 -0
  69. package/mocks/restore/projects/test/branches/br-self-tolsn-123456/restore/POST.js +15 -0
  70. package/mocks/single_project/projects/GET.json +10 -0
  71. package/mocks/single_project/projects/test-project-123456/GET.json +14 -0
  72. package/mocks/single_project/projects/test-project-123456/branches/GET.json +11 -0
  73. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/databases/GET.json +3 -0
  74. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/endpoints/GET.json +10 -0
  75. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/GET.json +3 -0
  76. package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/test_role/reveal_password/GET.json +3 -0
  77. package/package.json +6 -5
  78. package/pkg.js +45 -3
  79. package/rollup.config.js +20 -0
  80. package/snapshots/commands/branches.test.snap +221 -0
  81. package/snapshots/commands/connection_string.test.snap +70 -0
  82. package/snapshots/commands/databases.test.snap +20 -0
  83. package/snapshots/commands/ip_allow.test.snap +55 -0
  84. package/snapshots/commands/operations.test.snap +17 -0
  85. package/snapshots/commands/projects.test.snap +141 -0
  86. package/snapshots/commands/roles.test.snap +19 -0
  87. package/snapshots/commands/set_context.test.snap +30 -0
  88. package/snapshots/writer.test.snap +60 -0
  89. package/snapshotsResolver.cjs +32 -0
  90. package/src/analytics.ts +95 -0
  91. package/src/api.ts +44 -0
  92. package/src/auth.ts +137 -0
  93. package/{cli.js → src/cli.ts} +1 -0
  94. package/src/commands/auth.test.ts +62 -0
  95. package/src/commands/auth.ts +148 -0
  96. package/src/commands/branches.test.ts +354 -0
  97. package/src/commands/branches.ts +451 -0
  98. package/src/commands/connection_string.test.ts +250 -0
  99. package/src/commands/connection_string.ts +210 -0
  100. package/src/commands/databases.test.ts +55 -0
  101. package/src/commands/databases.ts +129 -0
  102. package/src/commands/help.test.ts +13 -0
  103. package/{commands/index.js → src/commands/index.ts} +11 -10
  104. package/src/commands/ip_allow.test.ts +86 -0
  105. package/src/commands/ip_allow.ts +202 -0
  106. package/src/commands/operations.test.ts +13 -0
  107. package/src/commands/operations.ts +41 -0
  108. package/src/commands/projects.test.ts +147 -0
  109. package/src/commands/projects.ts +275 -0
  110. package/src/commands/roles.test.ts +46 -0
  111. package/src/commands/roles.ts +100 -0
  112. package/src/commands/set_context.test.ts +64 -0
  113. package/src/commands/set_context.ts +27 -0
  114. package/src/commands/user.ts +21 -0
  115. package/src/config.ts +22 -0
  116. package/src/context.ts +61 -0
  117. package/src/env.ts +7 -0
  118. package/src/errors.ts +24 -0
  119. package/src/help.ts +185 -0
  120. package/src/index.ts +180 -0
  121. package/src/log.ts +16 -0
  122. package/src/parameters.gen.ts +332 -0
  123. package/src/pkg.ts +9 -0
  124. package/src/test_utils/mock_server.ts +27 -0
  125. package/src/test_utils/oauth_server.ts +10 -0
  126. package/src/test_utils/test_cli_command.ts +117 -0
  127. package/src/types.ts +25 -0
  128. package/src/utils/enrichers.ts +73 -0
  129. package/src/utils/formats.test.ts +41 -0
  130. package/src/utils/formats.ts +11 -0
  131. package/src/utils/middlewares.ts +23 -0
  132. package/src/utils/point_in_time.ts +86 -0
  133. package/src/utils/psql.ts +29 -0
  134. package/src/utils/string.ts +8 -0
  135. package/src/utils/ui.ts +64 -0
  136. package/src/writer.test.ts +98 -0
  137. package/src/writer.ts +131 -0
  138. package/tsconfig.json +17 -0
  139. package/analytics.js +0 -78
  140. package/api.js +0 -35
  141. package/auth.js +0 -101
  142. package/commands/auth.js +0 -102
  143. package/commands/auth.test.js +0 -42
  144. package/commands/branches.js +0 -303
  145. package/commands/branches.test.js +0 -321
  146. package/commands/connection_string.js +0 -137
  147. package/commands/connection_string.test.js +0 -204
  148. package/commands/databases.js +0 -79
  149. package/commands/databases.test.js +0 -51
  150. package/commands/help.test.js +0 -11
  151. package/commands/ip_allow.js +0 -135
  152. package/commands/ip_allow.test.js +0 -78
  153. package/commands/operations.js +0 -28
  154. package/commands/operations.test.js +0 -11
  155. package/commands/projects.js +0 -170
  156. package/commands/projects.test.js +0 -132
  157. package/commands/roles.js +0 -57
  158. package/commands/roles.test.js +0 -42
  159. package/commands/set_context.js +0 -22
  160. package/commands/set_context.test.js +0 -53
  161. package/commands/user.js +0 -15
  162. package/config.js +0 -11
  163. package/context.js +0 -48
  164. package/env.js +0 -6
  165. package/errors.js +0 -16
  166. package/help.js +0 -146
  167. package/index.js +0 -168
  168. package/log.js +0 -15
  169. package/parameters.gen.js +0 -322
  170. package/test_utils/mock_server.js +0 -16
  171. package/test_utils/oauth_server.js +0 -9
  172. package/test_utils/test_cli_command.js +0 -80
  173. package/types.js +0 -1
  174. package/utils/enrichers.js +0 -49
  175. package/utils/formats.js +0 -5
  176. package/utils/formats.test.js +0 -32
  177. package/utils/middlewares.js +0 -20
  178. package/utils/point_in_time.js +0 -44
  179. package/utils/psql.js +0 -24
  180. package/utils/string.js +0 -5
  181. package/utils/ui.js +0 -59
  182. package/writer.js +0 -87
  183. package/writer.test.js +0 -86
  184. /package/{callback.html → src/callback.html} +0 -0
@@ -0,0 +1,202 @@
1
+ import yargs from 'yargs';
2
+ import { CommonProps, ProjectScopeProps } from '../types';
3
+ import { writer } from '../writer.js';
4
+ import { fillSingleProject } from '../utils/enrichers.js';
5
+ import { Project, ProjectUpdateRequest } from '@neondatabase/api-client';
6
+ import { projectUpdateRequest } from '../parameters.gen.js';
7
+ import { log } from '../log.js';
8
+
9
+ const IP_ALLOW_FIELDS = [
10
+ 'id',
11
+ 'name',
12
+ 'IP_addresses',
13
+ 'primary_branch_only',
14
+ ] as const;
15
+
16
+ export const command = 'ip-allow';
17
+ export const describe = 'Manage IP Allow';
18
+ export const builder = (argv: yargs.Argv) => {
19
+ return argv
20
+ .usage('$0 ip-allow <sub-command> [options]')
21
+ .options({
22
+ 'project-id': {
23
+ describe: 'Project ID',
24
+ type: 'string',
25
+ },
26
+ })
27
+ .middleware(fillSingleProject as any)
28
+ .command(
29
+ 'list',
30
+ 'List the IP allowlist',
31
+ (yargs) => yargs,
32
+ async (args) => {
33
+ await list(args as any);
34
+ },
35
+ )
36
+ .command(
37
+ 'add [ips...]',
38
+ 'Add IP addresses to the IP allowlist',
39
+ (yargs) =>
40
+ yargs
41
+ .usage('$0 ip-allow add [ips...]')
42
+ .positional('ips', {
43
+ describe: 'The list of IP addresses to add',
44
+ type: 'string',
45
+ default: [],
46
+ array: true,
47
+ })
48
+ .options({
49
+ 'primary-only': {
50
+ describe:
51
+ projectUpdateRequest[
52
+ 'project.settings.allowed_ips.primary_branch_only'
53
+ ].description,
54
+ type: 'boolean',
55
+ },
56
+ }),
57
+ async (args) => {
58
+ await add(args as any);
59
+ },
60
+ )
61
+ .command(
62
+ 'remove [ips...]',
63
+ 'Remove IP addresses from the IP allowlist',
64
+ (yargs) =>
65
+ yargs.usage('$0 ip-allow remove [ips...]').positional('ips', {
66
+ describe: 'The list of IP addresses to remove',
67
+ type: 'string',
68
+ default: [],
69
+ array: true,
70
+ }),
71
+ async (args) => {
72
+ await remove(args as any);
73
+ },
74
+ )
75
+ .command(
76
+ 'reset [ips...]',
77
+ 'Reset the IP allowlist',
78
+ (yargs) =>
79
+ yargs.usage('$0 ip-allow reset [ips...]').positional('ips', {
80
+ describe: 'The list of IP addresses to reset',
81
+ type: 'string',
82
+ default: [],
83
+ array: true,
84
+ }),
85
+ async (args) => {
86
+ await reset(args as any);
87
+ },
88
+ );
89
+ };
90
+
91
+ export const handler = (args: yargs.Argv) => {
92
+ return args;
93
+ };
94
+
95
+ const list = async (props: CommonProps & ProjectScopeProps) => {
96
+ const { data } = await props.apiClient.getProject(props.projectId);
97
+ writer(props).end(parse(data.project), {
98
+ fields: IP_ALLOW_FIELDS,
99
+ });
100
+ };
101
+
102
+ const add = async (
103
+ props: CommonProps &
104
+ ProjectScopeProps & {
105
+ ips: string[];
106
+ primaryOnly?: boolean;
107
+ },
108
+ ) => {
109
+ if (props.ips.length <= 0) {
110
+ throw new Error(`Enter individual IP addresses, define ranges with a dash, or use CIDR notation for more flexibility.
111
+ Example: neonctl ip-allow add 192.168.1.1, 192.168.1.20-192.168.1.50, 192.168.1.0/24 --project-id <id>`);
112
+ }
113
+
114
+ const project: ProjectUpdateRequest['project'] = {};
115
+ const { data } = await props.apiClient.getProject(props.projectId);
116
+ const existingAllowedIps = data.project.settings?.allowed_ips;
117
+
118
+ project.settings = {
119
+ allowed_ips: {
120
+ ips: [...new Set(props.ips.concat(existingAllowedIps?.ips ?? []))],
121
+ primary_branch_only:
122
+ props.primaryOnly ?? existingAllowedIps?.primary_branch_only ?? false,
123
+ },
124
+ };
125
+
126
+ const { data: response } = await props.apiClient.updateProject(
127
+ props.projectId,
128
+ {
129
+ project,
130
+ },
131
+ );
132
+
133
+ writer(props).end(parse(response.project), {
134
+ fields: IP_ALLOW_FIELDS,
135
+ });
136
+ };
137
+
138
+ const remove = async (props: ProjectScopeProps & { ips: string[] }) => {
139
+ if (props.ips.length <= 0) {
140
+ throw new Error(
141
+ `Remove individual IP addresses and ranges. Example: neonctl ip-allow remove 192.168.1.1 --project-id <id>`,
142
+ );
143
+ }
144
+
145
+ const project: ProjectUpdateRequest['project'] = {};
146
+ const { data } = await props.apiClient.getProject(props.projectId);
147
+ const existingAllowedIps = data.project.settings?.allowed_ips;
148
+
149
+ project.settings = {
150
+ allowed_ips: {
151
+ ips:
152
+ existingAllowedIps?.ips?.filter((ip) => !props.ips.includes(ip)) ?? [],
153
+ primary_branch_only: existingAllowedIps?.primary_branch_only ?? false,
154
+ },
155
+ };
156
+
157
+ const { data: response } = await props.apiClient.updateProject(
158
+ props.projectId,
159
+ {
160
+ project,
161
+ },
162
+ );
163
+
164
+ writer(props).end(parse(response.project), {
165
+ fields: IP_ALLOW_FIELDS,
166
+ });
167
+ };
168
+
169
+ const reset = async (props: ProjectScopeProps & { ips: string[] }) => {
170
+ const project: ProjectUpdateRequest['project'] = {};
171
+ project.settings = {
172
+ allowed_ips: {
173
+ ips: props.ips,
174
+ primary_branch_only: false,
175
+ },
176
+ };
177
+
178
+ const { data } = await props.apiClient.updateProject(props.projectId, {
179
+ project,
180
+ });
181
+
182
+ writer(props).end(parse(data.project), {
183
+ fields: IP_ALLOW_FIELDS,
184
+ });
185
+
186
+ if (props.ips.length <= 0) {
187
+ log.info(
188
+ `The IP allowlist has been reset. All databases on project "${data.project.name}" are now exposed to the internet`,
189
+ );
190
+ }
191
+ };
192
+
193
+ const parse = (project: Project) => {
194
+ const ips = project.settings?.allowed_ips?.ips ?? [];
195
+ return {
196
+ id: project.id,
197
+ name: project.name,
198
+ IP_addresses: ips,
199
+ primary_branch_only:
200
+ project.settings?.allowed_ips?.primary_branch_only ?? false,
201
+ };
202
+ };
@@ -0,0 +1,13 @@
1
+ import { describe } from '@jest/globals';
2
+
3
+ import { testCliCommand } from '../test_utils/test_cli_command.js';
4
+
5
+ describe('operations', () => {
6
+ testCliCommand({
7
+ name: 'list',
8
+ args: ['operations', 'list', '--project-id', 'test'],
9
+ expected: {
10
+ snapshot: true,
11
+ },
12
+ });
13
+ });
@@ -0,0 +1,41 @@
1
+ import yargs from 'yargs';
2
+ import { fillSingleProject } from '../utils/enrichers.js';
3
+
4
+ import { ProjectScopeProps } from '../types.js';
5
+ import { writer } from '../writer.js';
6
+
7
+ const OPERATIONS_FIELDS = ['id', 'action', 'status', 'created_at'] as const;
8
+
9
+ export const command = 'operations';
10
+ export const describe = 'Manage operations';
11
+ export const aliases = ['operation'];
12
+ export const builder = (argv: yargs.Argv) =>
13
+ argv
14
+ .usage('$0 operations <sub-command> [options]')
15
+ .options({
16
+ 'project-id': {
17
+ describe: 'Project ID',
18
+ type: 'string',
19
+ },
20
+ })
21
+ .middleware(fillSingleProject as any)
22
+ .command(
23
+ 'list',
24
+ 'List operations',
25
+ (yargs) => yargs,
26
+ async (args) => await list(args as any),
27
+ );
28
+
29
+ export const handler = (args: yargs.Argv) => {
30
+ return args;
31
+ };
32
+
33
+ export const list = async (props: ProjectScopeProps & { limit: number }) => {
34
+ const { data } = await props.apiClient.listProjectOperations({
35
+ projectId: props.projectId,
36
+ limit: props.limit,
37
+ });
38
+ writer(props).end(data.operations, {
39
+ fields: OPERATIONS_FIELDS,
40
+ });
41
+ };
@@ -0,0 +1,147 @@
1
+ import { afterAll, describe, expect, test } from '@jest/globals';
2
+ import { readFileSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { testCliCommand } from '../test_utils/test_cli_command.js';
6
+
7
+ const CONTEXT_FILE = join(tmpdir(), `neon_project_create_ctx_${Date.now()}`);
8
+
9
+ describe('projects', () => {
10
+ testCliCommand({
11
+ name: 'list',
12
+ args: ['projects', 'list'],
13
+ expected: {
14
+ snapshot: true,
15
+ },
16
+ });
17
+
18
+ testCliCommand({
19
+ name: 'create',
20
+ args: ['projects', 'create', '--name', 'test_project'],
21
+ expected: {
22
+ snapshot: true,
23
+ },
24
+ });
25
+
26
+ testCliCommand({
27
+ name: 'create with database and role',
28
+ args: [
29
+ 'projects',
30
+ 'create',
31
+ '--name',
32
+ 'test_project',
33
+ '--database',
34
+ 'test_db',
35
+ '--role',
36
+ 'test_role',
37
+ ],
38
+ expected: {
39
+ snapshot: true,
40
+ },
41
+ });
42
+
43
+ testCliCommand({
44
+ name: 'create and connect with psql',
45
+ args: ['projects', 'create', '--name', 'test_project', '--psql'],
46
+ expected: {
47
+ snapshot: true,
48
+ },
49
+ });
50
+
51
+ testCliCommand({
52
+ name: 'create and connect with psql and psql args',
53
+ args: [
54
+ 'projects',
55
+ 'create',
56
+ '--name',
57
+ 'test_project',
58
+ '--psql',
59
+ '--',
60
+ '-c',
61
+ 'SELECT 1',
62
+ ],
63
+ expected: {
64
+ snapshot: true,
65
+ },
66
+ });
67
+
68
+ testCliCommand({
69
+ name: 'create project with setting the context',
70
+ args: [
71
+ 'projects',
72
+ 'create',
73
+ '--name',
74
+ 'test_project',
75
+ '--context-file',
76
+ CONTEXT_FILE,
77
+ '--set-context',
78
+ ],
79
+ expected: {
80
+ snapshot: true,
81
+ },
82
+ });
83
+
84
+ afterAll(() => {
85
+ rmSync(CONTEXT_FILE);
86
+ });
87
+
88
+ test('context file should exist and contain the project id', () => {
89
+ expect(readFileSync(CONTEXT_FILE, 'utf-8')).toContain('new-project-123456');
90
+ });
91
+
92
+ testCliCommand({
93
+ name: 'delete',
94
+ args: ['projects', 'delete', 'test'],
95
+ expected: {
96
+ snapshot: true,
97
+ },
98
+ });
99
+
100
+ testCliCommand({
101
+ name: 'update name',
102
+ args: ['projects', 'update', 'test', '--name', 'test_project_new_name'],
103
+ expected: {
104
+ snapshot: true,
105
+ },
106
+ });
107
+
108
+ testCliCommand({
109
+ name: 'update ip allow',
110
+ args: [
111
+ 'projects',
112
+ 'update',
113
+ 'test',
114
+ '--ip-allow',
115
+ '127.0.0.1',
116
+ '192.168.1.2/22',
117
+ '--ip-primary-only',
118
+ ],
119
+ expected: {
120
+ snapshot: true,
121
+ },
122
+ });
123
+
124
+ testCliCommand({
125
+ name: 'update ip allow primary only flag',
126
+ args: ['projects', 'update', 'test', '--ip-primary-only', 'false'],
127
+ expected: {
128
+ snapshot: true,
129
+ },
130
+ });
131
+
132
+ testCliCommand({
133
+ name: 'update ip allow remove',
134
+ args: ['projects', 'update', 'test', '--ip-allow'],
135
+ expected: {
136
+ snapshot: true,
137
+ },
138
+ });
139
+
140
+ testCliCommand({
141
+ name: 'get',
142
+ args: ['projects', 'get', 'test'],
143
+ expected: {
144
+ snapshot: true,
145
+ },
146
+ });
147
+ });
@@ -0,0 +1,275 @@
1
+ import {
2
+ ProjectCreateRequest,
3
+ ProjectListItem,
4
+ ProjectUpdateRequest,
5
+ } from '@neondatabase/api-client';
6
+ import yargs from 'yargs';
7
+
8
+ import { log } from '../log.js';
9
+ import {
10
+ projectCreateRequest,
11
+ projectUpdateRequest,
12
+ } from '../parameters.gen.js';
13
+ import { CommonProps, IdOrNameProps } from '../types.js';
14
+ import { writer } from '../writer.js';
15
+ import { psql } from '../utils/psql.js';
16
+ import { updateContextFile } from '../context.js';
17
+
18
+ const PROJECT_FIELDS = ['id', 'name', 'region_id', 'created_at'] as const;
19
+
20
+ const REGIONS = [
21
+ 'aws-us-west-2',
22
+ 'aws-ap-southeast-1',
23
+ 'aws-eu-central-1',
24
+ 'aws-us-east-2',
25
+ 'aws-us-east-1',
26
+ ];
27
+
28
+ const PROJECTS_LIST_LIMIT = 100;
29
+
30
+ export const command = 'projects';
31
+ export const describe = 'Manage projects';
32
+ export const aliases = ['project'];
33
+ export const builder = (argv: yargs.Argv) => {
34
+ return argv
35
+ .usage('$0 projects <sub-command> [options]')
36
+ .command(
37
+ 'list',
38
+ 'List projects',
39
+ (yargs) => yargs,
40
+ async (args) => {
41
+ await list(args as any);
42
+ },
43
+ )
44
+ .command(
45
+ 'create',
46
+ 'Create a project',
47
+ (yargs) =>
48
+ yargs.options({
49
+ name: {
50
+ describe: projectCreateRequest['project.name'].description,
51
+ type: 'string',
52
+ },
53
+ 'region-id': {
54
+ describe: `The region ID. Possible values: ${REGIONS.join(', ')}`,
55
+ type: 'string',
56
+ },
57
+ psql: {
58
+ type: 'boolean',
59
+ describe: 'Connect to a new project via psql',
60
+ default: false,
61
+ },
62
+ database: {
63
+ describe:
64
+ projectCreateRequest['project.branch.database_name'].description,
65
+ type: 'string',
66
+ },
67
+ role: {
68
+ describe:
69
+ projectCreateRequest['project.branch.role_name'].description,
70
+ type: 'string',
71
+ },
72
+ 'set-context': {
73
+ type: 'boolean',
74
+ describe: 'Set the current context to the new project',
75
+ default: false,
76
+ },
77
+ }),
78
+ async (args) => {
79
+ await create(args as any);
80
+ },
81
+ )
82
+ .command(
83
+ 'update <id>',
84
+ 'Update a project',
85
+ (yargs) =>
86
+ yargs.options({
87
+ name: {
88
+ describe: projectCreateRequest['project.name'].description,
89
+ type: 'string',
90
+ },
91
+ 'ip-allow': {
92
+ describe:
93
+ projectUpdateRequest['project.settings.allowed_ips.ips']
94
+ .description,
95
+ type: 'string',
96
+ array: true,
97
+ deprecated: "Deprecated. Use 'ip-allow' command",
98
+ },
99
+ 'ip-primary-only': {
100
+ describe:
101
+ projectUpdateRequest[
102
+ 'project.settings.allowed_ips.primary_branch_only'
103
+ ].description,
104
+ type: 'boolean',
105
+ deprecated: "Deprecated. Use 'ip-allow' command",
106
+ },
107
+ }),
108
+ async (args) => {
109
+ await update(args as any);
110
+ },
111
+ )
112
+ .command(
113
+ 'delete <id>',
114
+ 'Delete a project',
115
+ (yargs) => yargs,
116
+ async (args) => {
117
+ await deleteProject(args as any);
118
+ },
119
+ )
120
+ .command(
121
+ 'get <id>',
122
+ 'Get a project',
123
+ (yargs) => yargs,
124
+ async (args) => {
125
+ await get(args as any);
126
+ },
127
+ );
128
+ };
129
+ export const handler = (args: yargs.Argv) => {
130
+ return args;
131
+ };
132
+
133
+ const list = async (props: CommonProps) => {
134
+ const getList = async (
135
+ fn:
136
+ | typeof props.apiClient.listProjects
137
+ | typeof props.apiClient.listSharedProjects,
138
+ ) => {
139
+ const result: ProjectListItem[] = [];
140
+ let cursor: string | undefined;
141
+ let end = false;
142
+ while (!end) {
143
+ const { data } = await fn({
144
+ limit: PROJECTS_LIST_LIMIT,
145
+ cursor,
146
+ });
147
+ result.push(...data.projects);
148
+ cursor = data.pagination?.cursor;
149
+ log.debug(
150
+ 'Got %d projects, with cursor: %s',
151
+ data.projects.length,
152
+ cursor,
153
+ );
154
+ if (data.projects.length < PROJECTS_LIST_LIMIT) {
155
+ end = true;
156
+ }
157
+ }
158
+
159
+ return result;
160
+ };
161
+
162
+ const [ownedProjects, sharedProjects] = await Promise.all([
163
+ getList(props.apiClient.listProjects),
164
+ getList(props.apiClient.listSharedProjects),
165
+ ]);
166
+
167
+ const out = writer(props);
168
+
169
+ out.write(ownedProjects, {
170
+ fields: PROJECT_FIELDS,
171
+ title: 'Projects',
172
+ });
173
+ out.write(sharedProjects, {
174
+ fields: PROJECT_FIELDS,
175
+ title: 'Shared with me',
176
+ });
177
+ out.end();
178
+ };
179
+
180
+ const create = async (
181
+ props: CommonProps & {
182
+ name?: string;
183
+ regionId?: string;
184
+ database?: string;
185
+ role?: string;
186
+ psql: boolean;
187
+ setContext: boolean;
188
+ '--'?: string[];
189
+ },
190
+ ) => {
191
+ const project: ProjectCreateRequest['project'] = {};
192
+ if (props.name) {
193
+ project.name = props.name;
194
+ }
195
+ if (props.regionId) {
196
+ project.region_id = props.regionId;
197
+ }
198
+ project.branch = {};
199
+ if (props.database) {
200
+ project.branch.database_name = props.database;
201
+ }
202
+ if (props.role) {
203
+ project.branch.role_name = props.role;
204
+ }
205
+ const { data } = await props.apiClient.createProject({
206
+ project,
207
+ });
208
+
209
+ if (props.setContext) {
210
+ updateContextFile(props.contextFile, {
211
+ projectId: data.project.id,
212
+ branchId: data.branch.id,
213
+ });
214
+ }
215
+
216
+ const out = writer(props);
217
+ out.write(data.project, { fields: PROJECT_FIELDS, title: 'Project' });
218
+ out.write(data.connection_uris, {
219
+ fields: ['connection_uri'],
220
+ title: 'Connection URIs',
221
+ });
222
+ out.end();
223
+
224
+ if (props.psql) {
225
+ const connection_uri = data.connection_uris[0].connection_uri;
226
+ const psqlArgs = props['--'];
227
+ await psql(connection_uri, psqlArgs);
228
+ }
229
+ };
230
+
231
+ const deleteProject = async (props: CommonProps & IdOrNameProps) => {
232
+ const { data } = await props.apiClient.deleteProject(props.id);
233
+ writer(props).end(data.project, {
234
+ fields: PROJECT_FIELDS,
235
+ });
236
+ };
237
+
238
+ const update = async (
239
+ props: CommonProps &
240
+ IdOrNameProps & {
241
+ name?: string;
242
+ ipAllow?: string[];
243
+ ipPrimaryOnly?: boolean;
244
+ },
245
+ ) => {
246
+ const project: ProjectUpdateRequest['project'] = {};
247
+ if (props.name) {
248
+ project.name = props.name;
249
+ }
250
+ if (props.ipAllow || props.ipPrimaryOnly != undefined) {
251
+ const { data } = await props.apiClient.getProject(props.id);
252
+ const existingAllowedIps = data.project.settings?.allowed_ips;
253
+
254
+ project.settings = {
255
+ allowed_ips: {
256
+ ips: props.ipAllow ?? existingAllowedIps?.ips ?? [],
257
+ primary_branch_only:
258
+ props.ipPrimaryOnly ??
259
+ existingAllowedIps?.primary_branch_only ??
260
+ false,
261
+ },
262
+ };
263
+ }
264
+
265
+ const { data } = await props.apiClient.updateProject(props.id, {
266
+ project,
267
+ });
268
+
269
+ writer(props).end(data.project, { fields: PROJECT_FIELDS });
270
+ };
271
+
272
+ const get = async (props: CommonProps & IdOrNameProps) => {
273
+ const { data } = await props.apiClient.getProject(props.id);
274
+ writer(props).end(data.project, { fields: PROJECT_FIELDS });
275
+ };