Skip to content

Commit 949f718

Browse files
committed
chore: auto generate apiClient
1 parent 0becdaa commit 949f718

File tree

6 files changed

+147
-47
lines changed

6 files changed

+147
-47
lines changed

.vscode/launch.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
"request": "launch",
1010
"name": "Launch Program",
1111
"skipFiles": ["<node_internals>/**"],
12-
"program": "${workspaceFolder}/dist/index.js",
13-
"preLaunchTask": "tsc: build - tsconfig.json",
14-
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
12+
"program": "${workspaceFolder}/scripts/apply.ts",
13+
"args": [
14+
"--spec",
15+
"${workspaceFolder}/scripts/bundledSpec.json",
16+
"--template",
17+
"${workspaceFolder}/scripts/apiClient.ts.template"
18+
],
1519
}
1620
]
1721
}

scripts/apply.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import fs, { writeFile } from "fs";
2+
import { OpenAPIV3_1 } from "openapi-types";
3+
import argv from "yargs-parser";
4+
import { promisify } from "util";
5+
6+
const readFileAsync = promisify(fs.readFile);
7+
const writeFileAsync = promisify(fs.writeFile);
8+
9+
function findParamFromRef(ref: string, openapi: OpenAPIV3_1.Document): OpenAPIV3_1.ParameterObject {
10+
const paramParts = ref.split("/");
11+
paramParts.shift(); // Remove the first part which is always '#'
12+
let param: any = openapi; // eslint-disable-line @typescript-eslint/no-explicit-any
13+
while (true) {
14+
const part = paramParts.shift();
15+
if (!part) {
16+
break;
17+
}
18+
param = param[part];
19+
}
20+
return param;
21+
}
22+
23+
async function main() {
24+
const {spec, file} = argv(process.argv.slice(2));
25+
26+
if (!spec || !file) {
27+
console.error("Please provide both --spec and --file arguments.");
28+
process.exit(1);
29+
}
30+
31+
const specFile = await readFileAsync(spec, "utf8") as string;
32+
33+
const operations: {
34+
path: string;
35+
method: string;
36+
operationId: string;
37+
requiredParams: boolean;
38+
tag: string;
39+
}[] = [];
40+
41+
const openapi = JSON.parse(specFile) as OpenAPIV3_1.Document;
42+
for (const path in openapi.paths) {
43+
for (const method in openapi.paths[path]) {
44+
const operation: OpenAPIV3_1.OperationObject = openapi.paths[path][method];
45+
46+
if (!operation.operationId || !operation.tags?.length) {
47+
continue;
48+
}
49+
50+
let requiredParams = !!operation.requestBody;
51+
52+
for (const param of operation.parameters || []) {
53+
const ref = (param as OpenAPIV3_1.ReferenceObject).$ref as string | undefined;
54+
let paramObject: OpenAPIV3_1.ParameterObject = param as OpenAPIV3_1.ParameterObject;
55+
if (ref) {
56+
paramObject = findParamFromRef(ref, openapi);
57+
}
58+
if (paramObject.in === "path") {
59+
requiredParams = true;
60+
}
61+
}
62+
63+
operations.push({
64+
path,
65+
method: method.toUpperCase(),
66+
operationId: operation.operationId || '',
67+
requiredParams,
68+
tag: operation.tags[0],
69+
});
70+
}
71+
}
72+
73+
const operationOutput = operations.map((operation) => {
74+
const { operationId, method, path, requiredParams } = operation;
75+
return `async ${operationId}(options${requiredParams ? '' : '?'}: FetchOptions<operations["${operationId}"]>) {
76+
const { data } = await this.client.${method}("${path}", options);
77+
return data;
78+
}
79+
`;
80+
}).join("\n");
81+
82+
const templateFile = await readFileAsync(file, "utf8") as string;
83+
const output = templateFile.replace(/\/\/ DO NOT EDIT\. This is auto-generated code\.\n.*\/\/ DO NOT EDIT\. This is auto-generated code\./g, operationOutput);
84+
85+
await writeFileAsync(file, output, "utf8");
86+
}
87+
88+
main().catch((error) => {
89+
console.error("Error:", error);
90+
process.exit(1);
91+
});

scripts/filter.ts

100644100755
File mode changed.

scripts/generate.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ curl -Lo ./scripts/spec.json https://github.com/mongodb/openapi/raw/refs/heads/m
66
tsx ./scripts/filter.ts > ./scripts/filteredSpec.json < ./scripts/spec.json
77
redocly bundle --ext json --remove-unused-components ./scripts/filteredSpec.json --output ./scripts/bundledSpec.json
88
openapi-typescript ./scripts/bundledSpec.json --root-types-no-schema-prefix --root-types --output ./src/common/atlas/openapi.d.ts
9-
prettier --write ./src/common/atlas/openapi.d.ts
9+
tsx ./scripts/apply.ts --spec ./scripts/bundledSpec.json --file ./src/common/atlas/apiClient.ts
10+
prettier --write ./src/common/atlas/openapi.d.ts ./src/common/atlas/apiClient.ts
1011
rm -rf ./scripts/bundledSpec.json ./scripts/filteredSpec.json ./scripts/spec.json

