mongodb-mcp-server 0.1.0 → 0.1.2

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.
Files changed (137) hide show
  1. package/.dockerignore +11 -0
  2. package/.github/CODEOWNERS +0 -2
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +8 -0
  4. package/.github/workflows/check-pr-title.yml +29 -0
  5. package/.github/workflows/{lint.yml → check.yml} +22 -1
  6. package/.github/workflows/code_health.yaml +0 -22
  7. package/.github/workflows/code_health_fork.yaml +7 -63
  8. package/.github/workflows/docker.yaml +57 -0
  9. package/.github/workflows/stale.yml +32 -0
  10. package/.smithery/Dockerfile +30 -0
  11. package/.smithery/smithery.yaml +63 -0
  12. package/.vscode/extensions.json +9 -0
  13. package/.vscode/settings.json +11 -0
  14. package/CONTRIBUTING.md +1 -1
  15. package/Dockerfile +10 -0
  16. package/README.md +173 -35
  17. package/dist/common/atlas/apiClient.js +151 -35
  18. package/dist/common/atlas/apiClient.js.map +1 -1
  19. package/dist/common/atlas/apiClientError.js +38 -5
  20. package/dist/common/atlas/apiClientError.js.map +1 -1
  21. package/dist/common/atlas/cluster.js +66 -0
  22. package/dist/common/atlas/cluster.js.map +1 -0
  23. package/dist/common/atlas/generatePassword.js +9 -0
  24. package/dist/common/atlas/generatePassword.js.map +1 -0
  25. package/dist/helpers/EJsonTransport.js +38 -0
  26. package/dist/helpers/EJsonTransport.js.map +1 -0
  27. package/dist/helpers/connectionOptions.js +10 -0
  28. package/dist/helpers/connectionOptions.js.map +1 -0
  29. package/dist/{packageInfo.js → helpers/packageInfo.js} +1 -1
  30. package/dist/helpers/packageInfo.js.map +1 -0
  31. package/dist/index.js +23 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/logger.js +7 -0
  34. package/dist/logger.js.map +1 -1
  35. package/dist/server.js +16 -12
  36. package/dist/server.js.map +1 -1
  37. package/dist/session.js +8 -3
  38. package/dist/session.js.map +1 -1
  39. package/dist/telemetry/constants.js +1 -3
  40. package/dist/telemetry/constants.js.map +1 -1
  41. package/dist/telemetry/eventCache.js.map +1 -1
  42. package/dist/telemetry/telemetry.js +126 -47
  43. package/dist/telemetry/telemetry.js.map +1 -1
  44. package/dist/tools/atlas/atlasTool.js +38 -0
  45. package/dist/tools/atlas/atlasTool.js.map +1 -1
  46. package/dist/tools/atlas/create/createDBUser.js +19 -2
  47. package/dist/tools/atlas/create/createDBUser.js.map +1 -1
  48. package/dist/tools/atlas/create/createProject.js +5 -1
  49. package/dist/tools/atlas/create/createProject.js.map +1 -1
  50. package/dist/tools/atlas/metadata/connectCluster.js +5 -22
  51. package/dist/tools/atlas/metadata/connectCluster.js.map +1 -1
  52. package/dist/tools/atlas/read/inspectCluster.js +4 -24
  53. package/dist/tools/atlas/read/inspectCluster.js.map +1 -1
  54. package/dist/tools/atlas/read/listAlerts.js +41 -0
  55. package/dist/tools/atlas/read/listAlerts.js.map +1 -0
  56. package/dist/tools/atlas/read/listClusters.js +9 -18
  57. package/dist/tools/atlas/read/listClusters.js.map +1 -1
  58. package/dist/tools/atlas/read/listProjects.js +3 -1
  59. package/dist/tools/atlas/read/listProjects.js.map +1 -1
  60. package/dist/tools/atlas/tools.js +2 -0
  61. package/dist/tools/atlas/tools.js.map +1 -1
  62. package/dist/tools/mongodb/metadata/listDatabases.js.map +1 -1
  63. package/dist/tools/mongodb/read/count.js +2 -2
  64. package/dist/tools/mongodb/read/count.js.map +1 -1
  65. package/dist/tools/mongodb/tools.js +2 -4
  66. package/dist/tools/mongodb/tools.js.map +1 -1
  67. package/dist/tools/tool.js +38 -6
  68. package/dist/tools/tool.js.map +1 -1
  69. package/eslint.config.js +2 -1
  70. package/{jest.config.ts → jest.config.cjs} +1 -1
  71. package/package.json +11 -9
  72. package/scripts/apply.ts +8 -5
  73. package/scripts/filter.ts +5 -0
  74. package/src/common/atlas/apiClient.ts +190 -38
  75. package/src/common/atlas/apiClientError.ts +58 -7
  76. package/src/common/atlas/cluster.ts +94 -0
  77. package/src/common/atlas/generatePassword.ts +10 -0
  78. package/src/common/atlas/openapi.d.ts +1876 -239
  79. package/src/helpers/EJsonTransport.ts +47 -0
  80. package/src/helpers/connectionOptions.ts +20 -0
  81. package/src/{packageInfo.ts → helpers/packageInfo.ts} +1 -1
  82. package/src/index.ts +27 -3
  83. package/src/logger.ts +8 -0
  84. package/src/server.ts +23 -15
  85. package/src/session.ts +8 -4
  86. package/src/telemetry/constants.ts +2 -3
  87. package/src/telemetry/eventCache.ts +1 -1
  88. package/src/telemetry/telemetry.ts +182 -64
  89. package/src/telemetry/types.ts +1 -1
  90. package/src/tools/atlas/atlasTool.ts +47 -1
  91. package/src/tools/atlas/create/createDBUser.ts +22 -2
  92. package/src/tools/atlas/create/createProject.ts +7 -1
  93. package/src/tools/atlas/metadata/connectCluster.ts +5 -27
  94. package/src/tools/atlas/read/inspectCluster.ts +4 -40
  95. package/src/tools/atlas/read/listAlerts.ts +45 -0
  96. package/src/tools/atlas/read/listClusters.ts +19 -36
  97. package/src/tools/atlas/read/listProjects.ts +4 -2
  98. package/src/tools/atlas/tools.ts +2 -0
  99. package/src/tools/mongodb/metadata/listDatabases.ts +0 -1
  100. package/src/tools/mongodb/read/count.ts +3 -2
  101. package/src/tools/mongodb/tools.ts +2 -4
  102. package/src/tools/tool.ts +45 -8
  103. package/src/types/mongodb-connection-string-url.d.ts +69 -0
  104. package/tests/integration/helpers.ts +41 -2
  105. package/tests/integration/tools/atlas/accessLists.test.ts +2 -2
  106. package/tests/integration/tools/atlas/alerts.test.ts +42 -0
  107. package/tests/integration/tools/atlas/atlasHelpers.ts +5 -3
  108. package/tests/integration/tools/atlas/clusters.test.ts +4 -4
  109. package/tests/integration/tools/atlas/dbUsers.test.ts +58 -33
  110. package/tests/integration/tools/atlas/orgs.test.ts +2 -2
  111. package/tests/integration/tools/atlas/projects.test.ts +3 -3
  112. package/tests/integration/tools/mongodb/create/createCollection.test.ts +2 -2
  113. package/tests/integration/tools/mongodb/create/createIndex.test.ts +2 -2
  114. package/tests/integration/tools/mongodb/create/insertMany.test.ts +1 -1
  115. package/tests/integration/tools/mongodb/delete/dropCollection.test.ts +1 -1
  116. package/tests/integration/tools/mongodb/metadata/collectionSchema.test.ts +2 -2
  117. package/tests/integration/tools/mongodb/metadata/connect.test.ts +2 -6
  118. package/tests/integration/tools/mongodb/metadata/dbStats.test.ts +4 -4
  119. package/tests/integration/tools/mongodb/metadata/explain.test.ts +10 -10
  120. package/tests/integration/tools/mongodb/metadata/listCollections.test.ts +1 -1
  121. package/tests/integration/tools/mongodb/metadata/listDatabases.test.ts +9 -5
  122. package/tests/integration/tools/mongodb/metadata/logs.test.ts +4 -4
  123. package/tests/integration/tools/mongodb/mongodbHelpers.ts +15 -24
  124. package/tests/integration/tools/mongodb/read/aggregate.test.ts +22 -7
  125. package/tests/integration/tools/mongodb/read/collectionIndexes.test.ts +5 -5
  126. package/tests/integration/tools/mongodb/read/count.test.ts +15 -10
  127. package/tests/integration/tools/mongodb/read/find.test.ts +32 -4
  128. package/tests/integration/tools/mongodb/update/renameCollection.test.ts +4 -4
  129. package/tests/unit/EJsonTransport.test.ts +71 -0
  130. package/tests/unit/apiClient.test.ts +193 -0
  131. package/tests/unit/session.test.ts +65 -0
  132. package/tests/unit/telemetry.test.ts +222 -80
  133. package/tsconfig.build.json +2 -1
  134. package/dist/packageInfo.js.map +0 -1
  135. package/dist/telemetry/device-id.js +0 -20
  136. package/dist/telemetry/device-id.js.map +0 -1
  137. package/src/telemetry/device-id.ts +0 -21
