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.
- package/.bump +1 -0
- package/.editorconfig +7 -0
- package/.eslintrc.cjs +15 -0
- package/.github/workflows/commitlint.yml +46 -0
- package/.github/workflows/pr.yml +25 -0
- package/.github/workflows/release.yml +30 -0
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +4 -0
- package/.nvmrc +1 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +3 -0
- package/.releaserc.json +47 -0
- package/LICENSE +202 -0
- package/commitlint.config.cjs +7 -0
- package/generateOptionsFromSpec.ts +68 -0
- package/jest/setup.js +5 -0
- package/jest.config.ts +199 -0
- package/mocks/bin/psql.cjs +9 -0
- package/mocks/main/projects/GET.js +27 -0
- package/mocks/main/projects/POST.js +22 -0
- package/mocks/main/projects/shared/GET.js +16 -0
- package/mocks/main/projects/test/DELETE.json +7 -0
- package/mocks/main/projects/test/GET.json +13 -0
- package/mocks/main/projects/test/PATCH.js +18 -0
- package/mocks/main/projects/test/branches/GET.json +25 -0
- package/mocks/main/projects/test/branches/POST.js +83 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/DELETE.json +7 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/GET.json +9 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/PATCH.js +14 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/GET.json +6 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/POST.js +13 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/databases/test_db/DELETE.json +6 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/GET.json +26 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/endpoints/POST.json +6 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/GET.json +3 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/POST.js +14 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/DELETE.json +6 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/roles/test_role/reveal_password/GET.json +3 -0
- package/mocks/main/projects/test/branches/br-cloudy-branch-12345678/set_as_primary/POST.json +9 -0
- package/mocks/main/projects/test/branches/br-numbered-branch-123456/GET.json +10 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/DELETE.json +7 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/GET.json +10 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/PATCH.js +14 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/GET.json +6 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/POST.js +13 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/databases/test_db/DELETE.json +6 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/GET.json +26 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/endpoints/POST.json +6 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/restore/POST.js +16 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/GET.json +3 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/POST.js +14 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/DELETE.json +6 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/roles/test_role/reveal_password/GET.json +3 -0
- package/mocks/main/projects/test/branches/br-sunny-branch-123456/set_as_primary/POST.json +9 -0
- package/mocks/main/projects/test/endpoints/GET.json +9 -0
- package/mocks/main/projects/test/endpoints/POST.js +32 -0
- package/mocks/main/projects/test/endpoints/test_endpoint_id/DELETE.json +7 -0
- package/mocks/main/projects/test/endpoints/test_endpoint_id/GET.json +9 -0
- package/mocks/main/projects/test/endpoints/test_endpoint_id/PATCH.js +17 -0
- package/mocks/main/projects/test/operations/GET.json +22 -0
- package/mocks/main/users/me/GET.json +5 -0
- package/mocks/restore/projects/test/branches/GET.json +21 -0
- package/mocks/restore/projects/test/branches/br-another-branch-123456/GET.json +6 -0
- package/mocks/restore/projects/test/branches/br-another-branch-123456/restore/POST.js +13 -0
- package/mocks/restore/projects/test/branches/br-any-branch-123456/GET.json +6 -0
- package/mocks/restore/projects/test/branches/br-parent-tots-123456/GET.json +7 -0
- package/mocks/restore/projects/test/branches/br-parent-tots-123456/restore/POST.js +14 -0
- package/mocks/restore/projects/test/branches/br-self-tolsn-123456/GET.json +6 -0
- package/mocks/restore/projects/test/branches/br-self-tolsn-123456/restore/POST.js +15 -0
- package/mocks/single_project/projects/GET.json +10 -0
- package/mocks/single_project/projects/test-project-123456/GET.json +14 -0
- package/mocks/single_project/projects/test-project-123456/branches/GET.json +11 -0
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/databases/GET.json +3 -0
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/endpoints/GET.json +10 -0
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/GET.json +3 -0
- package/mocks/single_project/projects/test-project-123456/branches/br-main-branch-123456/roles/test_role/reveal_password/GET.json +3 -0
- package/package.json +6 -5
- package/pkg.js +45 -3
- package/rollup.config.js +20 -0
- package/snapshots/commands/branches.test.snap +221 -0
- package/snapshots/commands/connection_string.test.snap +70 -0
- package/snapshots/commands/databases.test.snap +20 -0
- package/snapshots/commands/ip_allow.test.snap +55 -0
- package/snapshots/commands/operations.test.snap +17 -0
- package/snapshots/commands/projects.test.snap +141 -0
- package/snapshots/commands/roles.test.snap +19 -0
- package/snapshots/commands/set_context.test.snap +30 -0
- package/snapshots/writer.test.snap +60 -0
- package/snapshotsResolver.cjs +32 -0
- package/src/analytics.ts +95 -0
- package/src/api.ts +44 -0
- package/src/auth.ts +137 -0
- package/{cli.js → src/cli.ts} +1 -0
- package/src/commands/auth.test.ts +62 -0
- package/src/commands/auth.ts +148 -0
- package/src/commands/branches.test.ts +354 -0
- package/src/commands/branches.ts +451 -0
- package/src/commands/connection_string.test.ts +250 -0
- package/src/commands/connection_string.ts +210 -0
- package/src/commands/databases.test.ts +55 -0
- package/src/commands/databases.ts +129 -0
- package/src/commands/help.test.ts +13 -0
- package/{commands/index.js → src/commands/index.ts} +11 -10
- package/src/commands/ip_allow.test.ts +86 -0
- package/src/commands/ip_allow.ts +202 -0
- package/src/commands/operations.test.ts +13 -0
- package/src/commands/operations.ts +41 -0
- package/src/commands/projects.test.ts +147 -0
- package/src/commands/projects.ts +275 -0
- package/src/commands/roles.test.ts +46 -0
- package/src/commands/roles.ts +100 -0
- package/src/commands/set_context.test.ts +64 -0
- package/src/commands/set_context.ts +27 -0
- package/src/commands/user.ts +21 -0
- package/src/config.ts +22 -0
- package/src/context.ts +61 -0
- package/src/env.ts +7 -0
- package/src/errors.ts +24 -0
- package/src/help.ts +185 -0
- package/src/index.ts +180 -0
- package/src/log.ts +16 -0
- package/src/parameters.gen.ts +332 -0
- package/src/pkg.ts +9 -0
- package/src/test_utils/mock_server.ts +27 -0
- package/src/test_utils/oauth_server.ts +10 -0
- package/src/test_utils/test_cli_command.ts +117 -0
- package/src/types.ts +25 -0
- package/src/utils/enrichers.ts +73 -0
- package/src/utils/formats.test.ts +41 -0
- package/src/utils/formats.ts +11 -0
- package/src/utils/middlewares.ts +23 -0
- package/src/utils/point_in_time.ts +86 -0
- package/src/utils/psql.ts +29 -0
- package/src/utils/string.ts +8 -0
- package/src/utils/ui.ts +64 -0
- package/src/writer.test.ts +98 -0
- package/src/writer.ts +131 -0
- package/tsconfig.json +17 -0
- package/analytics.js +0 -78
- package/api.js +0 -35
- package/auth.js +0 -101
- package/commands/auth.js +0 -102
- package/commands/auth.test.js +0 -42
- package/commands/branches.js +0 -303
- package/commands/branches.test.js +0 -321
- package/commands/connection_string.js +0 -137
- package/commands/connection_string.test.js +0 -204
- package/commands/databases.js +0 -79
- package/commands/databases.test.js +0 -51
- package/commands/help.test.js +0 -11
- package/commands/ip_allow.js +0 -135
- package/commands/ip_allow.test.js +0 -78
- package/commands/operations.js +0 -28
- package/commands/operations.test.js +0 -11
- package/commands/projects.js +0 -170
- package/commands/projects.test.js +0 -132
- package/commands/roles.js +0 -57
- package/commands/roles.test.js +0 -42
- package/commands/set_context.js +0 -22
- package/commands/set_context.test.js +0 -53
- package/commands/user.js +0 -15
- package/config.js +0 -11
- package/context.js +0 -48
- package/env.js +0 -6
- package/errors.js +0 -16
- package/help.js +0 -146
- package/index.js +0 -168
- package/log.js +0 -15
- package/parameters.gen.js +0 -322
- package/test_utils/mock_server.js +0 -16
- package/test_utils/oauth_server.js +0 -9
- package/test_utils/test_cli_command.js +0 -80
- package/types.js +0 -1
- package/utils/enrichers.js +0 -49
- package/utils/formats.js +0 -5
- package/utils/formats.test.js +0 -32
- package/utils/middlewares.js +0 -20
- package/utils/point_in_time.js +0 -44
- package/utils/psql.js +0 -24
- package/utils/string.js +0 -5
- package/utils/ui.js +0 -59
- package/writer.js +0 -87
- package/writer.test.js +0 -86
- /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
|
+
};
|
package/src/utils/ui.ts
ADDED
|
@@ -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
|
+
});
|