revisium 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/e2e/utils/cli-runner.d.ts +1 -0
- package/dist/e2e/utils/cli-runner.js +3 -2
- package/dist/e2e/utils/cli-runner.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/src/commands/migration/apply-migrations.command.d.ts +2 -0
- package/dist/src/commands/migration/apply-migrations.command.js +16 -1
- package/dist/src/commands/migration/apply-migrations.command.js.map +1 -1
- package/dist/src/services/connection/connection-factory.service.d.ts +3 -0
- package/dist/src/services/connection/connection-factory.service.js +30 -5
- package/dist/src/services/connection/connection-factory.service.js.map +1 -1
- package/dist/src/services/connection/connection.service.d.ts +1 -0
- package/dist/src/services/connection/connection.service.js +3 -1
- package/dist/src/services/connection/connection.service.js.map +1 -1
- package/dist/src/services/url/url-parser.service.d.ts +1 -1
- package/dist/src/services/url/url-parser.service.js +12 -6
- package/dist/src/services/url/url-parser.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/migrate-commands.md +6 -1
- package/docs/url-format.md +25 -0
- package/e2e/tests/01-auth.e2e-spec.ts +19 -0
- package/e2e/tests/03-migrate.e2e-spec.ts +49 -1
- package/e2e/utils/cli-runner.ts +11 -3
- package/package.json +1 -1
- package/src/commands/migration/apply-migrations.command.ts +14 -1
- package/src/services/connection/__tests__/connection-factory.service.spec.ts +141 -0
- package/src/services/connection/__tests__/connection.service.spec.ts +1 -0
- package/src/services/connection/connection-factory.service.ts +47 -5
- package/src/services/connection/connection.service.ts +4 -1
- package/src/services/url/__tests__/url-parser.service.spec.ts +162 -0
- package/src/services/url/url-parser.service.ts +21 -6
package/docs/migrate-commands.md
CHANGED
|
@@ -39,7 +39,7 @@ revisium migrate save --file ./migrations.json \
|
|
|
39
39
|
Apply migrations from a JSON file to Revisium.
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
revisium migrate apply --file <path> [--commit]
|
|
42
|
+
revisium migrate apply --file <path> [--commit] [--create-project]
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
### Options
|
|
@@ -48,6 +48,7 @@ revisium migrate apply --file <path> [--commit]
|
|
|
48
48
|
|--------|-------------|----------|
|
|
49
49
|
| `-f, --file <path>` | Migrations file path | Yes |
|
|
50
50
|
| `-c, --commit` | Create revision after applying | No |
|
|
51
|
+
| `--create-project` | Auto-create project if it doesn't exist | No |
|
|
51
52
|
| `--url <url>` | Revisium URL (see [URL Format](./url-format.md)) | No* |
|
|
52
53
|
|
|
53
54
|
*If `--url` is not provided, uses `REVISIUM_URL` environment variable or prompts interactively.
|
|
@@ -165,6 +166,10 @@ git commit -m "Add user phone field migration"
|
|
|
165
166
|
export REVISIUM_URL=revisium://cloud.revisium.io/myorg/myproject/master
|
|
166
167
|
export REVISIUM_TOKEN=$DEPLOY_TOKEN
|
|
167
168
|
revisium migrate apply --file ./migrations.json --commit
|
|
169
|
+
|
|
170
|
+
# Apply in CI/CD with auto-create (fresh instance)
|
|
171
|
+
revisium migrate apply --file ./migrations.json --commit --create-project \
|
|
172
|
+
--url revisium://admin:admin@localhost:8888/admin/billing/master:draft
|
|
168
173
|
```
|
|
169
174
|
|
|
170
175
|
See [URL Format](./url-format.md) for complete URL syntax and [Authentication](./authentication.md) for auth options.
|
package/docs/url-format.md
CHANGED
|
@@ -6,6 +6,8 @@ Revisium CLI uses a special URL format to specify project connections.
|
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
8
|
revisium://[user:password@]host[:port]/organization/project/branch[:revision][?params]
|
|
9
|
+
revisium+http://[user:password@]host[:port]/organization/project/branch[:revision][?params]
|
|
10
|
+
revisium+https://[user:password@]host[:port]/organization/project/branch[:revision][?params]
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
## URL Parts
|
|
@@ -44,6 +46,29 @@ revisium://[user:password@]host[:port]/organization/project/branch[:revision][?p
|
|
|
44
46
|
| `127.0.0.1` | http | 8080 |
|
|
45
47
|
| Other hosts | https | 443 |
|
|
46
48
|
|
|
49
|
+
### Explicit Protocol Override
|
|
50
|
+
|
|
51
|
+
Use `revisium+http://` or `revisium+https://` to force a specific protocol, bypassing the auto-detection logic. This is useful in Kubernetes or other environments where internal services use HTTP on non-localhost hostnames.
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
revisium+http://[user:password@]host[:port]/organization/project/branch[:revision][?params]
|
|
55
|
+
revisium+https://[user:password@]host[:port]/organization/project/branch[:revision][?params]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Prefix | Behavior |
|
|
59
|
+
|--------|----------|
|
|
60
|
+
| `revisium://` | Auto-detect (localhost → http, others → https) |
|
|
61
|
+
| `revisium+http://` | Force HTTP |
|
|
62
|
+
| `revisium+https://` | Force HTTPS |
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Force HTTP for internal Kubernetes service
|
|
66
|
+
revisium+http://admin:pass@payment-config-svc:80/org/proj/master?token=xxx
|
|
67
|
+
|
|
68
|
+
# Force HTTPS explicitly
|
|
69
|
+
revisium+https://admin:pass@my-host/org/proj/master
|
|
70
|
+
```
|
|
71
|
+
|
|
47
72
|
## Authentication Examples
|
|
48
73
|
|
|
49
74
|
### Token Authentication (Recommended)
|
|
@@ -156,4 +156,23 @@ describe('CLI Authentication', () => {
|
|
|
156
156
|
expect(result.exitCode).toBe(0);
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
|
+
|
|
160
|
+
describe('Protocol override (revisium+http://)', () => {
|
|
161
|
+
it('connects via explicit HTTP protocol', async () => {
|
|
162
|
+
const token = process.env.E2E_ADMIN_TOKEN!;
|
|
163
|
+
|
|
164
|
+
const result = await runCli([
|
|
165
|
+
'schema',
|
|
166
|
+
'save',
|
|
167
|
+
'--url',
|
|
168
|
+
buildUrl(projectName, { token, protocol: 'http' }),
|
|
169
|
+
'--folder',
|
|
170
|
+
tempDir,
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
expect(result.exitCode).toBe(0);
|
|
174
|
+
expect(result.stdout).toContain('Authenticated as admin');
|
|
175
|
+
expect(result.stdout).toContain('Successfully saved');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
159
178
|
});
|
|
@@ -2,7 +2,11 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import { runCli, buildUrl } from '../utils/cli-runner';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createTestProject,
|
|
7
|
+
deleteTestProject,
|
|
8
|
+
generateProjectName,
|
|
9
|
+
} from '../utils/test-project';
|
|
6
10
|
import { api } from '../utils/api-client';
|
|
7
11
|
import { FIXTURES_PATH } from '../utils/constants';
|
|
8
12
|
|
|
@@ -88,6 +92,50 @@ describe('Migrate Commands', () => {
|
|
|
88
92
|
expect(updatedProject.rootBranch.headRevisionId).not.toBe(beforeHeadId);
|
|
89
93
|
});
|
|
90
94
|
|
|
95
|
+
it('creates project with --create-project when project does not exist', async () => {
|
|
96
|
+
const projectName = generateProjectName('e2e-create');
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const result = await runCli([
|
|
100
|
+
'migrate',
|
|
101
|
+
'apply',
|
|
102
|
+
'--url',
|
|
103
|
+
buildUrl(projectName, { token }),
|
|
104
|
+
'--file',
|
|
105
|
+
path.join(FIXTURES_PATH, 'migrations.json'),
|
|
106
|
+
'--create-project',
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
expect(result.exitCode).toBe(0);
|
|
110
|
+
expect(result.stdout).toContain('creating automatically');
|
|
111
|
+
expect(result.stdout).toContain('Successfully applied');
|
|
112
|
+
|
|
113
|
+
const project = await api.getProject('admin', projectName);
|
|
114
|
+
expect(project.name).toBe(projectName);
|
|
115
|
+
|
|
116
|
+
const tables = await api.getTables(project.rootBranch.draftRevisionId);
|
|
117
|
+
expect(tables.length).toBe(14);
|
|
118
|
+
} finally {
|
|
119
|
+
await deleteTestProject(projectName);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('fails with hint when project not found without --create-project', async () => {
|
|
124
|
+
const projectName = generateProjectName('e2e-no-create');
|
|
125
|
+
|
|
126
|
+
const result = await runCli([
|
|
127
|
+
'migrate',
|
|
128
|
+
'apply',
|
|
129
|
+
'--url',
|
|
130
|
+
buildUrl(projectName, { token }),
|
|
131
|
+
'--file',
|
|
132
|
+
path.join(FIXTURES_PATH, 'migrations.json'),
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
expect(result.exitCode).toBe(1);
|
|
136
|
+
expect(result.stdout + result.stderr).toContain('--create-project');
|
|
137
|
+
});
|
|
138
|
+
|
|
91
139
|
it('skips already applied migrations', async () => {
|
|
92
140
|
const project = await createTestProject();
|
|
93
141
|
|
package/e2e/utils/cli-runner.ts
CHANGED
|
@@ -81,11 +81,19 @@ export function buildUrl(
|
|
|
81
81
|
branch?: string;
|
|
82
82
|
revision?: 'draft' | 'head';
|
|
83
83
|
token?: string;
|
|
84
|
+
protocol?: 'http' | 'https';
|
|
84
85
|
} = {},
|
|
85
86
|
): string {
|
|
86
|
-
const {
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const {
|
|
88
|
+
orgId = 'admin',
|
|
89
|
+
branch = 'master',
|
|
90
|
+
revision,
|
|
91
|
+
token,
|
|
92
|
+
protocol,
|
|
93
|
+
} = options;
|
|
94
|
+
|
|
95
|
+
const scheme = protocol ? `revisium+${protocol}` : 'revisium';
|
|
96
|
+
let url = `${scheme}://localhost:8082/${orgId}/${projectName}/${branch}`;
|
|
89
97
|
|
|
90
98
|
if (revision) {
|
|
91
99
|
url += `:${revision}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "revisium",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"homepage": "https://revisium.io",
|
|
5
5
|
"description": "A CLI tool for interacting with Revisium instances, providing migration management, schema export, and data export capabilities.",
|
|
6
6
|
"author": "Anton Kashirov",
|
|
@@ -11,6 +11,7 @@ import { parseBooleanOption } from 'src/utils/parse-boolean.utils';
|
|
|
11
11
|
type Options = BaseOptions & {
|
|
12
12
|
file: string;
|
|
13
13
|
commit?: boolean;
|
|
14
|
+
createProject?: boolean;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
@SubCommand({
|
|
@@ -36,7 +37,10 @@ export class ApplyMigrationsCommand extends BaseCommand {
|
|
|
36
37
|
const jsonData = await this.validateJsonFile(options.file);
|
|
37
38
|
this.logger.success('Migration file validation passed');
|
|
38
39
|
|
|
39
|
-
await this.connectionService.connect(
|
|
40
|
+
await this.connectionService.connect({
|
|
41
|
+
url: options.url,
|
|
42
|
+
createProject: options.createProject,
|
|
43
|
+
});
|
|
40
44
|
|
|
41
45
|
const countAppliedMigrations = await this.applyMigration(jsonData);
|
|
42
46
|
|
|
@@ -133,4 +137,13 @@ export class ApplyMigrationsCommand extends BaseCommand {
|
|
|
133
137
|
parseCommit(value?: string): boolean {
|
|
134
138
|
return parseBooleanOption(value);
|
|
135
139
|
}
|
|
140
|
+
|
|
141
|
+
@Option({
|
|
142
|
+
flags: '--create-project [boolean]',
|
|
143
|
+
description:
|
|
144
|
+
'Automatically create the project if it does not exist (for CI/CD)',
|
|
145
|
+
})
|
|
146
|
+
parseCreateProject(value?: string): boolean {
|
|
147
|
+
return parseBooleanOption(value);
|
|
148
|
+
}
|
|
136
149
|
}
|
|
@@ -12,6 +12,7 @@ describe('ConnectionFactoryService', () => {
|
|
|
12
12
|
connecting: jest.Mock;
|
|
13
13
|
connected: jest.Mock;
|
|
14
14
|
authenticated: jest.Mock;
|
|
15
|
+
info: jest.Mock;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const mockDraftScope = {
|
|
@@ -55,6 +56,7 @@ describe('ConnectionFactoryService', () => {
|
|
|
55
56
|
connecting: jest.fn(),
|
|
56
57
|
connected: jest.fn(),
|
|
57
58
|
authenticated: jest.fn(),
|
|
59
|
+
info: jest.fn(),
|
|
58
60
|
};
|
|
59
61
|
|
|
60
62
|
const MockApiClient = RevisiumApiClient as jest.MockedClass<
|
|
@@ -114,4 +116,143 @@ describe('ConnectionFactoryService', () => {
|
|
|
114
116
|
expect(result.revisionScope).toBe(mockExplicitScope);
|
|
115
117
|
});
|
|
116
118
|
});
|
|
119
|
+
|
|
120
|
+
describe('createProject option', () => {
|
|
121
|
+
let mockOrgScope: { createProject: jest.Mock };
|
|
122
|
+
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
mockOrgScope = {
|
|
125
|
+
createProject: jest.fn().mockResolvedValue({ id: 'new-project-id' }),
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('creates project and retries when createProject is true and project not found', async () => {
|
|
130
|
+
const projectNotFoundError = new Error(
|
|
131
|
+
'A project with this name does not exist in the organization',
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const MockApiClient = RevisiumApiClient as jest.MockedClass<
|
|
135
|
+
typeof RevisiumApiClient
|
|
136
|
+
>;
|
|
137
|
+
MockApiClient.mockImplementation(
|
|
138
|
+
() =>
|
|
139
|
+
({
|
|
140
|
+
client: {
|
|
141
|
+
branch: jest
|
|
142
|
+
.fn()
|
|
143
|
+
.mockRejectedValueOnce(projectNotFoundError)
|
|
144
|
+
.mockResolvedValueOnce(mockBranchScope),
|
|
145
|
+
org: jest.fn().mockReturnValue(mockOrgScope),
|
|
146
|
+
},
|
|
147
|
+
authenticate: jest.fn().mockResolvedValue('test-user'),
|
|
148
|
+
}) as unknown as RevisiumApiClient,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
service = new ConnectionFactoryService(
|
|
152
|
+
urlBuilderFake as unknown as UrlBuilderService,
|
|
153
|
+
loggerFake as unknown as LoggerService,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const result = await service.createConnection(baseUrl, {
|
|
157
|
+
createProject: true,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(mockOrgScope.createProject).toHaveBeenCalledWith({
|
|
161
|
+
projectName: 'test-project',
|
|
162
|
+
});
|
|
163
|
+
expect(result.revisionScope).toBe(mockDraftScope);
|
|
164
|
+
expect(loggerFake.info).toHaveBeenCalledWith(
|
|
165
|
+
'Project "test-project" not found — creating automatically',
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('throws with hint when createProject is false and project not found', async () => {
|
|
170
|
+
const projectNotFoundError = new Error(
|
|
171
|
+
'A project with this name does not exist in the organization',
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const MockApiClient = RevisiumApiClient as jest.MockedClass<
|
|
175
|
+
typeof RevisiumApiClient
|
|
176
|
+
>;
|
|
177
|
+
MockApiClient.mockImplementation(
|
|
178
|
+
() =>
|
|
179
|
+
({
|
|
180
|
+
client: {
|
|
181
|
+
branch: jest.fn().mockRejectedValue(projectNotFoundError),
|
|
182
|
+
},
|
|
183
|
+
authenticate: jest.fn().mockResolvedValue('test-user'),
|
|
184
|
+
}) as unknown as RevisiumApiClient,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
service = new ConnectionFactoryService(
|
|
188
|
+
urlBuilderFake as unknown as UrlBuilderService,
|
|
189
|
+
loggerFake as unknown as LoggerService,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
await expect(service.createConnection(baseUrl)).rejects.toThrow(
|
|
193
|
+
'Use --create-project to auto-create',
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('creates project on generic not-found error when createProject is true', async () => {
|
|
198
|
+
const notFoundError = new Error('Resource not found');
|
|
199
|
+
|
|
200
|
+
const MockApiClient = RevisiumApiClient as jest.MockedClass<
|
|
201
|
+
typeof RevisiumApiClient
|
|
202
|
+
>;
|
|
203
|
+
MockApiClient.mockImplementation(
|
|
204
|
+
() =>
|
|
205
|
+
({
|
|
206
|
+
client: {
|
|
207
|
+
branch: jest
|
|
208
|
+
.fn()
|
|
209
|
+
.mockRejectedValueOnce(notFoundError)
|
|
210
|
+
.mockResolvedValueOnce(mockBranchScope),
|
|
211
|
+
org: jest.fn().mockReturnValue(mockOrgScope),
|
|
212
|
+
},
|
|
213
|
+
authenticate: jest.fn().mockResolvedValue('test-user'),
|
|
214
|
+
}) as unknown as RevisiumApiClient,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
service = new ConnectionFactoryService(
|
|
218
|
+
urlBuilderFake as unknown as UrlBuilderService,
|
|
219
|
+
loggerFake as unknown as LoggerService,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const result = await service.createConnection(baseUrl, {
|
|
223
|
+
createProject: true,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(mockOrgScope.createProject).toHaveBeenCalledWith({
|
|
227
|
+
projectName: 'test-project',
|
|
228
|
+
});
|
|
229
|
+
expect(result.revisionScope).toBe(mockDraftScope);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('rethrows non-project errors even with createProject', async () => {
|
|
233
|
+
const networkError = new Error('ECONNREFUSED');
|
|
234
|
+
|
|
235
|
+
const MockApiClient = RevisiumApiClient as jest.MockedClass<
|
|
236
|
+
typeof RevisiumApiClient
|
|
237
|
+
>;
|
|
238
|
+
MockApiClient.mockImplementation(
|
|
239
|
+
() =>
|
|
240
|
+
({
|
|
241
|
+
client: {
|
|
242
|
+
branch: jest.fn().mockRejectedValue(networkError),
|
|
243
|
+
},
|
|
244
|
+
authenticate: jest.fn().mockResolvedValue('test-user'),
|
|
245
|
+
}) as unknown as RevisiumApiClient,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
service = new ConnectionFactoryService(
|
|
249
|
+
urlBuilderFake as unknown as UrlBuilderService,
|
|
250
|
+
loggerFake as unknown as LoggerService,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
await expect(
|
|
254
|
+
service.createConnection(baseUrl, { createProject: true }),
|
|
255
|
+
).rejects.toThrow('ECONNREFUSED');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
117
258
|
});
|
|
@@ -13,6 +13,7 @@ export interface ConnectionInfo {
|
|
|
13
13
|
|
|
14
14
|
export interface ConnectOptions {
|
|
15
15
|
label?: string;
|
|
16
|
+
createProject?: boolean;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
@Injectable()
|
|
@@ -33,11 +34,7 @@ export class ConnectionFactoryService {
|
|
|
33
34
|
|
|
34
35
|
const client = await this.createAuthenticatedClient(url);
|
|
35
36
|
|
|
36
|
-
const branchScope = await
|
|
37
|
-
org: url.organization,
|
|
38
|
-
project: url.project,
|
|
39
|
-
branch: url.branch || 'master',
|
|
40
|
-
});
|
|
37
|
+
const branchScope = await this.resolveBranchScope(client, url, options);
|
|
41
38
|
|
|
42
39
|
const revisionScope = await this.resolveRevisionScope(
|
|
43
40
|
url.revision,
|
|
@@ -74,6 +71,51 @@ export class ConnectionFactoryService {
|
|
|
74
71
|
return client;
|
|
75
72
|
}
|
|
76
73
|
|
|
74
|
+
private async resolveBranchScope(
|
|
75
|
+
client: RevisiumApiClient,
|
|
76
|
+
url: RevisiumUrlComplete,
|
|
77
|
+
options: ConnectOptions,
|
|
78
|
+
): Promise<BranchScope> {
|
|
79
|
+
const branchOptions = {
|
|
80
|
+
org: url.organization,
|
|
81
|
+
project: url.project,
|
|
82
|
+
branch: url.branch || 'master',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
return await client.client.branch(branchOptions);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (options.createProject && this.isProjectNotFoundError(error)) {
|
|
89
|
+
this.logger.info(
|
|
90
|
+
`Project "${url.project}" not found — creating automatically`,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
await client.client
|
|
94
|
+
.org(url.organization)
|
|
95
|
+
.createProject({ projectName: url.project });
|
|
96
|
+
|
|
97
|
+
return client.client.branch(branchOptions);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!options.createProject && this.isProjectNotFoundError(error)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Project "${url.project}" not found in organization "${url.organization}". Use --create-project to auto-create`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private isProjectNotFoundError(error: unknown): boolean {
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
return (
|
|
113
|
+
message.includes('does not exist') ||
|
|
114
|
+
message.includes('not found') ||
|
|
115
|
+
message.includes('Not Found')
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
77
119
|
private async resolveRevisionScope(
|
|
78
120
|
revision: string,
|
|
79
121
|
branchScope: BranchScope,
|
|
@@ -11,6 +11,7 @@ export { ConnectionInfo } from './connection-factory.service';
|
|
|
11
11
|
|
|
12
12
|
export interface ConnectionOptions {
|
|
13
13
|
url?: string;
|
|
14
|
+
createProject?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
@Injectable()
|
|
@@ -38,7 +39,9 @@ export class ConnectionService {
|
|
|
38
39
|
const env = this.getEnvConfig();
|
|
39
40
|
const url = await this.urlBuilder.parseAndComplete(options.url, 'api', env);
|
|
40
41
|
|
|
41
|
-
this._connection = await this.connectionFactory.createConnection(url
|
|
42
|
+
this._connection = await this.connectionFactory.createConnection(url, {
|
|
43
|
+
createProject: options.createProject,
|
|
44
|
+
});
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
private getEnvConfig(): UrlEnvConfig {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { UrlParserService } from '../url-parser.service';
|
|
2
|
+
|
|
3
|
+
describe('UrlParserService', () => {
|
|
4
|
+
let service: UrlParserService;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
service = new UrlParserService();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('parse - revisium+http://', () => {
|
|
11
|
+
it('forces HTTP for non-localhost host', () => {
|
|
12
|
+
const result = service.parse(
|
|
13
|
+
'revisium+http://admin:pass@my-service:80/org/proj/branch',
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect(result).toEqual({
|
|
17
|
+
baseUrl: 'http://my-service:80',
|
|
18
|
+
username: 'admin',
|
|
19
|
+
password: 'pass',
|
|
20
|
+
token: undefined,
|
|
21
|
+
apikey: undefined,
|
|
22
|
+
organization: 'org',
|
|
23
|
+
project: 'proj',
|
|
24
|
+
branch: 'branch',
|
|
25
|
+
revision: undefined,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('forces HTTP for non-localhost host with draft revision', () => {
|
|
30
|
+
const result = service.parse(
|
|
31
|
+
'revisium+http://admin:pass@my-service:80/org/proj/branch:draft',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(result).toEqual({
|
|
35
|
+
baseUrl: 'http://my-service:80',
|
|
36
|
+
username: 'admin',
|
|
37
|
+
password: 'pass',
|
|
38
|
+
token: undefined,
|
|
39
|
+
apikey: undefined,
|
|
40
|
+
organization: 'org',
|
|
41
|
+
project: 'proj',
|
|
42
|
+
branch: 'branch',
|
|
43
|
+
revision: 'draft',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('forces HTTP without port', () => {
|
|
48
|
+
const result = service.parse(
|
|
49
|
+
'revisium+http://admin:pass@my-service/org/proj/branch',
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
baseUrl: 'http://my-service',
|
|
54
|
+
username: 'admin',
|
|
55
|
+
password: 'pass',
|
|
56
|
+
token: undefined,
|
|
57
|
+
apikey: undefined,
|
|
58
|
+
organization: 'org',
|
|
59
|
+
project: 'proj',
|
|
60
|
+
branch: 'branch',
|
|
61
|
+
revision: undefined,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('forces HTTP with token auth', () => {
|
|
66
|
+
const result = service.parse(
|
|
67
|
+
'revisium+http://payment-svc:80/org/proj/master?token=abc123',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(result).toEqual({
|
|
71
|
+
baseUrl: 'http://payment-svc:80',
|
|
72
|
+
username: undefined,
|
|
73
|
+
password: undefined,
|
|
74
|
+
token: 'abc123',
|
|
75
|
+
apikey: undefined,
|
|
76
|
+
organization: 'org',
|
|
77
|
+
project: 'proj',
|
|
78
|
+
branch: 'master',
|
|
79
|
+
revision: undefined,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('parse - revisium+https://', () => {
|
|
85
|
+
it('forces HTTPS for any host', () => {
|
|
86
|
+
const result = service.parse(
|
|
87
|
+
'revisium+https://admin:pass@my-service/org/proj/branch',
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(result).toEqual({
|
|
91
|
+
baseUrl: 'https://my-service',
|
|
92
|
+
username: 'admin',
|
|
93
|
+
password: 'pass',
|
|
94
|
+
token: undefined,
|
|
95
|
+
apikey: undefined,
|
|
96
|
+
organization: 'org',
|
|
97
|
+
project: 'proj',
|
|
98
|
+
branch: 'branch',
|
|
99
|
+
revision: undefined,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('forces HTTPS even for localhost', () => {
|
|
104
|
+
const result = service.parse(
|
|
105
|
+
'revisium+https://admin:pass@localhost:8443/org/proj/main',
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(result).toEqual({
|
|
109
|
+
baseUrl: 'https://localhost:8443',
|
|
110
|
+
username: 'admin',
|
|
111
|
+
password: 'pass',
|
|
112
|
+
token: undefined,
|
|
113
|
+
apikey: undefined,
|
|
114
|
+
organization: 'org',
|
|
115
|
+
project: 'proj',
|
|
116
|
+
branch: 'main',
|
|
117
|
+
revision: undefined,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('parse - revisium:// (unchanged behavior)', () => {
|
|
123
|
+
it('auto-detects http for localhost', () => {
|
|
124
|
+
const result = service.parse(
|
|
125
|
+
'revisium://admin:pass@localhost:8888/org/proj/branch',
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(result.baseUrl).toBe('http://localhost:8888');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('auto-detects https for remote host', () => {
|
|
132
|
+
const result = service.parse(
|
|
133
|
+
'revisium://admin:pass@cloud.revisium.io/org/proj/branch',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(result.baseUrl).toBe('https://cloud.revisium.io');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('buildBaseUrlFromHost - protocolOverride', () => {
|
|
141
|
+
it('uses override when provided', () => {
|
|
142
|
+
expect(service.buildBaseUrlFromHost('my-service:80', 'http')).toBe(
|
|
143
|
+
'http://my-service:80',
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('override takes precedence over localhost auto-detect', () => {
|
|
148
|
+
expect(service.buildBaseUrlFromHost('localhost:8080', 'https')).toBe(
|
|
149
|
+
'https://localhost:8080',
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('falls back to auto-detect when no override', () => {
|
|
154
|
+
expect(service.buildBaseUrlFromHost('localhost:8080')).toBe(
|
|
155
|
+
'http://localhost:8080',
|
|
156
|
+
);
|
|
157
|
+
expect(service.buildBaseUrlFromHost('cloud.revisium.io')).toBe(
|
|
158
|
+
'https://cloud.revisium.io',
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|