@@ -4,7 +4,7 @@ import { AccessToken, ClientCredentials } from "simple-oauth2";
4
4
  import { ApiClientError } from "./apiClientError.js";
5
5
  import { paths, operations } from "./openapi.js";
6
6
  import { CommonProperties, TelemetryEvent } from "../../telemetry/types.js";
7
- import { packageInfo } from "../../packageInfo.js";
7
+ import { packageInfo } from "../../helpers/packageInfo.js";
8
8
 
9
9
  const ATLAS_API_VERSION = "2025-03-12";
10
10
 
@@ -34,7 +34,9 @@ export class ApiClient {
34
34
 
35
35
  private getAccessToken = async () => {
36
36
  if (this.oauth2Client && (!this.accessToken || this.accessToken.expired())) {
37
- this.accessToken = await this.oauth2Client.getToken({});
37
+ this.accessToken = await this.oauth2Client.getToken({
38
+ agent: this.options.userAgent,
39
+ });
38
40
  }
39
41
  return this.accessToken?.token.access_token as string | undefined;
40
42
  };
@@ -55,14 +57,6 @@ export class ApiClient {
55
57
  },
56
58
  };
57
59
 
58
- private readonly errorMiddleware: Middleware = {
59
- async onResponse({ response }) {
60
- if (!response.ok) {
61
- throw await ApiClientError.fromResponse(response);
62
- }
63
- },
64
- };
65
-
66
60
  constructor(options: ApiClientOptions) {
67
61
  this.options = {
68
62
  ...options,
@@ -91,13 +85,16 @@ export class ApiClient {
91
85
  });
92
86
  this.client.use(this.authMiddleware);
93
87
  }
