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.
@@ -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
@@ -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
- uploadAppData(appName: string, file: File): Promise<void>;
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
  }
@@ -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?encoding=hex`, {}));
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() //
@@ -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: 'GET' | 'POST', endpoint: string, variables: any): () => Promise<any>;
12
- fetchInternal(method: 'GET' | 'POST', endpoint: string, variables: any): Promise<any>;
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
  }
@@ -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.19",
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": "echo \"Error: no test specified\" && exit 1",
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"
@@ -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
- http.GET,
279
- `/user/apps/appData/${appName}/logs?encoding=hex`,
280
- {}
281
- )
284
+ http.fetch(http.GET, `/user/apps/appData/${appName}/logs`, {
285
+ encoding,
286
+ })
282
287
  )
283
288
  }
284
289
 
285
- uploadAppData(appName: string, file: File): Promise<void> {
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}?detached=1`,
294
- formData
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> {
@@ -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: Record<string, any>,
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(url, {
27
- method: 'POST',
28
- headers: {
29
- 'Content-Type': 'application/json',
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(method: 'GET' | 'POST', endpoint: string, variables: any) {
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(method, endpoint, variables) //
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(method, endpoint, variables)
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(method: 'GET' | 'POST', endpoint: string, variables: any) {
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) return this.postReq(endpoint, variables)
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(endpoint: string, variables: any) {
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
+ })