caprover-api 0.0.18 → 0.0.20

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
@@ -124,7 +124,7 @@ export default class ApiManager {
124
124
  addNewCustomOneClickRepo(repositoryUrl: string): Promise<void>;
125
125
  deleteCustomOneClickRepo(repositoryUrl: string): Promise<void>;
126
126
  addDockerNode(nodeType: string, privateKey: string, remoteNodeIpAddress: string, sshPort: string, sshUser: string, captainIpAddress: string): Promise<void>;
127
- startOneClickAppDeploy(template: any, values?: any): Promise<{
127
+ startOneClickAppDeploy(template: any, values?: any, templateName?: string): Promise<{
128
128
  jobId: string;
129
129
  }>;
130
130
  getOneClickAppDeployProgress(jobId: string): Promise<OneClickAppDeploymentState>;
@@ -172,7 +172,7 @@ class ApiManager {
172
172
  let formData = new FormData();
173
173
  formData.append('sourceFile', file);
174
174
  return Promise.resolve() //
175
- .then(http.fetch(http.POST, `/user/apps/appData/${appName}?detached=1`, formData));
175
+ .then(http.fetch(http.POST, `/user/apps/appData/${appName}?detached=1`, formData, http.FORM_DATA));
176
176
  }
177
177
  registerProject(selectedProject) {
178
178
  const http = this.http;
@@ -507,12 +507,13 @@ class ApiManager {
507
507
  captainIpAddress,
508
508
  }));
509
509
  }