94
- this.client.use(this.errorMiddleware);
95
88
  }
96
89
 
97
90
  public hasCredentials(): boolean {
98
91
  return !!(this.oauth2Client && this.accessToken);
99
92
  }
100
93
 
94
+ public async validateAccessToken(): Promise<void> {
95
+ await this.getAccessToken();
96
+ }
97
+
101
98
  public async getIpInfo(): Promise<{
102
99
  currentIpv4Address: string;
103
100
  }> {
@@ -123,22 +120,59 @@ export class ApiClient {
123
120
  }>;
124
121
  }
125
122
 
126
- async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
127
- let endpoint = "api/private/unauth/telemetry/events";
123
+ public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
124
+ if (!this.options.credentials) {
125
+ await this.sendUnauthEvents(events);
126
+ return;
127
+ }
128
+
129
+ try {
130
+ await this.sendAuthEvents(events);
131
+ } catch (error) {
132
+ if (error instanceof ApiClientError) {
133
+ if (error.response.status !== 401) {
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ // send unauth events if any of the following are true:
139
+ // 1: the token is not valid (not ApiClientError)
140
+ // 2: if the api responded with 401 (ApiClientError with status 401)
141
+ await this.sendUnauthEvents(events);
142
+ }
143
+ }
144
+
145
+ private async sendAuthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
146
+ const accessToken = await this.getAccessToken();
147
+ if (!accessToken) {
148
+ throw new Error("No access token available");
149
+ }
150
+ const authUrl = new URL("api/private/v1.0/telemetry/events", this.options.baseUrl);
151
+ const response = await fetch(authUrl, {
152
+ method: "POST",
153
+ headers: {
154
+ Accept: "application/json",
155
+ "Content-Type": "application/json",
156
+ "User-Agent": this.options.userAgent,
157
+ Authorization: `Bearer ${accessToken}`,
158
+ },
159
+ body: JSON.stringify(events),
160
+ });
161
+
162
+ if (!response.ok) {
163
+ throw await ApiClientError.fromResponse(response);
164
+ }
165
+ }
166
+
167
+ private async sendUnauthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
128
168
  const headers: Record<string, string> = {
129
169
  Accept: "application/json",
130
170
  "Content-Type": "application/json",
131
171
  "User-Agent": this.options.userAgent,
132
172
  };
