Skip to content

Commit e0e4bf7

Browse files
committed
refactor: use openapi-fetch
1 parent 280ceaf commit e0e4bf7

File tree

11 files changed

+207
-107
lines changed

11 files changed

+207
-107
lines changed

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"mongodb-log-writer": "^2.4.1",
5959
"mongodb-redact": "^1.1.6",
6060
"mongodb-schema": "^12.6.2",
61+
"openapi-fetch": "^0.13.5",
6162
"zod": "^3.24.2"
6263
},
6364
"engines": {

scripts/filter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document {
2222
"getProject",
2323
"createProject",
2424
"listClusters",
25+
"getCluster",
2526
"createCluster",
2627
"listClustersForAllProjects",
2728
"createDatabaseUser",

src/common/atlas/apiClient.ts

Lines changed: 111 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
import config from "../../config.js";
2+
import createClient, { Middleware } from "openapi-fetch";
23

34
import {
4-
Group,
5-
PaginatedOrgGroupView,
6-
PaginatedAtlasGroupView,
5+
paths,
76
ClusterDescription20240805,
8-
PaginatedClusterDescription20240805,
9-
PaginatedNetworkAccessView,
107
NetworkPermissionEntry,
118
CloudDatabaseUser,
12-
PaginatedApiAtlasDatabaseUserView,
139
} from "./openapi.js";
1410

1511
export interface OAuthToken {
@@ -48,32 +44,40 @@ export interface ApiClientOptions {
4844
}
4945

5046
export class ApiClient {
51-
token?: OAuthToken;
52-
saveToken?: saveTokenFunction;
47+
private token?: OAuthToken;
48+
private saveToken?: saveTokenFunction;
49+
private client = createClient<paths>({
50+
baseUrl: config.apiBaseURL,
51+
headers: {
52+
"User-Agent": config.userAgent,
53+
Accept: `application/vnd.atlas.${config.atlasApiVersion}+json`,
54+
},
55+
});
56+
private authMiddleware = (apiClient: ApiClient): Middleware => ({
57+
async onRequest({ request, schemaPath }) {
58+
if (schemaPath.startsWith("/api/private/unauth") || schemaPath.startsWith("/api/oauth")) {
59+
return undefined;
60+
}
61+
if (await apiClient.validateToken()) {
62+
request.headers.set("Authorization", `Bearer ${apiClient.token?.access_token}`);
63+
return request;
64+
}
65+
},
66+
});
67+
private errorMiddleware = (): Middleware => ({
68+
async onResponse({ response }) {
69+
if (!response.ok) {
70+
throw new ApiClientError(`Error calling Atlas API: ${await response.text()}`, response);
71+
}
72+
},
73+
});
5374

5475
constructor(options: ApiClientOptions) {
5576
const { token, saveToken } = options;
5677
this.token = token;
5778
this.saveToken = saveToken;
58-
}
59-
60-
private defaultOptions(): RequestInit {
61-
const authHeaders = !this.token?.access_token
62-
? null
63-
: {
64-
Authorization: `Bearer ${this.token.access_token}`,
65-
};
66-
67-
return {
68-
method: "GET",
69-
credentials: !this.token?.access_token ? undefined : "include",
70-
headers: {
71-
"Content-Type": "application/json",
72-
Accept: `application/vnd.atlas.${config.atlasApiVersion}+json`,
73-
"User-Agent": config.userAgent,
74-
...authHeaders,
75-
},
76-
};
79+
this.client.use(this.authMiddleware(this));
80+
this.client.use(this.errorMiddleware());
7781
}
7882

7983
async storeToken(token: OAuthToken): Promise<OAuthToken> {
@@ -86,36 +90,6 @@ export class ApiClient {
8690
return token;
8791
}
8892

89-
async do<T>(endpoint: string, options?: RequestInit): Promise<T> {
90-
if (!this.token || !this.token.access_token) {
91-
throw new Error("Not authenticated. Please run the auth tool first.");
92-
}
93-
94-
const url = new URL(`api/atlas/v2${endpoint}`, `${config.apiBaseURL}`);
95-
96-
if (!this.checkTokenExpiry()) {
97-
await this.refreshToken();
98-
}
99-
100-
const defaultOpt = this.defaultOptions();
101-
const opt = {
102-
...defaultOpt,
103-
...options,
104-
headers: {
105-
...defaultOpt.headers,
106-
...options?.headers,
107-
},
108-
};
109-
110-
const response = await fetch(url, opt);
111-
112-
if (!response.ok) {
113-
throw new ApiClientError(`Error calling Atlas API: ${await response.text()}`, response);
114-
}
115-
116-
return (await response.json()) as T;
117-
}
118-
11993
async authenticate(): Promise<OauthDeviceCode> {
12094
const endpoint = "api/private/unauth/account/device/authorize";
12195

@@ -269,58 +243,108 @@ export class ApiClient {
269243
}
270244
}
271245

272-
async listProjects(): Promise<PaginatedAtlasGroupView> {
273-
return await this.do<PaginatedAtlasGroupView>("/groups");
246+
async listProjects() {
247+
const { data } = await this.client.GET(`/api/atlas/v2/groups`);
248+
return data;
274249
}
275250

276-
async listProjectIpAccessLists(groupId: string): Promise<PaginatedNetworkAccessView> {
277-
return await this.do<PaginatedNetworkAccessView>(`/groups/${groupId}/accessList`);
251+
async listProjectIpAccessLists(groupId: string) {
252+
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/accessList`, {
253+
params: {
254+
path: {
255+
groupId,
256+
}
257+
}
258+
});
259+
return data;
278260
}
279261

280262
async createProjectIpAccessList(
281263
groupId: string,
282-
entries: NetworkPermissionEntry[]
283-
): Promise<PaginatedNetworkAccessView> {
284-
return await this.do<PaginatedNetworkAccessView>(`/groups/${groupId}/accessList`, {
285-
method: "POST",
286-
body: JSON.stringify(entries),
264+
...entries: NetworkPermissionEntry[]
265+
) {
266+
const { data } = await this.client.POST(`/api/atlas/v2/groups/{groupId}/accessList`, {
267+
params: {
268+
path: {
269+
groupId,
270+
}
271+
},
272+
body: entries,
287273
});
274+
return data;
288275
}
289276

290-
async getProject(groupId: string): Promise<Group> {
291-
return await this.do<Group>(`/groups/${groupId}`);
277+
async getProject(groupId: string) {
278+
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}`, {
279+
params: {
280+
path: {
281+
groupId,
282+
}
283+
}
284+
});
285+
return data;
292286
}
293287

294-
async listClusters(groupId: string): Promise<PaginatedClusterDescription20240805> {
295-
return await this.do<PaginatedClusterDescription20240805>(`/groups/${groupId}/clusters`);
288+
async listClusters(groupId: string) {
289+
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters`, {
290+
params: {
291+
path: {
292+
groupId,
293+
}
294+
}
295+
});
296+
return data;
296297
}
297298

298-
async listClustersForAllProjects(): Promise<PaginatedOrgGroupView> {
299-
return await this.do<PaginatedOrgGroupView>(`/clusters`);
299+
async listClustersForAllProjects() {
300+
const { data } = await this.client.GET(`/api/atlas/v2/clusters`);
301+
return data;
300302
}
301303

302-
async getCluster(groupId: string, clusterName: string): Promise<ClusterDescription20240805> {
303-
return await this.do<ClusterDescription20240805>(`/groups/${groupId}/clusters/${clusterName}`);
304+
async getCluster(groupId: string, clusterName: string) {
305+
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters/{clusterName}`, {
306+
params: {
307+
path: {
308+
groupId,
309+
clusterName,
310+
}
311+
}
312+
});
313+
return data;
304314
}
305315

306-
async createCluster(groupId: string, cluster: ClusterDescription20240805): Promise<ClusterDescription20240805> {
307-
if (!cluster.groupId) {
308-
throw new Error("Cluster groupId is required");
309-
}
310-
return await this.do<ClusterDescription20240805>(`/groups/${groupId}/clusters`, {
311-
method: "POST",
312-
body: JSON.stringify(cluster),
316+
async createCluster(groupId: string, cluster: ClusterDescription20240805) {
317+
const { data } = await this.client.POST('/api/atlas/v2/groups/{groupId}/clusters', {
318+
params: {
319+
path: {
320+
groupId,
321+
}
322+
},
323+
body: cluster,
313324
});
325+
return data;
314326
}
315327

316-
async createDatabaseUser(groupId: string, user: CloudDatabaseUser): Promise<CloudDatabaseUser> {
317-
return await this.do<CloudDatabaseUser>(`/groups/${groupId}/databaseUsers`, {
318-
method: "POST",
319-
body: JSON.stringify(user),
328+
async createDatabaseUser(groupId: string, user: CloudDatabaseUser) {
329+
const { data } = await this.client.POST('/api/atlas/v2/groups/{groupId}/databaseUsers', {
330+
params: {
331+
path: {
332+
groupId,
333+
}
334+
},
335+
body: user,
320336
});
337+
return data;
321338
}
322339

323-
async listDatabaseUsers(groupId: string): Promise<PaginatedApiAtlasDatabaseUserView> {
324-
return await this.do<PaginatedApiAtlasDatabaseUserView>(`/groups/${groupId}/databaseUsers`);
340+
async listDatabaseUsers(groupId: string) {
341+
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/databaseUsers`, {
342+
params: {
343+
path: {
344+
groupId,
345+
}
346+
}
347+
});
348+
return data;
325349
}
326350
}

src/common/atlas/openapi.d.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,28 @@ export interface paths {
120120
patch?: never;
121121
trace?: never;
122122
};
123+
"/api/atlas/v2/groups/{groupId}/clusters/{clusterName}": {
124+
parameters: {
125+
query?: never;
126+
header?: never;
127+
path?: never;
128+
cookie?: never;
129+
};
130+
/**
131+
* Return One Cluster from One Project
132+
* @description Returns the details for one cluster in the specified project. Clusters contain a group of hosts that maintain the same data set. The response includes clusters with asymmetrically-sized shards. To use this resource, the requesting Service Account or API Key must have the Project Read Only role. This feature is not available for serverless clusters.
133+
*
134+
* This endpoint can also be used on Flex clusters that were created using the [createCluster](https://www.mongodb.com/docs/atlas/reference/api-resources-spec/v2/#tag/Clusters/operation/createCluster) endpoint or former M2/M5 clusters that have been migrated to Flex clusters until January 2026. Please use the getFlexCluster endpoint for Flex clusters instead. Deprecated versions: v2-{2023-02-01}, v2-{2023-01-01}
135+
*/
136+
get: operations["getCluster"];
137+
put?: never;
138+
post?: never;
139+
delete?: never;
140+
options?: never;
141+
head?: never;
142+
patch?: never;
143+
trace?: never;
144+
};
123145
"/api/atlas/v2/groups/{groupId}/databaseUsers": {
124146
parameters: {
125147
query?: never;
@@ -7467,6 +7489,42 @@ export interface operations {
74677489
500: components["responses"]["internalServerError"];
74687490
};
74697491
};
7492+
getCluster: {
7493+
parameters: {
7494+
query?: {
7495+
/** @description Flag that indicates whether Application wraps the response in an `envelope` JSON object. Some API clients cannot access the HTTP response headers or status code. To remediate this, set envelope=true in the query. Endpoints that return a list of results use the results object as an envelope. Application adds the status parameter to the response body. */
7496+
envelope?: components["parameters"]["envelope"];
7497+
/** @description Flag that indicates whether the response body should be in the prettyprint format. */
7498+
pretty?: components["parameters"]["pretty"];
7499+
};
7500+
header?: never;
7501+
path: {
7502+
/** @description Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access.
7503+
*
7504+
* **NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group/project id remains the same. The resource and corresponding endpoints use the term groups. */
7505+
groupId: components["parameters"]["groupId"];
7506+
/** @description Human-readable label that identifies this cluster. */
7507+
clusterName: string;
7508+
};
7509+
cookie?: never;
7510+
};
7511+
requestBody?: never;
7512+
responses: {
7513+
/** @description OK */
7514+
200: {
7515+
headers: {
7516+
[name: string]: unknown;
7517+
};
7518+
content: {
7519+
"application/vnd.atlas.2024-08-05+json": components["schemas"]["ClusterDescription20240805"];
7520+
};
7521+
};
7522+
401: components["responses"]["unauthorized"];
7523+
404: components["responses"]["notFound"];
7524+
409: components["responses"]["conflict"];
7525+
500: components["responses"]["internalServerError"];
7526+
};
7527+
};
74707528
listDatabaseUsers: {
74717529
parameters: {
74727530
query?: {

src/tools/atlas/createAccessList.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class CreateAccessListTool extends AtlasToolBase {
4444

4545
const inputs = [...ipInputs, ...cidrInputs];
4646

47-
await this.apiClient.createProjectIpAccessList(projectId, inputs);
47+
await this.apiClient.createProjectIpAccessList(projectId, ...inputs);
4848

4949
return {
5050
content: [

src/tools/atlas/inspectAccessList.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class InspectAccessListTool extends AtlasToolBase {
1515

1616
const accessList = await this.apiClient.listProjectIpAccessLists(projectId);
1717

18-
if (!accessList.results?.length) {
18+
if (!accessList?.results?.length) {
1919
throw new Error("No access list entries found.");
2020
}
2121

0 commit comments

Comments
 (0)