caprover-api 0.0.20 → 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.
@@ -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, http.FORM_DATA));
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,18 +1,22 @@
1
1
  export type RequestBodyType = 'json' | 'form-data';
2
+ export type HttpMethod = 'GET' | 'POST' | 'PATCH';
2
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;
3
5
  export default class HttpClient {
4
6
  private baseUrl;
5
7
  private authTokenProvider;
6
8
  private onLoginRequested;
7
9
  readonly GET = "GET";
8
10
  readonly POST = "POST";
11
+ readonly PATCH = "PATCH";
9
12
  readonly FORM_DATA = "form-data";
10
13
  isDestroyed: boolean;
11
14
  constructor(baseUrl: string, authTokenProvider: () => Promise<string>, onLoginRequested: () => Promise<void>);
12
15
  createHeaders(): Promise<any>;
13
16
  destroy(): void;
14
- fetch(method: 'GET' | 'POST', endpoint: string, variables: any, bodyType?: RequestBodyType): () => Promise<any>;
15
- fetchInternal(method: 'GET' | 'POST', endpoint: string, variables: any, bodyType?: RequestBodyType): 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>;
16
19
  getReq(endpoint: string, variables: any): Promise<any>;
17
20
  postReq(endpoint: string, variables: any, bodyType?: RequestBodyType): Promise<any>;
21
+ patchReq(endpoint: string, variables: any): Promise<any>;
18
22
  }
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createPostRequestInit = createPostRequestInit;
7
+ exports.createPatchRequestInit = createPatchRequestInit;
7
8
  const ErrorFactory_1 = __importDefault(require("./ErrorFactory"));
8
9
  const cross_fetch_1 = __importDefault(require("cross-fetch"));
9
10
  function createPostRequestInit(variables, headers, bodyType) {
@@ -23,6 +24,16 @@ function createPostRequestInit(variables, headers, bodyType) {
23
24
  body: JSON.stringify(variables),
24
25
  };
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
+ }
26
37
  function buildQueryParams(params) {
27
38
  if (!params || Object.keys(params).length === 0)
28
39
  return '';
@@ -61,6 +72,14 @@ class CrossFetchEngine {
61
72
  }
62
73
  return res.json();
63
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
+ }
64
83
  }
65
84
  class HttpClient {
66
85
  constructor(baseUrl, authTokenProvider, onLoginRequested) {
@@ -69,6 +88,7 @@ class HttpClient {
69
88
  this.onLoginRequested = onLoginRequested;
70
89
  this.GET = 'GET';
71
90
  this.POST = 'POST';
91
+ this.PATCH = 'PATCH';
72
92
  this.FORM_DATA = 'form-data';
73
93
  this.isDestroyed = false;
74
94
  //
@@ -151,6 +171,8 @@ class HttpClient {
151
171
  return this.getReq(endpoint, variables);
152
172
  if (method === this.POST)
153
173
  return this.postReq(endpoint, variables, bodyType);
174
+ if (method === this.PATCH)
175
+ return this.patchReq(endpoint, variables);
154
176
  throw new Error(`Unknown method: ${method}`);
155
177
  }
156
178
  getReq(endpoint, variables) {
@@ -181,5 +203,18 @@ class HttpClient {
181
203
  return data;
182
204
  });
183
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
+ }
184
219
  }
185
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.20",
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",
@@ -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,7 +299,9 @@ export default class ApiManager {
290
299
  .then(
291
300
  http.fetch(
292
301
  http.POST,
293
- `/user/apps/appData/${appName}?detached=1`,
302
+ `/user/apps/appData/${appName}${
303
+ detached ? '?detached=1' : ''
304
+ }`,
294
305
  formData,
295
306
  http.FORM_DATA
296
307
  )
@@ -399,6 +410,21 @@ export default class ApiManager {
399
410
  )
400
411
  }
401
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
+
402
428
  renameApp(oldAppName: string, newAppName: string): Promise<void> {
403
429
  const http = this.http
404
430
 
@@ -895,7 +921,7 @@ export default class ApiManager {
895
921
  }
896
922
 
897
923
  executeGenericApiCommand(
898
- verb: 'GET' | 'POST',
924
+ verb: 'GET' | 'POST' | 'PATCH',
899
925
  endpoint: string,
900
926
  data: any
901
927
  ): Promise<any> {
@@ -2,6 +2,7 @@ import ErrorFactory from './ErrorFactory'
2
2
  import fetch from 'cross-fetch'
3
3
 
4
4
  export type RequestBodyType = 'json' | 'form-data'
5
+ export type HttpMethod = 'GET' | 'POST' | 'PATCH'
5
6
 
6
7
  export function createPostRequestInit(
7
8
  variables: any,
@@ -26,6 +27,20 @@ export function createPostRequestInit(
26
27
  }
27
28
  }
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
+
29
44
  function buildQueryParams(params: Record<string, any>): string {
30
45
  if (!params || Object.keys(params).length === 0) return ''
31
46
  return (
@@ -86,11 +101,26 @@ class CrossFetchEngine {
86
101
  }
87
102
  return res.json()
88
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
+ }
89
118
  }
90
119
 
91
120
  export default class HttpClient {
92
121
  public readonly GET = 'GET'
93
122
  public readonly POST = 'POST'
123
+ public readonly PATCH = 'PATCH'
94
124
  public readonly FORM_DATA = 'form-data'
95
125
  public isDestroyed = false
96
126
 
@@ -123,7 +153,7 @@ export default class HttpClient {
123
153
  }
124
154
 
125
155
  fetch(
126
- method: 'GET' | 'POST',
156
+ method: HttpMethod,
127
157
  endpoint: string,
128
158
  variables: any,
129
159
  bodyType: RequestBodyType = 'json'
@@ -198,7 +228,7 @@ export default class HttpClient {
198
228
  }
199
229
 
200
230
  fetchInternal(
201
- method: 'GET' | 'POST',
231
+ method: HttpMethod,
202
232
  endpoint: string,
203
233
  variables: any,
204
234
  bodyType: RequestBodyType = 'json'
@@ -208,6 +238,8 @@ export default class HttpClient {
208
238
  if (method === this.POST)
209
239
  return this.postReq(endpoint, variables, bodyType)
210
240
 
241
+ if (method === this.PATCH) return this.patchReq(endpoint, variables)
242
+
211
243
  throw new Error(`Unknown method: ${method}`)
212
244
  }
213
245
 
@@ -253,4 +285,22 @@ export default class HttpClient {
253
285
  return data
254
286
  })
255
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
+ }
256
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
+ })
@@ -5,6 +5,7 @@ const ErrorFactory = require('../dist/api/ErrorFactory').default
5
5
  const HttpClientModule = require('../dist/api/HttpClient')
6
6
  const HttpClient = HttpClientModule.default
7
7
  const { createPostRequestInit } = HttpClientModule
8
+ const { createPatchRequestInit } = HttpClientModule
8
9
 
9
10
  test('JSON requests preserve the existing serialization and content type', () => {
10
11
  const payload = { appName: 'test-app' }
@@ -31,6 +32,20 @@ test('form data is passed through without setting a content type', () => {
31
32
  assert.equal(Object.hasOwn(request.headers, 'Content-Type'), false)
32
33
  })
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
+
34
49
  test('authentication retry preserves the form-data body type', async () => {
35
50
  let loginRequests = 0
36
51
  const bodyTypes = []