133
173
 
134
- const accessToken = await this.getAccessToken();
135
- if (accessToken) {
136
- endpoint = "api/private/v1.0/telemetry/events";
137
- headers["Authorization"] = `Bearer ${accessToken}`;
138
- }
139
-
140
- const url = new URL(endpoint, this.options.baseUrl);
141
- const response = await fetch(url, {
174
+ const unauthUrl = new URL("api/private/unauth/telemetry/events", this.options.baseUrl);
175
+ const response = await fetch(unauthUrl, {
142
176
  method: "POST",
143
177
  headers,
144
178
  body: JSON.stringify(events),
@@ -151,83 +185,201 @@ export class ApiClient {
151
185
 
152
186
  // DO NOT EDIT. This is auto-generated code.
153
187
  async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
154
- const { data } = await this.client.GET("/api/atlas/v2/clusters", options);
188
+ const { data, error, response } = await this.client.GET("/api/atlas/v2/clusters", options);
189
+ if (error) {
190
+ throw ApiClientError.fromError(response, error);
191
+ }
155
192
  return data;
156
193
  }
157
194
 
158
195
  async listProjects(options?: FetchOptions<operations["listProjects"]>) {
159
- const { data } = await this.client.GET("/api/atlas/v2/groups", options);
196
+ const { data, error, response } = await this.client.GET("/api/atlas/v2/groups", options);
197
+ if (error) {
198
+ throw ApiClientError.fromError(response, error);
199
+ }
160
200
  return data;
161
201
  }
162
202
 
163
203
  async createProject(options: FetchOptions<operations["createProject"]>) {
164
- const { data } = await this.client.POST("/api/atlas/v2/groups", options);
204
+ const { data, error, response } = await this.client.POST("/api/atlas/v2/groups", options);
205
+ if (error) {
206
+ throw ApiClientError.fromError(response, error);
207
+ }
165
208
  return data;
166
209
  }
167
210
 
168
211
  async deleteProject(options: FetchOptions<operations["deleteProject"]>) {
169
- await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
212
+ const { error, response } = await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
213
+ if (error) {
214
+ throw ApiClientError.fromError(response, error);
215
+ }
170
216
  }
171
217
 
172
218
  async getProject(options: FetchOptions<operations["getProject"]>) {
173
- const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
219
+ const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
220
+ if (error) {
221
+ throw ApiClientError.fromError(response, error);
222
+ }
174
223
  return data;
175
224
  }
176
225
 
177
226
  async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
178
- const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
227
+ const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
228
+ if (error) {
229
+ throw ApiClientError.fromError(response, error);
230
+ }
179
231
  return data;
180
232
  }
181
233
 
182
234
  async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
183
- const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
235
+ const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
236
+ if (error) {
237
+ throw ApiClientError.fromError(response, error);
238
+ }
184
239
  return data;
185
240
  }
186
241
 
187
242
  async deleteProjectIpAccessList(options: FetchOptions<operations["deleteProjectIpAccessList"]>) {
188
- await this.client.DELETE("/api/atlas/v2/groups/{groupId}/accessList/{entryValue}", options);
243
+ const { error, response } = await this.client.DELETE(
244
+ "/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
245
+ options
246
+ );
247
+ if (error) {
248
+ throw ApiClientError.fromError(response, error);
249
+ }
250
+ }
251
+
252
+ async listAlerts(options: FetchOptions<operations["listAlerts"]>) {
253
+ const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/alerts", options);
254
+ if (error) {
255
+ throw ApiClientError.fromError(response, error);
256
+ }
257
+ return data;
189
258
  }
190
259
 
191
260
  async listClusters(options: FetchOptions<operations["listClusters"]>) {
192
- const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
261
+ const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
262
+ if (error) {
263
+ throw ApiClientError.fromError(response, error);
264
+ }
193
265
  return data;
194
266
  }
195
267
 
196
268
  async createCluster(options: FetchOptions<operations["createCluster"]>) {
197
- const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
269
+ const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
270
+ if (error) {
271
+ throw ApiClientError.fromError(response, error);
272
+ }
198
273
  return data;
199
274
  }
200
275
 
201
276
  async deleteCluster(options: FetchOptions<operations["deleteCluster"]>) {
202
- await this.client.DELETE("/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", options);
277
+ const { error, response } = await this.client.DELETE(
278
+ "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
279
+ options
280
+ );
281
+ if (error) {
282
+ throw ApiClientError.fromError(response, error);
283
+ }
203
284
  }
204
285
 
205
286
  async getCluster(options: FetchOptions<operations["getCluster"]>) {
206
- const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", options);
287
+ const { data, error, response } = await this.client.GET(
288
+ "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
289
+ options
290
+ );
291
+
292
+ if (error) {
293
+ throw ApiClientError.fromError(response, error);
294
+ }
207
295
  return data;
208
296
  }
209
297
 
210
298
  async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
211
- const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
299
+ const { data, error, response } = await this.client.GET(
300
+ "/api/atlas/v2/groups/{groupId}/databaseUsers",
301
+ options
302
+ );
303
+ if (error) {
304
+ throw ApiClientError.fromError(response, error);
305
+ }
212
306
  return data;
213
307
  }
214
308
 
215
309
  async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
216
- const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
310
+ const { data, error, response } = await this.client.POST(
311
+ "/api/atlas/v2/groups/{groupId}/databaseUsers",
312
+ options
313
+ );
314
+ if (error) {
315
+ throw ApiClientError.fromError(response, error);
316
+ }
217
317
  return data;
218
318
  }