src/common/atlas/apiClient.ts

Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,11 @@
11
import config from "../../config.js";
22
import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch";
33
import { AccessToken, ClientCredentials } from "simple-oauth2";
4-
4+
import { ApiClientError } from "./apiClientError.js";
55
import { paths, operations } from "./openapi.js";
66

77
const ATLAS_API_VERSION = "2025-03-12";
88

9-
export class ApiClientError extends Error {
10-
response?: Response;
11-
12-
constructor(message: string, response: Response | undefined = undefined) {
13-
super(message);
14-
this.name = "ApiClientError";
15-
this.response = response;
16-
}
17-
18-
static async fromResponse(response: Response, message?: string): Promise<ApiClientError> {
19-
message ||= `error calling Atlas API`;
20-
try {
21-
const text = await response.text();
22-
return new ApiClientError(`${message}: [${response.status} ${response.statusText}] ${text}`, response);
23-
} catch {
24-
return new ApiClientError(`${message}: ${response.status} ${response.statusText}`, response);
25-
}
26-
}
27-
}
28-
299
export interface ApiClientOptions {
3010
credentials?: {
3111
clientId: string;
@@ -79,14 +59,12 @@ export class ApiClient {
7959
});
8060

8161
constructor(options?: ApiClientOptions) {
82-
const defaultOptions = {
83-
baseUrl: "https://cloud.mongodb.com/",
84-
userAgent: `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
85-
};
86-
8762
this.options = {
88-
...defaultOptions,
8963
...options,
64+
baseUrl: options?.baseUrl || "https://cloud.mongodb.com/",
65+
userAgent:
66+
options?.userAgent ||
67+
`AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
9068
};
9169

9270
this.client = createClient<paths>({
@@ -136,38 +114,39 @@ export class ApiClient {
136114
};
137115
}
138116

139-
async listProjects(options?: FetchOptions<operations["listProjects"]>) {
140-
const { data } = await this.client.GET(`/api/atlas/v2/groups`, options);
117+
// DO NOT EDIT. This is auto-generated code.
118+
async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
119+
const { data } = await this.client.GET("/api/atlas/v2/clusters", options);
141120
return data;
142121
}
143122

144-
async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
145-
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/accessList`, options);
123+
async listProjects(options?: FetchOptions<operations["listProjects"]>) {
124+
const { data } = await this.client.GET("/api/atlas/v2/groups", options);
146125
return data;
147126
}
148127

149-
async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
150-
const { data } = await this.client.POST(`/api/atlas/v2/groups/{groupId}/accessList`, options);
128+
async createProject(options: FetchOptions<operations["createProject"]>) {
129+
const { data } = await this.client.POST("/api/atlas/v2/groups", options);
151130
return data;
152131
}
153132

154133
async getProject(options: FetchOptions<operations["getProject"]>) {
155-
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}`, options);
134+
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
156135
return data;
157136
}
158137

159-
async listClusters(options: FetchOptions<operations["listClusters"]>) {
160-
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters`, options);
138+
async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
139+
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
161140
return data;
162141
}
163142

164-
async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
165-
const { data } = await this.client.GET(`/api/atlas/v2/clusters`, options);
143+
async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
144+
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
166145
return data;
167146
}
168147

169-
async getCluster(options: FetchOptions<operations["getCluster"]>) {
170-
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters/{clusterName}`, options);
148+
async listClusters(options: FetchOptions<operations["listClusters"]>) {
149+
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
171150
return data;
172151
}
173152

@@ -176,13 +155,19 @@ export class ApiClient {
176155
return data;
177156
}
178157

179-
async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
180-
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
158+
async getCluster(options: FetchOptions<operations["getCluster"]>) {
159+
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", options);
181160
return data;
182161
}
183162

184163
async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
185-
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/databaseUsers`, options);
164+
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
165+
return data;
166+
}
167+
168+
async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
169+
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
186170
return data;
187171
}
172+
// DO NOT EDIT. This is auto-generated code.
188173
}

src/common/atlas/apiClientError.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export class ApiClientError extends Error {
2+
response?: Response;
3+
4+
constructor(message: string, response: Response | undefined = undefined) {
5+
super(message);
6+
this.name = "ApiClientError";
7+
this.response = response;
8+
}
9+
10+
static async fromResponse(response: Response, message?: string): Promise<ApiClientError> {
11+
message ||= `error calling Atlas API`;
12+
try {
13+
const text = await response.text();
14+
return new ApiClientError(`${message}: [${response.status} ${response.statusText}] ${text}`, response);
15+
} catch {
16+
return new ApiClientError(`${message}: ${response.status} ${response.statusText}`, response);
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)