510
- startOneClickAppDeploy(template, values) {
510
+ startOneClickAppDeploy(template, values, templateName) {
511
511
  const http = this.http;
512
512
  return Promise.resolve() //
513
513
  .then(http.fetch(http.POST, '/user/oneclick/deploy', {
514
514
  template,
515
515
  values,
516
+ templateName,
516
517
  }));
517
518
  }
518
519
  getOneClickAppDeployProgress(jobId) {
@@ -1,15 +1,18 @@
1
+ export type RequestBodyType = 'json' | 'form-data';
2
+ export declare function createPostRequestInit(variables: any, headers: Record<string, string>, bodyType: RequestBodyType): RequestInit;
1
3
  export default class HttpClient {
2
4
  private baseUrl;
3
5
  private authTokenProvider;
4
6
  private onLoginRequested;
5
7
  readonly GET = "GET";
6
8
  readonly POST = "POST";
9
+ readonly FORM_DATA = "form-data";
7
10
  isDestroyed: boolean;
8
11
  constructor(baseUrl: string, authTokenProvider: () => Promise<string>, onLoginRequested: () => Promise<void>);
9
12
  createHeaders(): Promise<any>;
10
13
  destroy(): void;
11
- fetch(method: 'GET' | 'POST', endpoint: string, variables: any): () => Promise<any>;
12
- fetchInternal(method: 'GET' | 'POST', endpoint: string, variables: any): Promise<any>;
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>;
13
16
  getReq(endpoint: string, variables: any): Promise<any>;
14
- postReq(endpoint: string, variables: any): Promise<any>;
17
+ postReq(endpoint: string, variables: any, bodyType?: RequestBodyType): Promise<any>;
15
18
  }
@@ -3,8 +3,26 @@ 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;
6
7
  const ErrorFactory_1 = __importDefault(require("./ErrorFactory"));
7
8
  const cross_fetch_1 = __importDefault(require("cross-fetch"));
9
+ function createPostRequestInit(variables, headers, bodyType) {
10
+ if (bodyType === 'form-data') {
11
+ return {
12
+ method: 'POST',
13
+ headers,
14
+ body: variables,
15
+ };
16
+ }
17
+ return {
18
+ method: 'POST',
19
+ headers: {
20
+ 'Content-Type': 'application/json',
21
+ ...headers,
22
+ },
23
+ body: JSON.stringify(variables),
24
+ };
25
+ }
8
26
  function buildQueryParams(params) {
9
27
  if (!params || Object.keys(params).length === 0)
10
28
  return '';
@@ -17,15 +35,8 @@ let TOKEN_HEADER = 'x-captain-auth';
17
35
  let NAMESPACE = 'x-namespace';
18
36
  let CAPTAIN = 'captain';
19
37
  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
- });
38
+ static async post(url, variables, headers, bodyType) {
39
+ const res = await (0, cross_fetch_1.default)(url, createPostRequestInit(variables, headers, bodyType));
29
40
  if (!res.ok) {
30
41
  const errBody = await res.text();
31
42
  throw new Error(`HTTP ${res.status}: ${errBody}`);
@@ -58,6 +69,7 @@ class HttpClient {
58
69
  this.onLoginRequested = onLoginRequested;
59
70
  this.GET = 'GET';
60
71
  this.POST = 'POST';
72
+ this.FORM_DATA = 'form-data';
61
73
  this.isDestroyed = false;
62
74
  //
63
75
  }
@@ -78,12 +90,12 @@ class HttpClient {
78
90
  destroy() {
79
91
  this.isDestroyed = true;
80
92
  }
81
- fetch(method, endpoint, variables) {
93
+ fetch(method, endpoint, variables, bodyType = 'json') {
82
94
  const self = this;
83
95
  return function () {
84
96
  return Promise.resolve() //
85
97
  .then(function () {
86
- return self.fetchInternal(method, endpoint, variables); //
98
+ return self.fetchInternal(method, endpoint, variables, bodyType); //
87
99
  })
88
100
  .then(function (fetchResponse) {
89
101
  if (
@@ -94,7 +106,7 @@ class HttpClient {
94
106
  .onLoginRequested() //
95
107
  .then(function () {
96
108
  return self
97
- .fetchInternal(method, endpoint, variables)
109
+ .fetchInternal(method, endpoint, variables, bodyType)
98
110
  .then(function (httpResponse) {
99
111
  return httpResponse;
100
112
  });
@@ -134,11 +146,11 @@ class HttpClient {
134
146
  });
135
147
  };
136
148
  }
137
- fetchInternal(method, endpoint, variables) {
149
+ fetchInternal(method, endpoint, variables, bodyType = 'json') {
138
150
  if (method === this.GET)
139
151
  return this.getReq(endpoint, variables);
140
152
  if (method === this.POST)
141
- return this.postReq(endpoint, variables);
153
+ return this.postReq(endpoint, variables, bodyType);
142
154
  throw new Error(`Unknown method: ${method}`);
143
155
  }
144
156
  getReq(endpoint, variables) {
@@ -155,14 +167,14 @@ class HttpClient {
155
167
  return data;
156
168
  });
157
169
  }
158
- postReq(endpoint, variables) {
170
+ postReq(endpoint, variables, bodyType = 'json') {
159
171
  const self = this;
160
172
  return Promise.resolve() //
161
173
  .then(function () {
162
174
  return self.createHeaders();
163
175
  })
164
176
  .then(function (headers) {
165
- return CrossFetchEngine.post(self.baseUrl + endpoint, variables, headers);
177
+ return CrossFetchEngine.post(self.baseUrl + endpoint, variables, headers, bodyType);
166
178
  })
167
179
  .then(function (data) {
168
180
  // console.log(data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caprover-api",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
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"
@@ -291,7 +291,8 @@ export default class ApiManager {
291
291
  http.fetch(
292
292
  http.POST,
293
293
  `/user/apps/appData/${appName}?detached=1`,
294
- formData
294
+ formData,
295
+ http.FORM_DATA
295
296
  )
296
297
  )
297
298
  }
@@ -865,7 +866,8 @@ export default class ApiManager {
865
866
 
866
867
  startOneClickAppDeploy(
867
868
  template: any,
868
- values?: any
869
+ values?: any,
870
+ templateName?: string
869
871
  ): Promise<{ jobId: string }> {
870
872
  const http = this.http
871
873
 
@@ -874,6 +876,7 @@ export default class ApiManager {
874
876
  http.fetch(http.POST, '/user/oneclick/deploy', {
875
877
  template,
876
878
  values,
879
+ templateName,
877
880
  })
878
881
  )
879
882
  }
@@ -1,6 +1,31 @@
1
1
  import ErrorFactory from './ErrorFactory'
2
2
  import fetch from 'cross-fetch'
3
3
 
4
+ export type RequestBodyType = 'json' | 'form-data'
5
+
6
+ export function createPostRequestInit(
7
+ variables: any,
8
+ headers: Record<string, string>,
9
+ bodyType: RequestBodyType
10
+ ): RequestInit {
11
+ if (bodyType === 'form-data') {
12
+ return {
13
+ method: 'POST',
14
+ headers,
15
+ body: variables,
16
+ }
17
+ }
18
+
19
+ return {
20
+ method: 'POST',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ ...headers,
24
+ },
25
+ body: JSON.stringify(variables),
26
+ }
27
+ }
28
+
4
29
  function buildQueryParams(params: Record<string, any>): string {
5
30
  if (!params || Object.keys(params).length === 0) return ''
6
31
  return (
@@ -20,17 +45,14 @@ let CAPTAIN = 'captain'
20
45
  class CrossFetchEngine {
21
46
  public static async post(
22
47
  url: string,
23
- variables: Record<string, any>,
24
- headers: Record<string, string>
48
+ variables: any,
49
+ headers: Record<string, string>,
50
+ bodyType: RequestBodyType
25
51
  ): 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
- })
52
+ const res = await fetch(
53
+ url,
54
+ createPostRequestInit(variables, headers, bodyType)
55
+ )
34
56
 
35
57
  if (!res.ok) {
36
58
  const errBody = await res.text()
@@ -69,6 +91,7 @@ class CrossFetchEngine {
69
91
  export default class HttpClient {
70
92
  public readonly GET = 'GET'
71
93
  public readonly POST = 'POST'
94
+ public readonly FORM_DATA = 'form-data'
72
95
  public isDestroyed = false
73
96
 
74
97
  constructor(
@@ -99,12 +122,22 @@ export default class HttpClient {
99
122
  this.isDestroyed = true
100
123
  }
101
124
 
102
- fetch(method: 'GET' | 'POST', endpoint: string, variables: any) {
125
+ fetch(
126
+ method: 'GET' | 'POST',
127
+ endpoint: string,
128
+ variables: any,
129
+ bodyType: RequestBodyType = 'json'
130
+ ) {
103
131
  const self = this
104
132
  return function (): Promise<any> {
105
133
  return Promise.resolve() //
106
134
  .then(function () {
107
- return self.fetchInternal(method, endpoint, variables) //
135
+ return self.fetchInternal(
136
+ method,
137
+ endpoint,
138
+ variables,
139
+ bodyType
140
+ ) //
108
141
  })
109
142
  .then(function (fetchResponse) {
110
143
  if (
@@ -116,7 +149,12 @@ export default class HttpClient {
116
149
  .onLoginRequested() //
117
150
  .then(function () {
118
151
  return self
119
- .fetchInternal(method, endpoint, variables)
152
+ .fetchInternal(
153
+ method,
154
+ endpoint,
155
+ variables,
156
+ bodyType
157
+ )
120
158
  .then(function (httpResponse) {
121
159
  return httpResponse
122
160
  })
@@ -159,10 +197,16 @@ export default class HttpClient {
159
197
  }
160
198
  }
161
199
 
162
- fetchInternal(method: 'GET' | 'POST', endpoint: string, variables: any) {
200
+ fetchInternal(
201
+ method: 'GET' | 'POST',
202
+ endpoint: string,
203
+ variables: any,
204
+ bodyType: RequestBodyType = 'json'
205
+ ) {
163
206
  if (method === this.GET) return this.getReq(endpoint, variables)
164
207
 
165
- if (method === this.POST) return this.postReq(endpoint, variables)
208
+ if (method === this.POST)
209
+ return this.postReq(endpoint, variables, bodyType)
166
210
 
167
211
  throw new Error(`Unknown method: ${method}`)
168
212
  }
@@ -186,7 +230,11 @@ export default class HttpClient {
186
230
  })
187
231
  }
188
232
 
189
- postReq(endpoint: string, variables: any) {
233
+ postReq(
234
+ endpoint: string,
235
+ variables: any,
236
+ bodyType: RequestBodyType = 'json'
237
+ ) {
190
238
  const self = this
191
239
  return Promise.resolve() //
192
240
  .then(function () {
@@ -196,7 +244,8 @@ export default class HttpClient {
196
244
  return CrossFetchEngine.post(
197
245
  self.baseUrl + endpoint,
198
246
  variables,
199
- headers
247
+ headers,
248
+ bodyType
200
249
  )
201
250
  })
202
251
  .then(function (data) {
@@ -0,0 +1,61 @@
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
+
9
+ test('JSON requests preserve the existing serialization and content type', () => {
10
+ const payload = { appName: 'test-app' }
11
+ const request = createPostRequestInit(
12
+ payload,
13
+ { 'x-captain-auth': 'token' },
14
+ 'json'
15
+ )
16
+
17
+ assert.deepEqual(request.headers, {
18
+ 'Content-Type': 'application/json',
19
+ 'x-captain-auth': 'token',
20
+ })
21
+ assert.equal(request.body, JSON.stringify(payload))
22
+ })
23
+
24
+ test('form data is passed through without setting a content type', () => {
25
+ const formData = { sourceFile: 'tar contents' }
26
+ const headers = { 'x-captain-auth': 'token' }
27
+ const request = createPostRequestInit(formData, headers, 'form-data')
28
+
29
+ assert.strictEqual(request.body, formData)
30
+ assert.deepEqual(request.headers, headers)
31
+ assert.equal(Object.hasOwn(request.headers, 'Content-Type'), false)
32
+ })
33
+
34
+ test('authentication retry preserves the form-data body type', async () => {
35
+ let loginRequests = 0
36
+ const bodyTypes = []
37
+ const client = new HttpClient(
38
+ 'https://captain.example.com',
39
+ () => Promise.resolve('token'),
40
+ () => {
41
+ loginRequests++
42
+ return Promise.resolve()
43
+ }
44
+ )
45
+
46
+ client.fetchInternal = (_method, _endpoint, _variables, bodyType) => {
47
+ bodyTypes.push(bodyType)
48
+ if (bodyTypes.length === 1) {
49
+ return Promise.resolve({
50
+ status: ErrorFactory.STATUS_AUTH_TOKEN_INVALID,
51
+ })
52
+ }
53
+
54
+ return Promise.resolve({ status: ErrorFactory.OKAY, data: {} })
55
+ }
56
+
57
+ await client.fetch(client.POST, '/upload', {}, client.FORM_DATA)()
58
+
59
+ assert.equal(loginRequests, 1)
60
+ assert.deepEqual(bodyTypes, ['form-data', 'form-data'])
61
+ })