219
319
 
220
320
  async deleteDatabaseUser(options: FetchOptions<operations["deleteDatabaseUser"]>) {
221
- await this.client.DELETE("/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}", options);
321
+ const { error, response } = await this.client.DELETE(
322
+ "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
323
+ options
324
+ );
325
+ if (error) {
326
+ throw ApiClientError.fromError(response, error);
327
+ }
328
+ }
329
+
330
+ async listFlexClusters(options: FetchOptions<operations["listFlexClusters"]>) {
331
+ const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/flexClusters", options);
332
+ if (error) {
333
+ throw ApiClientError.fromError(response, error);
334
+ }
335
+ return data;
336
+ }
337
+
338
+ async createFlexCluster(options: FetchOptions<operations["createFlexCluster"]>) {
339
+ const { data, error, response } = await this.client.POST(
340
+ "/api/atlas/v2/groups/{groupId}/flexClusters",
341
+ options
342
+ );
343
+ if (error) {
344
+ throw ApiClientError.fromError(response, error);
345
+ }
346
+ return data;
347
+ }
348
+
349
+ async deleteFlexCluster(options: FetchOptions<operations["deleteFlexCluster"]>) {
350
+ const { error, response } = await this.client.DELETE(
351
+ "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
352
+ options
353
+ );
354
+ if (error) {
355
+ throw ApiClientError.fromError(response, error);
356
+ }
357
+ }
358
+
359
+ async getFlexCluster(options: FetchOptions<operations["getFlexCluster"]>) {
360
+ const { data, error, response } = await this.client.GET(
361
+ "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
362
+ options
363
+ );
364
+ if (error) {
365
+ throw ApiClientError.fromError(response, error);
366
+ }
367
+ return data;
222
368
  }
