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,117 @@
1
+ import { test, expect, describe, beforeAll, afterAll } from '@jest/globals';
2
+ import { fork } from 'node:child_process';
3
+ import { Server } from 'node:http';
4
+ import { AddressInfo } from 'node:net';
5
+ import { join } from 'node:path';
6
+ import { log } from '../log.js';
7
+ import strip from 'strip-ansi';
8
+
9
+ import { runMockServer } from './mock_server.js';
10
+
11
+ export type TestCliCommandOptions = {
12
+ name: string;
13
+ args: string[];
14
+ before?: () => Promise<void>;
15
+ after?: () => Promise<void>;
16
+ mockDir?: string;
17
+ expected?: {
18
+ snapshot?: true;
19
+ stdout?: string | ReturnType<typeof expect.stringMatching>;
20
+ stderr?: string | ReturnType<typeof expect.stringMatching>;
21
+ code?: number;
22
+ };
23
+ };
24
+
25
+ export const testCliCommand = ({
26
+ args,
27
+ name,
28
+ expected,
29
+ before,
30
+ after,
31
+ mockDir = 'main',
32
+ }: TestCliCommandOptions) => {
33
+ let server: Server;
34
+ describe(name, () => {
35
+ beforeAll(async () => {
36
+ if (before) {
37
+ await before();
38
+ }
39
+ server = await runMockServer(mockDir);
40
+ });
41
+
42
+ afterAll(async () => {
43
+ if (after) {
44
+ await after();
45
+ }
46
+ return new Promise<void>((resolve) => {
47
+ server.close(() => {
48
+ resolve();
49
+ });
50
+ });
51
+ });
52
+
53
+ test('test', async () => {
54
+ let output = '';
55
+ let error = '';
56
+
57
+ const cp = fork(
58
+ join(process.cwd(), './dist/index.js'),
59
+ [
60
+ '--api-host',
61
+ `http://localhost:${(server.address() as AddressInfo).port}`,
62
+ '--output',
63
+ 'yaml',
64
+ '--api-key',
65
+ 'test-key',
66
+ '--no-analytics',
67
+ ...args,
68
+ ],
69
+ {
70
+ stdio: 'pipe',
71
+ env: {
72
+ PATH: `mocks/bin:${process.env.PATH}`,
73
+ },
74
+ },
75
+ );
76
+
77
+ return new Promise<void>((resolve, reject) => {
78
+ cp.stdout?.on('data', (data) => {
79
+ output += data.toString();
80
+ });
81
+
82
+ cp.stderr?.on('data', (data) => {
83
+ error += data.toString();
84
+ log.error(data.toString());
85
+ });
86
+
87
+ cp.on('error', (err) => {
88
+ throw err;
89
+ });
90
+
91
+ cp.on('close', (code) => {
92
+ try {
93
+ expect(code).toBe(expected?.code ?? 0);
94
+ if (expected) {
95
+ if (expected.snapshot) {
96
+ expect(output).toMatchSnapshot();
97
+ }
98
+ if (expected.stdout !== undefined) {
99
+ expect(strip(output)).toEqual(expected.stdout);
100
+ }
101
+ if (expected.stderr !== undefined) {
102
+ expect(strip(error).replace(/\s+/g, ' ').trim()).toEqual(
103
+ typeof expected.stderr === 'string'
104
+ ? expected.stderr.toString().replace(/\s+/g, ' ')
105
+ : expected.stderr,
106
+ );
107
+ }
108
+ }
109
+ resolve();
110
+ } catch (err) {
111
+ reject(err);
112
+ }
113
+ });
114
+ });
115
+ });
116
+ });
117
+ };
package/src/types.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { Api } from '@neondatabase/api-client';
2
+
3
+ export type CommonProps = {
4
+ apiClient: Api<unknown>;
5
+ apiKey: string;
6
+ apiHost: string;
7
+ output: 'yaml' | 'json' | 'table';
8
+ contextFile: string;
9
+ };
10
+
11
+ export type ProjectScopeProps = CommonProps & {
12
+ projectId: string;
13
+ };
14
+
15
+ export type IdOrNameProps = {
16
+ id: string;
17
+ };
18
+
19
+ export type BranchScopeProps = ProjectScopeProps &
20
+ (
21
+ | {
22
+ branch: string;
23
+ }
24
+ | IdOrNameProps
25
+ );
@@ -0,0 +1,73 @@
1
+ import { BranchScopeProps, CommonProps, ProjectScopeProps } from '../types.js';
2
+ import { looksLikeBranchId } from './formats.js';
3
+
4
+ export const branchIdResolve = async ({
5
+ branch,
6
+ apiClient,
7
+ projectId,
8
+ }: {
9
+ branch: string | number;
10
+ apiClient: CommonProps['apiClient'];
11
+ projectId: string;
12
+ }) => {
13
+ branch = branch.toString();
14
+ if (looksLikeBranchId(branch)) {
15
+ return branch;
16
+ }
17
+
18
+ const { data } = await apiClient.listProjectBranches(projectId);
19
+ const branchData = data.branches.find((b) => b.name === branch);
20
+ if (!branchData) {
21
+ throw new Error(
22
+ `Branch ${branch} not found.\nAvailable branches: ${data.branches
23
+ .map((b) => b.name)
24
+ .join(', ')}`,
25
+ );
26
+ }
27
+ return branchData.id;
28
+ };
29
+
30
+ export const branchIdFromProps = async (props: BranchScopeProps) => {
31
+ const branch =
32
+ 'branch' in props && typeof props.branch === 'string'
33
+ ? props.branch
34
+ : (props as any).id;
35
+
36
+ if (branch) {
37
+ return await branchIdResolve({
38
+ branch,
39
+ apiClient: props.apiClient,
40
+ projectId: props.projectId,
41
+ });
42
+ }
43
+
44
+ const { data } = await props.apiClient.listProjectBranches(props.projectId);
45
+ const primaryBranch = data.branches.find((b) => b.primary);
46
+
47
+ if (primaryBranch) {
48
+ return primaryBranch.id;
49
+ }
50
+
51
+ throw new Error('No primary branch found');
52
+ };
53
+
54
+ export const fillSingleProject = async (
55
+ props: CommonProps & Partial<Pick<ProjectScopeProps, 'projectId'>>,
56
+ ) => {
57
+ if (props.projectId) {
58
+ return props;
59
+ }
60
+ const { data } = await props.apiClient.listProjects({});
61
+ if (data.projects.length === 0) {
62
+ throw new Error('No projects found');
63
+ }
64
+ if (data.projects.length > 1) {
65
+ throw new Error(
66
+ `Multiple projects found, please provide one with the --project-id option`,
67
+ );
68
+ }
69
+ return {
70
+ ...props,
71
+ projectId: data.projects[0].id,
72
+ };
73
+ };
@@ -0,0 +1,41 @@
1
+ import { test, describe, expect } from '@jest/globals';
2
+
3
+ import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp } from './formats';
4
+
5
+ describe('branch formats', () => {
6
+ test('branch name', () => {
7
+ expect(looksLikeBranchId('master')).toBe(false);
8
+ });
9
+
10
+ test('initial short', () => {
11
+ expect(looksLikeBranchId('br-flower-sunshine-123456')).toBe(true);
12
+ });
13
+
14
+ test('update 1, longer version', () => {
15
+ expect(looksLikeBranchId('br-flower-sunshine-12345678')).toBe(true);
16
+ });
17
+
18
+ test('update 2, includes region', () => {
19
+ expect(looksLikeBranchId('br-bold-recipe-a13oexw7')).toBe(true);
20
+ });
21
+ });
22
+
23
+ describe('timestamp formats', () => {
24
+ test('valid', () => {
25
+ expect(looksLikeTimestamp('2021-03-13T19:47:33.000Z')).toBe(true);
26
+ });
27
+
28
+ test('invalid', () => {
29
+ expect(looksLikeTimestamp('branch_name')).toBe(false);
30
+ });
31
+ });
32
+
33
+ describe('LSN formats', () => {
34
+ test('valid', () => {
35
+ expect(looksLikeLSN('0/1F56000')).toBe(true);
36
+ });
37
+
38
+ test('invalid', () => {
39
+ expect(looksLikeLSN('branch_name')).toBe(false);
40
+ });
41
+ });
@@ -0,0 +1,11 @@
1
+ const HAIKU_REGEX = /^[a-z]+-[a-z]+-[a-z0-9]+$/;
2
+
3
+ export const looksLikeBranchId = (branch: string) =>
4
+ branch.startsWith('br-') && HAIKU_REGEX.test(branch.substring(3));
5
+
6
+ const LSN_REGEX = /^[a-fA-F0-9]{1,8}\/[a-fA-F0-9]{1,8}$/;
7
+
8
+ export const looksLikeLSN = (lsn: string) => LSN_REGEX.test(lsn);
9
+
10
+ export const looksLikeTimestamp = (timestamp: string) =>
11
+ !isNaN(Date.parse(timestamp));
@@ -0,0 +1,23 @@
1
+ /**
2
+ * This middleware is needed to fill in the args for nested objects,
3
+ * so that required arguments would work
4
+ * otherwise yargs just throws an error
5
+ */
6
+ export const fillInArgs = (
7
+ args: Record<string, unknown>,
8
+ currentArgs: Record<string, unknown> = args,
9
+ acc: string[] = [],
10
+ ) => {
11
+ Object.entries(currentArgs).forEach(([k, v]) => {
12
+ if (k === '_') {
13
+ return;
14
+ }
15
+ // check if the value is an Object
16
+ if (typeof v === 'object' && v !== null) {
17
+ fillInArgs(args, v as any, [...acc, k]);
18
+ } else if (acc.length > 0) {
19
+ // if it's not an object, and we have a path, fill it in
20
+ args[acc.join('.') + '.' + k] = v;
21
+ }
22
+ });
23
+ };
@@ -0,0 +1,86 @@
1
+ import { Api } from '@neondatabase/api-client';
2
+ import { looksLikeLSN, looksLikeTimestamp } from './formats.js';
3
+ import { branchIdResolve } from './enrichers.js';
4
+
5
+ export type PointInTime =
6
+ | {
7
+ tag: 'head';
8
+ }
9
+ | {
10
+ tag: 'lsn';
11
+ lsn: string;
12
+ }
13
+ | {
14
+ tag: 'timestamp';
15
+ timestamp: string;
16
+ };
17
+ export type PointInTimeBranchId = {
18
+ branchId: string;
19
+ } & PointInTime;
20
+
21
+ export type PointInTimeBranch = {
22
+ branch: string;
23
+ } & PointInTime;
24
+
25
+ export interface PointInTimeProps {
26
+ targetBranchId: string;
27
+ pointInTime: string;
28
+ projectId: string;
29
+ api: Api<unknown>;
30
+ }
31
+
32
+ export class PointInTimeParseError extends Error {
33
+ constructor(message: string) {
34
+ super(message);
35
+ this.name = 'PointInTimeParseError';
36
+ }
37
+ }
38
+
39
+ export const parsePITBranch = (input: string) => {
40
+ const splitIndex = input.lastIndexOf('@');
41
+ const sourceBranch = splitIndex === -1 ? input : input.slice(0, splitIndex);
42
+ const exactPIT = splitIndex === -1 ? null : input.slice(splitIndex + 1);
43
+ const result = {
44
+ branch: sourceBranch,
45
+ ...(exactPIT === null
46
+ ? { tag: 'head' }
47
+ : looksLikeLSN(exactPIT)
48
+ ? { tag: 'lsn', lsn: exactPIT }
49
+ : { tag: 'timestamp', timestamp: exactPIT }),
50
+ } satisfies PointInTimeBranch;
51
+ if (result.tag === 'timestamp' && !looksLikeTimestamp(result.timestamp)) {
52
+ throw new PointInTimeParseError('Invalid source branch format');
53
+ }
54
+ return result;
55
+ };
56
+
57
+ export const parsePointInTime = async ({
58
+ pointInTime,
59
+ targetBranchId,
60
+ projectId,
61
+ api,
62
+ }: PointInTimeProps) => {
63
+ const parsedPIT = parsePITBranch(pointInTime);
64
+
65
+ let branchId = '';
66
+ if (parsedPIT.branch === '^self') {
67
+ branchId = targetBranchId;
68
+ } else if (parsedPIT.branch === '^parent') {
69
+ const { data } = await api.getProjectBranch(projectId, targetBranchId);
70
+ const { parent_id: parentId } = data.branch;
71
+ if (parentId == null) {
72
+ throw new PointInTimeParseError('Branch has no parent');
73
+ }
74
+ branchId = parentId;
75
+ } else {
76
+ branchId = await branchIdResolve({
77
+ branch: parsedPIT.branch,
78
+ projectId,
79
+ apiClient: api,
80
+ });
81
+ }
82
+
83
+ // @ts-expect-error extracting pit from parsedPIT
84
+ delete parsedPIT.branch;
85
+ return { ...parsedPIT, branchId };
86
+ };
@@ -0,0 +1,29 @@
1
+ import { log } from '../log.js';
2
+ import { spawn } from 'child_process';
3
+ import which from 'which';
4
+
5
+ export const psql = async (connection_uri: string, args: string[] = []) => {
6
+ const psqlPathOrNull = await which('psql', { nothrow: true });
7
+
8
+ if (psqlPathOrNull === null) {
9
+ log.error(`psql is not available in the PATH`);
10
+ process.exit(1);
11
+ }
12
+
13
+ log.info('Connecting to the database using psql...');
14
+ const psql = spawn(psqlPathOrNull, [connection_uri, ...args], {
15
+ stdio: 'inherit',
16
+ });
17
+
18
+ for (const signame of ['SIGINT', 'SIGTERM']) {
19
+ process.on(signame, (code) => {
20
+ if (!psql.killed && code !== null) {
21
+ psql.kill(code);
22
+ }
23
+ });
24
+ }
25
+
26
+ psql.on('exit', (code: number | null) => {
27
+ process.exit(code === null ? 1 : code);
28
+ });
29
+ };
@@ -0,0 +1,8 @@
1
+ export const toSnakeCase = (str: string) =>
2
+ str
3
+ .split(' ')
4
+ .map((word) => word.toLowerCase())
5
+ .join('_');
6
+
7
+ export const isObject = (value: any) =>
8
+ value != null && value === Object(value);
@@ -0,0 +1,64 @@
1
+ // returns the next string if matches the given matcher,
2
+ // otherwise returns null
3
+ // consumes the line from the lines array
4
+ export const consumeNextMatching = (lines: string[], matcher: RegExp) => {
5
+ while (lines.length > 0) {
6
+ const line = (lines.shift() as string).trim();
7
+ if (line === '') {
8
+ continue;
9
+ }
10
+ if (matcher.test(line)) {
11
+ return line;
12
+ }
13
+ return null;
14
+ }
15
+ return null;
16
+ };
17
+
18
+ // returns strings if next non-empty line matches the given matcher,
19
+ // otherwise returns empty array
20
+ // consumes the lines from the lines array
21
+ export const consumeBlockIfMatches = (lines: string[], matcher: RegExp) => {
22
+ const result = [] as string[];
23
+ if (lines.length === 0) {
24
+ return result;
25
+ }
26
+
27
+ let line = lines.shift() as string;
28
+
29
+ while (line.trim() === '') {
30
+ line = lines.shift() as string;
31
+ }
32
+ if (!matcher.test(line)) {
33
+ lines.unshift(line);
34
+ return result;
35
+ }
36
+ result.push(line);
37
+ while (lines.length > 0) {
38
+ line = lines.shift() as string;
39
+ if (line.trim() === '') {
40
+ break;
41
+ }
42
+ result.push(line);
43
+ }
44
+ return result;
45
+ };
46
+
47
+ export const splitColumns = (line: string) => {
48
+ const result = line.trim().split(/\s{2,}/);
49
+ result[1] = result[1] ?? '';
50
+ if (result.length > 2) {
51
+ result[1] = result.slice(1).join(' ');
52
+ }
53
+ return result;
54
+ };
55
+
56
+ export const drawPointer = (width: number) => {
57
+ const result = [] as string[];
58
+ result.push('└');
59
+ for (let i = 0; i < width - 4; i++) {
60
+ result.push('─');
61
+ }
62
+ result.push('>');
63
+ return result.join('');
64
+ };
@@ -0,0 +1,98 @@
1
+ import { Writable } from 'node:stream';
2
+ import { describe, it, expect } from '@jest/globals';
3
+ import { writer } from './writer.js';
4
+
5
+ class MockWritable extends Writable {
6
+ _data: Buffer[] = [];
7
+
8
+ get data() {
9
+ return this._data.map((chunk) => chunk.toString()).join('');
10
+ }
11
+
12
+ _write(chunk: Buffer) {
13
+ this._data.push(chunk);
14
+ }
15
+ }
16
+
17
+ describe('writer', () => {
18
+ describe('outputs yaml', () => {
19
+ it('outputs single data', () => {
20
+ const stream = new MockWritable();
21
+ const out = writer({ output: 'yaml', out: stream });
22
+ out.end({ foo: 'bar' }, { fields: ['foo'] });
23
+ expect(stream.data).toMatchSnapshot();
24
+ });
25
+
26
+ it('outputs single data with title', () => {
27
+ const stream = new MockWritable();
28
+ const out = writer({ output: 'yaml', out: stream });
29
+ out.end({ foo: 'bar' }, { fields: ['foo'], title: 'baz' });
30
+ expect(stream.data).toMatchSnapshot();
31
+ });
32
+
33
+ it('outputs multiple data', () => {
34
+ const stream = new MockWritable();
35
+ const out = writer({ output: 'yaml', out: stream });
36
+ out
37
+ .write({ foo: 'bar' }, { fields: ['foo'], title: 'T1' })
38
+ .write({ baz: 'xyz' }, { fields: ['baz'], title: 'T2' })
39
+ .end();
40
+ expect(stream.data).toMatchSnapshot();
41
+ });
42
+ });
43
+
44
+ describe('outputs json', () => {
45
+ it('outputs single data', () => {
46
+ const stream = new MockWritable();
47
+ const out = writer({ output: 'json', out: stream });
48
+ out.end({ foo: 'bar' }, { fields: ['foo'] });
49
+ expect(stream.data).toMatchSnapshot();
50
+ });
51
+
52
+ it('outputs single data with title', () => {
53
+ const stream = new MockWritable();
54
+ const out = writer({ output: 'json', out: stream });
55
+ out.end({ foo: 'bar' }, { fields: ['foo'], title: 'baz' });
56
+ expect(stream.data).toMatchSnapshot();
57
+ });
58
+
59
+ it('outputs multiple data', () => {
60
+ const stream = new MockWritable();
61
+ const out = writer({ output: 'json', out: stream });
62
+ out
63
+ .write({ foo: 'bar' }, { fields: ['foo'], title: 'T1' })
64
+ .write({ baz: 'xyz' }, { fields: ['baz'], title: 'T2' })
65
+ .end();
66
+ expect(stream.data).toMatchSnapshot();
67
+ });
68
+ });
69
+
70
+ describe('outputs table', () => {
71
+ it('outputs single data', () => {
72
+ const stream = new MockWritable();
73
+ const out = writer({ output: 'table', out: stream });
74
+ out.end({ foo: 'bar', extra: 'extra' }, { fields: ['foo'] });
75
+ expect(stream.data).toMatchSnapshot();
76
+ });
77
+
78
+ it('outputs single data with title', () => {
79
+ const stream = new MockWritable();
80
+ const out = writer({ output: 'table', out: stream });
81
+ out.end(
82
+ { foo: 'bar', extra: 'extra' },
83
+ { fields: ['foo'], title: 'baz' },
84
+ );
85
+ expect(stream.data).toMatchSnapshot();
86
+ });
87
+
88
+ it('outputs multiple data', () => {
89
+ const stream = new MockWritable();
90
+ const out = writer({ output: 'table', out: stream });
91
+ out
92
+ .write({ foo: 'bar', extra: 'extra' }, { fields: ['foo'], title: 'T1' })
93
+ .write({ baz: 'xyz', extra: 'extra' }, { fields: ['baz'], title: 'T2' })
94
+ .end();
95
+ expect(stream.data).toMatchSnapshot();
96
+ });
97
+ });
98
+ });