caprover-api 0.0.19 → 0.0.21
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/.github/workflows/publish.yml +32 -0
- package/dist/api/ApiManager.d.ts +5 -3
- package/dist/api/ApiManager.js +16 -3
- package/dist/api/HttpClient.d.ts +10 -3
- package/dist/api/HttpClient.js +63 -16
- package/dist/models/AppDefinition.d.ts +7 -0
- package/package.json +2 -2
- package/src/api/ApiManager.ts +37 -10
- package/src/api/HttpClient.ts +116 -17
- package/src/models/AppDefinition.ts +31 -0
- package/test/ApiManager.test.js +103 -0
- package/test/HttpClient.test.js +76 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Publish npm package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- release
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Check out repository
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Node.js
|
|
20
|
+
uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: 24
|
|
23
|
+
registry-url: https://registry.npmjs.org
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: npm ci
|
|
27
|
+
|
|
28
|
+
- name: Run tests
|
|
29
|
+
run: npm test
|
|
30
|
+
|
|
31
|
+
- name: Publish package
|
|
32
|
+
run: npm publish --access public
|
package/dist/api/ApiManager.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IAppDef } from '../models/AppDefinition';
|
|
1
|
+
import { IAppDef, IAppDefinitionPatch } from '../models/AppDefinition';
|
|
2
2
|
import AppDefinitionsResponse from '../models/AppDefinitionsResponse';
|
|
3
3
|
import AppDeleteResponse from '../models/AppDeleteResponse';
|
|
4
4
|
import { IAutomatedCleanupConfigs } from '../models/AutomatedCleanupConfigs';
|
|
@@ -75,11 +75,13 @@ export default class ApiManager {
|
|
|
75
75
|
getAllProjects(): Promise<ProjectsResponse>;
|
|
76
76
|
fetchBuildLogs(appName: string): Promise<BuildLogsResponse>;
|
|
77
77
|
fetchAppLogsInHex(appName: string): Promise<LogsResponse>;
|
|
78
|
-
|
|
78
|
+
fetchAppLogs(appName: string, encoding?: string): Promise<LogsResponse>;
|
|
79
|
+
uploadAppData(appName: string, file: File, detached?: boolean): Promise<void>;
|
|
79
80
|
registerProject(selectedProject: ProjectDefinition): Promise<ProjectDefinition>;
|
|
80
81
|
updateProject(project: ProjectDefinition): Promise<void>;
|
|
81
82
|
uploadCaptainDefinitionContent(appName: string, captainDefinition: ICaptainDefinition, gitHash: string, detached: boolean): Promise<void>;
|
|
82
83
|
updateConfigAndSave(appName: string, appDefinition: IAppDef): Promise<void>;
|
|
84
|
+
patchAppDefinition(appName: string, patch: IAppDefinitionPatch): Promise<void>;
|
|
83
85
|
renameApp(oldAppName: string, newAppName: string): Promise<void>;
|
|
84
86
|
registerNewApp(appName: string, projectId: string, hasPersistentData: boolean, detached: boolean): Promise<void>;
|
|
85
87
|
deleteApp(appName: string | undefined, volumes: string[], appNames: string[] | undefined): Promise<AppDeleteResponse>;
|
|
@@ -128,5 +130,5 @@ export default class ApiManager {
|
|
|
128
130
|
jobId: string;
|
|
129
131
|
}>;
|
|
130
132
|
getOneClickAppDeployProgress(jobId: string): Promise<OneClickAppDeploymentState>;
|
|
131
|
-
executeGenericApiCommand(verb: 'GET' | 'POST', endpoint: string, data: any): Promise<any>;
|
|
133
|
+
executeGenericApiCommand(verb: 'GET' | 'POST' | 'PATCH', endpoint: string, data: any): Promise<any>;
|
|
132
134
|
}
|
package/dist/api/ApiManager.js
CHANGED
|
@@ -163,16 +163,21 @@ class ApiManager {
|
|
|
163
163
|
.then(http.fetch(http.GET, `/user/apps/appData/${appName}`, {}));
|
|
164
164
|
}
|
|
165
165
|
fetchAppLogsInHex(appName) {
|
|
166
|
+
return this.fetchAppLogs(appName, 'hex');
|
|
167
|
+
}
|
|
168
|
+
fetchAppLogs(appName, encoding = 'ascii') {
|
|
166
169
|
const http = this.http;
|
|
167
170
|
return Promise.resolve() //
|
|
168
|
-
.then(http.fetch(http.GET, `/user/apps/appData/${appName}/logs
|
|
171
|
+
.then(http.fetch(http.GET, `/user/apps/appData/${appName}/logs`, {
|
|
172
|
+
encoding,
|
|
173
|
+
}));
|
|
169
174
|
}
|
|
170
|
-
uploadAppData(appName, file) {
|
|
175
|
+
uploadAppData(appName, file, detached = true) {
|
|
171
176
|
const http = this.http;
|
|
172
177
|
let formData = new FormData();
|
|
173
178
|
formData.append('sourceFile', file);
|
|
174
179
|
return Promise.resolve() //
|
|
175
|
-
.then(http.fetch(http.POST, `/user/apps/appData/${appName}?detached=1`, formData));
|
|
180
|
+
.then(http.fetch(http.POST, `/user/apps/appData/${appName}${detached ? '?detached=1' : ''}`, formData, http.FORM_DATA));
|
|
176
181
|
}
|
|
177
182
|
registerProject(selectedProject) {
|
|
178
183
|
const http = this.http;
|
|
@@ -243,6 +248,14 @@ class ApiManager {
|
|
|
243
248
|
projectId: projectId,
|
|
244
249
|
}));
|
|
245
250
|
}
|
|
251
|
+
patchAppDefinition(appName, patch) {
|
|
252
|
+
const http = this.http;
|
|
253
|
+
return Promise.resolve() //
|
|
254
|
+
.then(http.fetch(http.PATCH, '/user/apps/appDefinitions/update', {
|
|
255
|
+
...patch,
|
|
256
|
+
appName,
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
246
259
|
renameApp(oldAppName, newAppName) {
|
|
247
260
|
const http = this.http;
|
|
248
261
|
return Promise.resolve() //
|
package/dist/api/HttpClient.d.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
export type RequestBodyType = 'json' | 'form-data';
|
|
2
|
+
export type HttpMethod = 'GET' | 'POST' | 'PATCH';
|
|
3
|
+
export declare function createPostRequestInit(variables: any, headers: Record<string, string>, bodyType: RequestBodyType): RequestInit;
|
|
4
|
+
export declare function createPatchRequestInit(variables: any, headers: Record<string, string>): RequestInit;
|
|
1
5
|
export default class HttpClient {
|
|
2
6
|
private baseUrl;
|
|
3
7
|
private authTokenProvider;
|
|
4
8
|
private onLoginRequested;
|
|
5
9
|
readonly GET = "GET";
|
|
6
10
|
readonly POST = "POST";
|
|
11
|
+
readonly PATCH = "PATCH";
|
|
12
|
+
readonly FORM_DATA = "form-data";
|
|
7
13
|
isDestroyed: boolean;
|
|
8
14
|
constructor(baseUrl: string, authTokenProvider: () => Promise<string>, onLoginRequested: () => Promise<void>);
|
|
9
15
|
createHeaders(): Promise<any>;
|
|
10
16
|
destroy(): void;
|
|
11
|
-
fetch(method:
|
|
12
|
-
fetchInternal(method:
|
|
17
|
+
fetch(method: HttpMethod, endpoint: string, variables: any, bodyType?: RequestBodyType): () => Promise<any>;
|
|
18
|
+
fetchInternal(method: HttpMethod, endpoint: string, variables: any, bodyType?: RequestBodyType): Promise<any>;
|
|
13
19
|
getReq(endpoint: string, variables: any): Promise<any>;
|
|
14
|
-
postReq(endpoint: string, variables: any): Promise<any>;
|
|
20
|
+
postReq(endpoint: string, variables: any, bodyType?: RequestBodyType): Promise<any>;
|
|
21
|
+
patchReq(endpoint: string, variables: any): Promise<any>;
|
|
15
22
|
}
|
package/dist/api/HttpClient.js
CHANGED
|
@@ -3,8 +3,37 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createPostRequestInit = createPostRequestInit;
|
|
7
|
+
exports.createPatchRequestInit = createPatchRequestInit;
|
|
6
8
|
const ErrorFactory_1 = __importDefault(require("./ErrorFactory"));
|
|
7
9
|
const cross_fetch_1 = __importDefault(require("cross-fetch"));
|
|
10
|
+
function createPostRequestInit(variables, headers, bodyType) {
|
|
11
|
+
if (bodyType === 'form-data') {
|
|
12
|
+
return {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers,
|
|
15
|
+
body: variables,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
...headers,
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify(variables),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createPatchRequestInit(variables, headers) {
|
|
28
|
+
return {
|
|
29
|
+
method: 'PATCH',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
...headers,
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(variables),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
8
37
|
function buildQueryParams(params) {
|
|
9
38
|
if (!params || Object.keys(params).length === 0)
|
|
10
39
|
return '';
|
|
@@ -17,15 +46,8 @@ let TOKEN_HEADER = 'x-captain-auth';
|
|
|
17
46
|
let NAMESPACE = 'x-namespace';
|
|
18
47
|
let CAPTAIN = 'captain';
|
|
19
48
|
class CrossFetchEngine {
|
|
20
|
-
static async post(url, variables, headers) {
|
|
21
|
-
const res = await (0, cross_fetch_1.default)(url,
|
|
22
|
-
method: 'POST',
|
|
23
|
-
headers: {
|
|
24
|
-
'Content-Type': 'application/json',
|
|
25
|
-
...headers,
|
|
26
|
-
},
|
|
27
|
-
body: JSON.stringify(variables),
|
|
28
|
-
});
|
|
49
|
+
static async post(url, variables, headers, bodyType) {
|
|
50
|
+
const res = await (0, cross_fetch_1.default)(url, createPostRequestInit(variables, headers, bodyType));
|
|
29
51
|
if (!res.ok) {
|
|
30
52
|
const errBody = await res.text();
|
|
31
53
|
throw new Error(`HTTP ${res.status}: ${errBody}`);
|
|
@@ -50,6 +72,14 @@ class CrossFetchEngine {
|
|
|
50
72
|
}
|
|
51
73
|
return res.json();
|
|
52
74
|
}
|
|
75
|
+
static async patch(url, variables, headers) {
|
|
76
|
+
const res = await (0, cross_fetch_1.default)(url, createPatchRequestInit(variables, headers));
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const errBody = await res.text();
|
|
79
|
+
throw new Error(`HTTP ${res.status}: ${errBody}`);
|
|
80
|
+
}
|
|
81
|
+
return res.json();
|
|
82
|
+
}
|
|
53
83
|
}
|
|
54
84
|
class HttpClient {
|
|
55
85
|
constructor(baseUrl, authTokenProvider, onLoginRequested) {
|
|
@@ -58,6 +88,8 @@ class HttpClient {
|
|
|
58
88
|
this.onLoginRequested = onLoginRequested;
|
|
59
89
|
this.GET = 'GET';
|
|
60
90
|
this.POST = 'POST';
|
|
91
|
+
this.PATCH = 'PATCH';
|
|
92
|
+
this.FORM_DATA = 'form-data';
|
|
61
93
|
this.isDestroyed = false;
|
|
62
94
|
//
|
|
63
95
|
}
|
|
@@ -78,12 +110,12 @@ class HttpClient {
|
|
|
78
110
|
destroy() {
|
|
79
111
|
this.isDestroyed = true;
|
|
80
112
|
}
|
|
81
|
-
fetch(method, endpoint, variables) {
|
|
113
|
+
fetch(method, endpoint, variables, bodyType = 'json') {
|
|
82
114
|
const self = this;
|
|
83
115
|
return function () {
|
|
84
116
|
return Promise.resolve() //
|
|
85
117
|
.then(function () {
|
|
86
|
-
return self.fetchInternal(method, endpoint, variables); //
|
|
118
|
+
return self.fetchInternal(method, endpoint, variables, bodyType); //
|
|
87
119
|
})
|
|
88
120
|
.then(function (fetchResponse) {
|
|
89
121
|
if (
|
|
@@ -94,7 +126,7 @@ class HttpClient {
|
|
|
94
126
|
.onLoginRequested() //
|
|
95
127
|
.then(function () {
|
|
96
128
|
return self
|
|
97
|
-
.fetchInternal(method, endpoint, variables)
|
|
129
|
+
.fetchInternal(method, endpoint, variables, bodyType)
|
|
98
130
|
.then(function (httpResponse) {
|
|
99
131
|
return httpResponse;
|
|
100
132
|
});
|
|
@@ -134,11 +166,13 @@ class HttpClient {
|
|
|
134
166
|
});
|
|
135
167
|
};
|
|
136
168
|
}
|
|
137
|
-
fetchInternal(method, endpoint, variables) {
|
|
169
|
+
fetchInternal(method, endpoint, variables, bodyType = 'json') {
|
|
138
170
|
if (method === this.GET)
|
|
139
171
|
return this.getReq(endpoint, variables);
|
|
140
172
|
if (method === this.POST)
|
|
141
|
-
return this.postReq(endpoint, variables);
|
|
173
|
+
return this.postReq(endpoint, variables, bodyType);
|
|
174
|
+
if (method === this.PATCH)
|
|
175
|
+
return this.patchReq(endpoint, variables);
|
|
142
176
|
throw new Error(`Unknown method: ${method}`);
|
|
143
177
|
}
|
|
144
178
|
getReq(endpoint, variables) {
|
|
@@ -155,19 +189,32 @@ class HttpClient {
|
|
|
155
189
|
return data;
|
|
156
190
|
});
|
|
157
191
|
}
|
|
158
|
-
postReq(endpoint, variables) {
|
|
192
|
+
postReq(endpoint, variables, bodyType = 'json') {
|
|
159
193
|
const self = this;
|
|
160
194
|
return Promise.resolve() //
|
|
161
195
|
.then(function () {
|
|
162
196
|
return self.createHeaders();
|
|
163
197
|
})
|
|
164
198
|
.then(function (headers) {
|
|
165
|
-
return CrossFetchEngine.post(self.baseUrl + endpoint, variables, headers);
|
|
199
|
+
return CrossFetchEngine.post(self.baseUrl + endpoint, variables, headers, bodyType);
|
|
166
200
|
})
|
|
167
201
|
.then(function (data) {
|
|
168
202
|
// console.log(data);
|
|
169
203
|
return data;
|
|
170
204
|
});
|
|
171
205
|
}
|
|
206
|
+
patchReq(endpoint, variables) {
|
|
207
|
+
const self = this;
|
|
208
|
+
return Promise.resolve() //
|
|
209
|
+
.then(function () {
|
|
210
|
+
return self.createHeaders();
|
|
211
|
+
})
|
|
212
|
+
.then(function (headers) {
|
|
213
|
+
return CrossFetchEngine.patch(self.baseUrl + endpoint, variables, headers);
|
|
214
|
+
})
|
|
215
|
+
.then(function (data) {
|
|
216
|
+
return data;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
172
219
|
}
|
|
173
220
|
exports.default = HttpClient;
|
|
@@ -72,6 +72,7 @@ export interface IAppDefinitionBase {
|
|
|
72
72
|
envVars: IAppEnvVar[];
|
|
73
73
|
versions: IAppVersion[];
|
|
74
74
|
appDeployTokenConfig?: AppDeployTokenConfig;
|
|
75
|
+
isLegacyAppName?: boolean;
|
|
75
76
|
}
|
|
76
77
|
export interface IHttpAuth {
|
|
77
78
|
user: string;
|
|
@@ -82,6 +83,12 @@ export interface AppDeployTokenConfig {
|
|
|
82
83
|
enabled: boolean;
|
|
83
84
|
appDeployToken?: string;
|
|
84
85
|
}
|
|
86
|
+
export type IAppDefinitionPatch = Partial<Pick<IAppDefinitionBase, 'projectId' | 'description' | 'instanceCount' | 'captainDefinitionRelativeFilePath' | 'envVars' | 'volumes' | 'tags' | 'nodeId' | 'notExposeAsWebApp' | 'containerHttpPort' | 'forceSsl' | 'ports' | 'customNginxConfig' | 'redirectDomain' | 'preDeployFunction' | 'serviceUpdateOverride' | 'websocketSupport' | 'appDeployTokenConfig'>> & {
|
|
87
|
+
httpAuth?: IHttpAuth;
|
|
88
|
+
appPushWebhook?: {
|
|
89
|
+
repoInfo?: RepoInfo;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
85
92
|
export interface IAppDef extends IAppDefinitionBase {
|
|
86
93
|
appPushWebhook?: {
|
|
87
94
|
tokenVersion: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caprover-api",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"description": "API client for CapRover",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"formatter-write": "prettier --write './src/**/*.ts*'",
|
|
10
10
|
"tslint": "tslint -c tslint.json -p tsconfig.json",
|
|
11
11
|
"tslint-fix": "tslint --fix -c tslint.json -p tsconfig.json",
|
|
12
|
-
"test": "
|
|
12
|
+
"test": "npm run build && node --test test/*.test.js",
|
|
13
13
|
"build": "rm -rf ./dist && npm run tslint && npx tsc && chmod +x ./dist -R",
|
|
14
14
|
"dev": "rm -rf ./dist && npx tsc && node ./dist/example.js",
|
|
15
15
|
"generate-barrels": "rm ./src/models/index.ts && barrelsby --directory src/models"
|
package/src/api/ApiManager.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IAppDef } from '../models/AppDefinition'
|
|
1
|
+
import { IAppDef, IAppDefinitionPatch } from '../models/AppDefinition'
|
|
2
2
|
import AppDefinitionsResponse from '../models/AppDefinitionsResponse'
|
|
3
3
|
import AppDeleteResponse from '../models/AppDeleteResponse'
|
|
4
4
|
import { IAutomatedCleanupConfigs } from '../models/AutomatedCleanupConfigs'
|
|
@@ -270,19 +270,28 @@ export default class ApiManager {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
fetchAppLogsInHex(appName: string): Promise<LogsResponse> {
|
|
273
|
+
return this.fetchAppLogs(appName, 'hex')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fetchAppLogs(
|
|
277
|
+
appName: string,
|
|
278
|
+
encoding: string = 'ascii'
|
|
279
|
+
): Promise<LogsResponse> {
|
|
273
280
|
const http = this.http
|
|
274
281
|
|
|
275
282
|
return Promise.resolve() //
|
|
276
283
|
.then(
|
|
277
|
-
http.fetch(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
{}
|
|
281
|
-
)
|
|
284
|
+
http.fetch(http.GET, `/user/apps/appData/${appName}/logs`, {
|
|
285
|
+
encoding,
|
|
286
|
+
})
|
|
282
287
|
)
|
|
283
288
|
}
|
|
284
289
|
|
|
285
|
-
uploadAppData(
|
|
290
|
+
uploadAppData(
|
|
291
|
+
appName: string,
|
|
292
|
+
file: File,
|
|
293
|
+
detached: boolean = true
|
|
294
|
+
): Promise<void> {
|
|
286
295
|
const http = this.http
|
|
287
296
|
let formData = new FormData()
|
|
288
297
|
formData.append('sourceFile', file)
|
|
@@ -290,8 +299,11 @@ export default class ApiManager {
|
|
|
290
299
|
.then(
|
|
291
300
|
http.fetch(
|
|
292
301
|
http.POST,
|
|
293
|
-
`/user/apps/appData/${appName}
|
|
294
|
-
|
|
302
|
+
`/user/apps/appData/${appName}${
|
|
303
|
+
detached ? '?detached=1' : ''
|
|
304
|
+
}`,
|
|
305
|
+
formData,
|
|
306
|
+
http.FORM_DATA
|
|
295
307
|
)
|
|
296
308
|
)
|
|
297
309
|
}
|
|
@@ -398,6 +410,21 @@ export default class ApiManager {
|
|
|
398
410
|
)
|
|
399
411
|
}
|
|
400
412
|
|
|
413
|
+
patchAppDefinition(
|
|
414
|
+
appName: string,
|
|
415
|
+
patch: IAppDefinitionPatch
|
|
416
|
+
): Promise<void> {
|
|
417
|
+
const http = this.http
|
|
418
|
+
|
|
419
|
+
return Promise.resolve() //
|
|
420
|
+
.then(
|
|
421
|
+
http.fetch(http.PATCH, '/user/apps/appDefinitions/update', {
|
|
422
|
+
...patch,
|
|
423
|
+
appName,
|
|
424
|
+
})
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
401
428
|
renameApp(oldAppName: string, newAppName: string): Promise<void> {
|
|
402
429
|
const http = this.http
|
|
403
430
|
|
|
@@ -894,7 +921,7 @@ export default class ApiManager {
|
|
|
894
921
|
}
|
|
895
922
|
|
|
896
923
|
executeGenericApiCommand(
|
|
897
|
-
verb: 'GET' | 'POST',
|
|
924
|
+
verb: 'GET' | 'POST' | 'PATCH',
|
|
898
925
|
endpoint: string,
|
|
899
926
|
data: any
|
|
900
927
|
): Promise<any> {
|
package/src/api/HttpClient.ts
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
import ErrorFactory from './ErrorFactory'
|
|
2
2
|
import fetch from 'cross-fetch'
|
|
3
3
|
|
|
4
|
+
export type RequestBodyType = 'json' | 'form-data'
|
|
5
|
+
export type HttpMethod = 'GET' | 'POST' | 'PATCH'
|
|
6
|
+
|
|
7
|
+
export function createPostRequestInit(
|
|
8
|
+
variables: any,
|
|
9
|
+
headers: Record<string, string>,
|
|
10
|
+
bodyType: RequestBodyType
|
|
11
|
+
): RequestInit {
|
|
12
|
+
if (bodyType === 'form-data') {
|
|
13
|
+
return {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers,
|
|
16
|
+
body: variables,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
...headers,
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify(variables),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createPatchRequestInit(
|
|
31
|
+
variables: any,
|
|
32
|
+
headers: Record<string, string>
|
|
33
|
+
): RequestInit {
|
|
34
|
+
return {
|
|
35
|
+
method: 'PATCH',
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
...headers,
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify(variables),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
4
44
|
function buildQueryParams(params: Record<string, any>): string {
|
|
5
45
|
if (!params || Object.keys(params).length === 0) return ''
|
|
6
46
|
return (
|
|
@@ -20,17 +60,14 @@ let CAPTAIN = 'captain'
|
|
|
20
60
|
class CrossFetchEngine {
|
|
21
61
|
public static async post(
|
|
22
62
|
url: string,
|
|
23
|
-
variables:
|
|
24
|
-
headers: Record<string, string
|
|
63
|
+
variables: any,
|
|
64
|
+
headers: Record<string, string>,
|
|
65
|
+
bodyType: RequestBodyType
|
|
25
66
|
): Promise<any> {
|
|
26
|
-
const res = await fetch(
|
|
27
|
-
|
|
28
|
-
headers
|
|
29
|
-
|
|
30
|
-
...headers,
|
|
31
|
-
},
|
|
32
|
-
body: JSON.stringify(variables),
|
|
33
|
-
})
|
|
67
|
+
const res = await fetch(
|
|
68
|
+
url,
|
|
69
|
+
createPostRequestInit(variables, headers, bodyType)
|
|
70
|
+
)
|
|
34
71
|
|
|
35
72
|
if (!res.ok) {
|
|
36
73
|
const errBody = await res.text()
|
|
@@ -64,11 +101,27 @@ class CrossFetchEngine {
|
|
|
64
101
|
}
|
|
65
102
|
return res.json()
|
|
66
103
|
}
|
|
104
|
+
|
|
105
|
+
public static async patch(
|
|
106
|
+
url: string,
|
|
107
|
+
variables: any,
|
|
108
|
+
headers: Record<string, string>
|
|
109
|
+
): Promise<any> {
|
|
110
|
+
const res = await fetch(url, createPatchRequestInit(variables, headers))
|
|
111
|
+
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const errBody = await res.text()
|
|
114
|
+
throw new Error(`HTTP ${res.status}: ${errBody}`)
|
|
115
|
+
}
|
|
116
|
+
return res.json()
|
|
117
|
+
}
|
|
67
118
|
}
|
|
68
119
|
|
|
69
120
|
export default class HttpClient {
|
|
70
121
|
public readonly GET = 'GET'
|
|
71
122
|
public readonly POST = 'POST'
|
|
123
|
+
public readonly PATCH = 'PATCH'
|
|
124
|
+
public readonly FORM_DATA = 'form-data'
|
|
72
125
|
public isDestroyed = false
|
|
73
126
|
|
|
74
127
|
constructor(
|
|
@@ -99,12 +152,22 @@ export default class HttpClient {
|
|
|
99
152
|
this.isDestroyed = true
|
|
100
153
|
}
|
|
101
154
|
|
|
102
|
-
fetch(
|
|
155
|
+
fetch(
|
|
156
|
+
method: HttpMethod,
|
|
157
|
+
endpoint: string,
|
|
158
|
+
variables: any,
|
|
159
|
+
bodyType: RequestBodyType = 'json'
|
|
160
|
+
) {
|
|
103
161
|
const self = this
|
|
104
162
|
return function (): Promise<any> {
|
|
105
163
|
return Promise.resolve() //
|
|
106
164
|
.then(function () {
|
|
107
|
-
return self.fetchInternal(
|
|
165
|
+
return self.fetchInternal(
|
|
166
|
+
method,
|
|
167
|
+
endpoint,
|
|
168
|
+
variables,
|
|
169
|
+
bodyType
|
|
170
|
+
) //
|
|
108
171
|
})
|
|
109
172
|
.then(function (fetchResponse) {
|
|
110
173
|
if (
|
|
@@ -116,7 +179,12 @@ export default class HttpClient {
|
|
|
116
179
|
.onLoginRequested() //
|
|
117
180
|
.then(function () {
|
|
118
181
|
return self
|
|
119
|
-
.fetchInternal(
|
|
182
|
+
.fetchInternal(
|
|
183
|
+
method,
|
|
184
|
+
endpoint,
|
|
185
|
+
variables,
|
|
186
|
+
bodyType
|
|
187
|
+
)
|
|
120
188
|
.then(function (httpResponse) {
|
|
121
189
|
return httpResponse
|
|
122
190
|
})
|
|
@@ -159,10 +227,18 @@ export default class HttpClient {
|
|
|
159
227
|
}
|
|
160
228
|
}
|
|
161
229
|
|
|
162
|
-
fetchInternal(
|
|
230
|
+
fetchInternal(
|
|
231
|
+
method: HttpMethod,
|
|
232
|
+
endpoint: string,
|
|
233
|
+
variables: any,
|
|
234
|
+
bodyType: RequestBodyType = 'json'
|
|
235
|
+
) {
|
|
163
236
|
if (method === this.GET) return this.getReq(endpoint, variables)
|
|
164
237
|
|
|
165
|
-
if (method === this.POST)
|
|
238
|
+
if (method === this.POST)
|
|
239
|
+
return this.postReq(endpoint, variables, bodyType)
|
|
240
|
+
|
|
241
|
+
if (method === this.PATCH) return this.patchReq(endpoint, variables)
|
|
166
242
|
|
|
167
243
|
throw new Error(`Unknown method: ${method}`)
|
|
168
244
|
}
|
|
@@ -186,7 +262,11 @@ export default class HttpClient {
|
|
|
186
262
|
})
|
|
187
263
|
}
|
|
188
264
|
|
|
189
|
-
postReq(
|
|
265
|
+
postReq(
|
|
266
|
+
endpoint: string,
|
|
267
|
+
variables: any,
|
|
268
|
+
bodyType: RequestBodyType = 'json'
|
|
269
|
+
) {
|
|
190
270
|
const self = this
|
|
191
271
|
return Promise.resolve() //
|
|
192
272
|
.then(function () {
|
|
@@ -196,7 +276,8 @@ export default class HttpClient {
|
|
|
196
276
|
return CrossFetchEngine.post(
|
|
197
277
|
self.baseUrl + endpoint,
|
|
198
278
|
variables,
|
|
199
|
-
headers
|
|
279
|
+
headers,
|
|
280
|
+
bodyType
|
|
200
281
|
)
|
|
201
282
|
})
|
|
202
283
|
.then(function (data) {
|
|
@@ -204,4 +285,22 @@ export default class HttpClient {
|
|
|
204
285
|
return data
|
|
205
286
|
})
|
|
206
287
|
}
|
|
288
|
+
|
|
289
|
+
patchReq(endpoint: string, variables: any) {
|
|
290
|
+
const self = this
|
|
291
|
+
return Promise.resolve() //
|
|
292
|
+
.then(function () {
|
|
293
|
+
return self.createHeaders()
|
|
294
|
+
})
|
|
295
|
+
.then(function (headers) {
|
|
296
|
+
return CrossFetchEngine.patch(
|
|
297
|
+
self.baseUrl + endpoint,
|
|
298
|
+
variables,
|
|
299
|
+
headers
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
.then(function (data) {
|
|
303
|
+
return data
|
|
304
|
+
})
|
|
305
|
+
}
|
|
207
306
|
}
|
|
@@ -84,6 +84,10 @@ export interface IAppDefinitionBase {
|
|
|
84
84
|
envVars: IAppEnvVar[]
|
|
85
85
|
versions: IAppVersion[]
|
|
86
86
|
appDeployTokenConfig?: AppDeployTokenConfig
|
|
87
|
+
|
|
88
|
+
// True for apps created before v1.15.0
|
|
89
|
+
// non-existent for apps created on or after v1.15.0
|
|
90
|
+
isLegacyAppName?: boolean
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
export interface IHttpAuth {
|
|
@@ -97,6 +101,33 @@ export interface AppDeployTokenConfig {
|
|
|
97
101
|
appDeployToken?: string
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
export type IAppDefinitionPatch = Partial<
|
|
105
|
+
Pick<
|
|
106
|
+
IAppDefinitionBase,
|
|
107
|
+
| 'projectId'
|
|
108
|
+
| 'description'
|
|
109
|
+
| 'instanceCount'
|
|
110
|
+
| 'captainDefinitionRelativeFilePath'
|
|
111
|
+
| 'envVars'
|
|
112
|
+
| 'volumes'
|
|
113
|
+
| 'tags'
|
|
114
|
+
| 'nodeId'
|
|
115
|
+
| 'notExposeAsWebApp'
|
|
116
|
+
| 'containerHttpPort'
|
|
117
|
+
| 'forceSsl'
|
|
118
|
+
| 'ports'
|
|
119
|
+
| 'customNginxConfig'
|
|
120
|
+
| 'redirectDomain'
|
|
121
|
+
| 'preDeployFunction'
|
|
122
|
+
| 'serviceUpdateOverride'
|
|
123
|
+
| 'websocketSupport'
|
|
124
|
+
| 'appDeployTokenConfig'
|
|
125
|
+
>
|
|
126
|
+
> & {
|
|
127
|
+
httpAuth?: IHttpAuth
|
|
128
|
+
appPushWebhook?: { repoInfo?: RepoInfo }
|
|
129
|
+
}
|
|
130
|
+
|
|
100
131
|
export interface IAppDef extends IAppDefinitionBase {
|
|
101
132
|
appPushWebhook?: {
|
|
102
133
|
tokenVersion: string
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const test = require('node:test')
|
|
3
|
+
|
|
4
|
+
const ApiManagerModule = require('../dist/api/ApiManager')
|
|
5
|
+
const ApiManager = ApiManagerModule.default
|
|
6
|
+
const { SimpleAuthenticationProvider } = ApiManagerModule
|
|
7
|
+
|
|
8
|
+
function createApiWithRequestRecorder() {
|
|
9
|
+
const api = new ApiManager(
|
|
10
|
+
'https://captain.example.com',
|
|
11
|
+
new SimpleAuthenticationProvider(() =>
|
|
12
|
+
Promise.resolve({ password: 'password' })
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
const requests = []
|
|
16
|
+
|
|
17
|
+
api.http = {
|
|
18
|
+
GET: 'GET',
|
|
19
|
+
POST: 'POST',
|
|
20
|
+
PATCH: 'PATCH',
|
|
21
|
+
FORM_DATA: 'form-data',
|
|
22
|
+
fetch(method, endpoint, data, bodyType = 'json') {
|
|
23
|
+
requests.push({ method, endpoint, data, bodyType })
|
|
24
|
+
return () => Promise.resolve({})
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { api, requests }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('patchAppDefinition uses the partial update endpoint', async () => {
|
|
32
|
+
const { api, requests } = createApiWithRequestRecorder()
|
|
33
|
+
|
|
34
|
+
await api.patchAppDefinition('test-app', {
|
|
35
|
+
instanceCount: 2,
|
|
36
|
+
description: 'scaled app',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(requests, [
|
|
40
|
+
{
|
|
41
|
+
method: 'PATCH',
|
|
42
|
+
endpoint: '/user/apps/appDefinitions/update',
|
|
43
|
+
data: {
|
|
44
|
+
instanceCount: 2,
|
|
45
|
+
description: 'scaled app',
|
|
46
|
+
appName: 'test-app',
|
|
47
|
+
},
|
|
48
|
+
bodyType: 'json',
|
|
49
|
+
},
|
|
50
|
+
])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('fetchAppLogs exposes the backend encoding argument', async () => {
|
|
54
|
+
const { api, requests } = createApiWithRequestRecorder()
|
|
55
|
+
|
|
56
|
+
await api.fetchAppLogs('test-app', 'utf8')
|
|
57
|
+
|
|
58
|
+
assert.deepEqual(requests[0], {
|
|
59
|
+
method: 'GET',
|
|
60
|
+
endpoint: '/user/apps/appData/test-app/logs',
|
|
61
|
+
data: { encoding: 'utf8' },
|
|
62
|
+
bodyType: 'json',
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('fetchAppLogsInHex remains backward compatible', async () => {
|
|
67
|
+
const { api, requests } = createApiWithRequestRecorder()
|
|
68
|
+
|
|
69
|
+
await api.fetchAppLogsInHex('test-app')
|
|
70
|
+
|
|
71
|
+
assert.deepEqual(requests[0].data, { encoding: 'hex' })
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('uploadAppData keeps detached deployment as the default', async () => {
|
|
75
|
+
const { api, requests } = createApiWithRequestRecorder()
|
|
76
|
+
|
|
77
|
+
await api.uploadAppData('test-app', 'tar contents')
|
|
78
|
+
|
|
79
|
+
assert.equal(
|
|
80
|
+
requests[0].endpoint,
|
|
81
|
+
'/user/apps/appData/test-app?detached=1'
|
|
82
|
+
)
|
|
83
|
+
assert.equal(requests[0].bodyType, 'form-data')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('uploadAppData can wait for an attached deployment', async () => {
|
|
87
|
+
const { api, requests } = createApiWithRequestRecorder()
|
|
88
|
+
|
|
89
|
+
await api.uploadAppData('test-app', 'tar contents', false)
|
|
90
|
+
|
|
91
|
+
assert.equal(requests[0].endpoint, '/user/apps/appData/test-app')
|
|
92
|
+
assert.equal(requests[0].bodyType, 'form-data')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('executeGenericApiCommand accepts PATCH', async () => {
|
|
96
|
+
const { api, requests } = createApiWithRequestRecorder()
|
|
97
|
+
|
|
98
|
+
await api.executeGenericApiCommand('PATCH', '/user/example', {
|
|
99
|
+
value: true,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
assert.equal(requests[0].method, 'PATCH')
|
|
103
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const test = require('node:test')
|
|
3
|
+
|
|
4
|
+
const ErrorFactory = require('../dist/api/ErrorFactory').default
|
|
5
|
+
const HttpClientModule = require('../dist/api/HttpClient')
|
|
6
|
+
const HttpClient = HttpClientModule.default
|
|
7
|
+
const { createPostRequestInit } = HttpClientModule
|
|
8
|
+
const { createPatchRequestInit } = HttpClientModule
|
|
9
|
+
|
|
10
|
+
test('JSON requests preserve the existing serialization and content type', () => {
|
|
11
|
+
const payload = { appName: 'test-app' }
|
|
12
|
+
const request = createPostRequestInit(
|
|
13
|
+
payload,
|
|
14
|
+
{ 'x-captain-auth': 'token' },
|
|
15
|
+
'json'
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
assert.deepEqual(request.headers, {
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
'x-captain-auth': 'token',
|
|
21
|
+
})
|
|
22
|
+
assert.equal(request.body, JSON.stringify(payload))
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('form data is passed through without setting a content type', () => {
|
|
26
|
+
const formData = { sourceFile: 'tar contents' }
|
|
27
|
+
const headers = { 'x-captain-auth': 'token' }
|
|
28
|
+
const request = createPostRequestInit(formData, headers, 'form-data')
|
|
29
|
+
|
|
30
|
+
assert.strictEqual(request.body, formData)
|
|
31
|
+
assert.deepEqual(request.headers, headers)
|
|
32
|
+
assert.equal(Object.hasOwn(request.headers, 'Content-Type'), false)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('PATCH requests use JSON serialization and content type', () => {
|
|
36
|
+
const payload = { appName: 'test-app', instanceCount: 2 }
|
|
37
|
+
const request = createPatchRequestInit(payload, {
|
|
38
|
+
'x-captain-auth': 'token',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
assert.equal(request.method, 'PATCH')
|
|
42
|
+
assert.deepEqual(request.headers, {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
'x-captain-auth': 'token',
|
|
45
|
+
})
|
|
46
|
+
assert.equal(request.body, JSON.stringify(payload))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('authentication retry preserves the form-data body type', async () => {
|
|
50
|
+
let loginRequests = 0
|
|
51
|
+
const bodyTypes = []
|
|
52
|
+
const client = new HttpClient(
|
|
53
|
+
'https://captain.example.com',
|
|
54
|
+
() => Promise.resolve('token'),
|
|
55
|
+
() => {
|
|
56
|
+
loginRequests++
|
|
57
|
+
return Promise.resolve()
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
client.fetchInternal = (_method, _endpoint, _variables, bodyType) => {
|
|
62
|
+
bodyTypes.push(bodyType)
|
|
63
|
+
if (bodyTypes.length === 1) {
|
|
64
|
+
return Promise.resolve({
|
|
65
|
+
status: ErrorFactory.STATUS_AUTH_TOKEN_INVALID,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Promise.resolve({ status: ErrorFactory.OKAY, data: {} })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await client.fetch(client.POST, '/upload', {}, client.FORM_DATA)()
|
|
73
|
+
|
|
74
|
+
assert.equal(loginRequests, 1)
|
|
75
|
+
assert.deepEqual(bodyTypes, ['form-data', 'form-data'])
|
|
76
|
+
})
|