223
369
 
224
370
  async listOrganizations(options?: FetchOptions<operations["listOrganizations"]>) {
225
- const { data } = await this.client.GET("/api/atlas/v2/orgs", options);
371
+ const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options);
372
+ if (error) {
373
+ throw ApiClientError.fromError(response, error);
374
+ }
226
375
  return data;
227
376
  }
228
377
 
229
378
  async listOrganizationProjects(options: FetchOptions<operations["listOrganizationProjects"]>) {
230
- const { data } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
379
+ const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
380
+ if (error) {
381
+ throw ApiClientError.fromError(response, error);
382
+ }
231
383
  return data;
232
384
  }
233
385
 
@@ -1,21 +1,72 @@
1
- export class ApiClientError extends Error {
2
- response?: Response;
1
+ import { ApiError } from "./openapi.js";
3
2
 
4
- constructor(message: string, response: Response | undefined = undefined) {
3
+ export class ApiClientError extends Error {
4
+ private constructor(
5
+ message: string,
6
+ public readonly response: Response,
7
+ public readonly apiError?: ApiError
8
+ ) {
5
9
  super(message);
6
10
  this.name = "ApiClientError";
7
- this.response = response;
8
11
  }
9
12
 
10
13
  static async fromResponse(
11
14
  response: Response,
12
15
  message: string = `error calling Atlas API`
13
16
  ): Promise<ApiClientError> {
17
+ const err = await this.extractError(response);
18
+
19
+ return this.fromError(response, err, message);
20
+ }
21
+
22
+ static fromError(
23
+ response: Response,
24
+ error?: ApiError | string | Error,
25
+ message: string = `error calling Atlas API`
26
+ ): ApiClientError {
27
+ const errorMessage = this.buildErrorMessage(error);
28
+
29
+ const apiError = typeof error === "object" && !(error instanceof Error) ? error : undefined;
30
+
31
+ return new ApiClientError(
32
+ `[${response.status} ${response.statusText}] ${message}: ${errorMessage}`,
33
+ response,
34
+ apiError
35
+ );
36
+ }
37
+
38
+ private static async extractError(response: Response): Promise<ApiError | string | undefined> {
14
39
  try {
15
- const text = await response.text();
16
- return new ApiClientError(`${message}: [${response.status} ${response.statusText}] ${text}`, response);
40
+ return (await response.json()) as ApiError;
17
41
  } catch {
18
- return new ApiClientError(`${message}: ${response.status} ${response.statusText}`, response);
42
+ try {
43
+ return await response.text();
44
+ } catch {
45
+ return undefined;
46
+ }
19
47
  }
20
48
  }
49
+
50
+ private static buildErrorMessage(error?: string | ApiError | Error): string {
51
+ let errorMessage: string = "unknown error";
52
+
53
+ if (error instanceof Error) {
54
+ return error.message;
55
+ }
56
+
57
+ //eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
58
+ switch (typeof error) {
59
+ case "object":
60
+ errorMessage = error.reason || "unknown error";
61
+ if (error.detail && error.detail.length > 0) {
62
+ errorMessage = `${errorMessage}; ${error.detail}`;
63
+ }
64
+ break;
65
+ case "string":
66
+ errorMessage = error;
67
+ break;
68
+ }
69
+
70
+ return errorMessage.trim();
71
+ }
21
72
  }
@@ -0,0 +1,94 @@
1
+ import { ClusterDescription20240805, FlexClusterDescription20241113 } from "./openapi.js";
2
+ import { ApiClient } from "./apiClient.js";
3
+ import logger, { LogId } from "../../logger.js";
4
+
5
+ export interface Cluster {
6
+ name?: string;
7
+ instanceType: "FREE" | "DEDICATED" | "FLEX";
8
+ instanceSize?: string;
9
+ state?: "IDLE" | "CREATING" | "UPDATING" | "DELETING" | "REPAIRING";
10
+ mongoDBVersion?: string;
11
+ connectionString?: string;
12
+ }
13
+
14
+ export function formatFlexCluster(cluster: FlexClusterDescription20241113): Cluster {
15
+ return {
16
+ name: cluster.name,
17
+ instanceType: "FLEX",
18
+ instanceSize: undefined,
19
+ state: cluster.stateName,
20
+ mongoDBVersion: cluster.mongoDBVersion,
21
+ connectionString: cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard,
22
+ };
23
+ }
24
+
25
+ export function formatCluster(cluster: ClusterDescription20240805): Cluster {
26
+ const regionConfigs = (cluster.replicationSpecs || [])
27
+ .map(
28
+ (replicationSpec) =>
29
+ (replicationSpec.regionConfigs || []) as {
30
+ providerName: string;
31
+ electableSpecs?: {
32
+ instanceSize: string;
33
+ };
34
+ readOnlySpecs?: {
35
+ instanceSize: string;
36
+ };
37
+ analyticsSpecs?: {
38
+ instanceSize: string;
39
+ };
40
+ }[]
41
+ )
42
+ .flat()
43
+ .map((regionConfig) => {
44
+ return {
45
+ providerName: regionConfig.providerName,
46
+ instanceSize:
47
+ regionConfig.electableSpecs?.instanceSize ||
48
+ regionConfig.readOnlySpecs?.instanceSize ||
49
+ regionConfig.analyticsSpecs?.instanceSize,
50
+ };
51
+ });
52
+
53
+ const instanceSize = regionConfigs[0]?.instanceSize ?? "UNKNOWN";
54
+ const clusterInstanceType = instanceSize == "M0" ? "FREE" : "DEDICATED";
55
+
56
+ return {
57
+ name: cluster.name,
58
+ instanceType: clusterInstanceType,
59
+ instanceSize: clusterInstanceType == "DEDICATED" ? instanceSize : undefined,
60
+ state: cluster.stateName,
61
+ mongoDBVersion: cluster.mongoDBVersion,
62
+ connectionString: cluster.connectionStrings?.standardSrv || cluster.connectionStrings?.standard,
63
+ };
64
+ }
65
+
66
+ export async function inspectCluster(apiClient: ApiClient, projectId: string, clusterName: string): Promise<Cluster> {
67
+ try {
68
+ const cluster = await apiClient.getCluster({
69
+ params: {
70
+ path: {
71
+ groupId: projectId,
72
+ clusterName,
73
+ },
74
+ },
75
+ });
76
+ return formatCluster(cluster);
77
+ } catch (error) {
78
+ try {
79
+ const cluster = await apiClient.getFlexCluster({
80
+ params: {
81
+ path: {
82
+ groupId: projectId,
83
+ name: clusterName,
84
+ },
85
+ },
86
+ });
87
+ return formatFlexCluster(cluster);
88
+ } catch (flexError) {
89
+ const err = flexError instanceof Error ? flexError : new Error(String(flexError));
90
+ logger.error(LogId.atlasInspectFailure, "inspect-cluster", `error inspecting cluster: ${err.message}`);
91
+ throw error;
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,10 @@
1
+ import { randomBytes } from "crypto";
2
+ import { promisify } from "util";
3
+
4
+ const randomBytesAsync = promisify(randomBytes);
5
+
6
+ export async function generateSecurePassword(): Promise<string> {
7
+ const buf = await randomBytesAsync(16);
8
+ const pass = buf.toString("base64url");
9
+ return pass;
10
+ }