@icib.dev/api-client 1.1.1 → 1.1.3

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.
package/README.md CHANGED
@@ -61,9 +61,13 @@ BASE_PATH=/v2 npx api-client-generate
61
61
  # Custom client base URL (default: from spec URL, BASE_URL, or spec host)
62
62
  npx api-client-generate --base-url https://api.mycompany.com
63
63
  BASE_URL=https://api.mycompany.com npx api-client-generate
64
+
65
+ # Override client.ts (by default, existing client is preserved if you customized it)
66
+ npx api-client-generate --override-client # prompts for confirmation
67
+ npx api-client-generate --override-client --yes # skip confirmation (e.g. CI)
64
68
  ```
65
69
 
66
- The client is generated in your project directory (e.g. `./api/`).
70
+ The client is generated in your project directory (e.g. `./api/`). If `client.ts` already exists, it is **not** overwritten unless you pass `--override-client` (which prompts for confirmation; use `--yes` to skip the prompt).
67
71
 
68
72
  ### From the library repo (maintainers)
69
73
 
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, mkdirSync, writeFileSync } from "fs";
2
+ import { readFileSync, mkdirSync, writeFileSync, existsSync } from "fs";
3
+ import { createInterface } from "readline";
3
4
  import { dirname, join } from "path";
4
5
  import { fileURLToPath } from "url";
5
6
  import SwaggerParser from "@apidevtools/swagger-parser";
@@ -20,6 +21,8 @@ function parseArgs() {
20
21
  let out = DEFAULT_OUT;
21
22
  let basePath = process.env.BASE_PATH;
22
23
  let baseUrl = process.env.BASE_URL;
24
+ let overrideClient = false;
25
+ let yes = false;
23
26
  for (let i = 0; i < args.length; i++) {
24
27
  if (args[i] === "--url" && args[i + 1]) {
25
28
  url = args[++i];
@@ -35,8 +38,24 @@ function parseArgs() {
35
38
  args[i + 1]) {
36
39
  baseUrl = args[++i];
37
40
  }
41
+ else if (args[i] === "--override-client" || args[i] === "--overwrite-client") {
42
+ overrideClient = true;
43
+ }
44
+ else if (args[i] === "--yes" || args[i] === "-y") {
45
+ yes = true;
46
+ }
38
47
  }
39
- return { url, out, basePath, baseUrl };
48
+ return { url, out, basePath, baseUrl, overrideClient, yes };
49
+ }
50
+ function askConfirmation(question) {
51
+ return new Promise((resolve) => {
52
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
53
+ rl.question(question, (answer) => {
54
+ rl.close();
55
+ const normalized = answer.trim().toLowerCase();
56
+ resolve(normalized === "y" || normalized === "yes");
57
+ });
58
+ });
40
59
  }
41
60
  async function fetchSpec(url) {
42
61
  const res = await fetch(url);
@@ -92,7 +111,11 @@ function getDefinitions(doc) {
92
111
  function getPaths(doc) {
93
112
  return doc.paths ?? {};
94
113
  }
95
- /** Extract response schema from OpenAPI 2.0 (schema) or OpenAPI 3.0 (content) */
114
+ /**
115
+ * Extract response schema. Compatible with:
116
+ * - OpenAPI 2.0: response.schema
117
+ * - OpenAPI 3.0: response.content['application/json'].schema
118
+ */
96
119
  function getResponseSchema(response) {
97
120
  if (!response || typeof response !== "object")
98
121
  return undefined;
@@ -111,6 +134,23 @@ function getResponseSchema(response) {
111
134
  }
112
135
  return undefined;
113
136
  }
137
+ /**
138
+ * Extract request body schema from OpenAPI 3.0 requestBody.content.
139
+ * OpenAPI 2.0 uses parameters with in: "body" (handled separately in extractOperations).
140
+ */
141
+ function getRequestBodySchema(requestBody) {
142
+ if (!requestBody || typeof requestBody !== "object")
143
+ return undefined;
144
+ const rb = requestBody;
145
+ const content = rb.content;
146
+ if (content) {
147
+ const jsonContent = content["application/json"] ??
148
+ content["*/*"] ??
149
+ Object.values(content)[0];
150
+ return jsonContent?.schema;
151
+ }
152
+ return undefined;
153
+ }
114
154
  function schemaToTsType(schema, definitions, refsSeen = new Set()) {
115
155
  if (!schema)
116
156
  return "unknown";
@@ -269,6 +309,29 @@ function extractOperations(paths, definitions) {
269
309
  };
270
310
  }
271
311
  }
312
+ // OpenAPI 3.0: body in requestBody (OAS 2.0 uses parameters in: "body" above)
313
+ if (!bodyParam && op.requestBody) {
314
+ const bodySchema = getRequestBodySchema(op.requestBody);
315
+ if (bodySchema) {
316
+ const propertyDescriptions = {};
317
+ if (bodySchema.properties) {
318
+ for (const [propName, propSchema] of Object.entries(bodySchema.properties)) {
319
+ const desc = propSchema
320
+ .description ??
321
+ propSchema.title;
322
+ if (desc)
323
+ propertyDescriptions[propName] = desc;
324
+ }
325
+ }
326
+ bodyParam = {
327
+ name: "data",
328
+ schema: bodySchema,
329
+ propertyDescriptions: Object.keys(propertyDescriptions).length > 0
330
+ ? propertyDescriptions
331
+ : undefined,
332
+ };
333
+ }
334
+ }
272
335
  const successResponse = op.responses?.["200"] ?? op.responses?.["201"];
273
336
  const respSchema = getResponseSchema(successResponse);
274
337
  const respDesc = successResponse?.description ?? "";
@@ -676,7 +739,7 @@ function generateIndex(contextTags) {
676
739
  return exports.join("\n");
677
740
  }
678
741
  async function main() {
679
- const { url, out, basePath: basePathOverride, baseUrl: baseUrlOverride, } = parseArgs();
742
+ const { url, out, basePath: basePathOverride, baseUrl: baseUrlOverride, overrideClient, yes, } = parseArgs();
680
743
  console.log(`Fetching spec from ${url}...`);
681
744
  const rawSpec = await loadRawSpec(url);
682
745
  const doc = await parseSpec(rawSpec);
@@ -713,7 +776,23 @@ async function main() {
713
776
  ? `${baseUrl.replace(/\/$/, "")}${basePath}`
714
777
  : baseUrl;
715
778
  writeFileSync(join(typesDir, "index.ts"), generateTypes(definitions));
716
- writeFileSync(join(outDir, "client.ts"), generateClient(clientBaseUrl));
779
+ const clientPath = join(outDir, "client.ts");
780
+ const clientExists = existsSync(clientPath);
781
+ let writeClient = !clientExists;
782
+ if (clientExists) {
783
+ if (overrideClient) {
784
+ if (yes) {
785
+ writeClient = true;
786
+ }
787
+ else {
788
+ const confirmed = await askConfirmation("This will overwrite your client.ts. Are you sure? (y/N) ");
789
+ writeClient = confirmed;
790
+ }
791
+ }
792
+ }
793
+ if (writeClient) {
794
+ writeFileSync(clientPath, generateClient(clientBaseUrl));
795
+ }
717
796
  const sortedTags = [...byTag.keys()].sort();
718
797
  for (const tag of sortedTags) {
719
798
  const ctxName = sanitizeContextName(tag);
@@ -736,7 +815,7 @@ async function main() {
736
815
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
737
816
  console.log(`Generated API client in ${outDir}`);
738
817
  console.log(` - types/index.ts`);
739
- console.log(` - client.ts`);
818
+ console.log(` - client.ts${writeClient ? "" : " (skipped, use --override-client to overwrite)"}`);
740
819
  console.log(` - apiClient.ts`);
741
820
  console.log(` - contexts/*.ts (${sortedTags.length} files)`);
742
821
  console.log(` - index.ts`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icib.dev/api-client",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Generator for strictly-typed TypeScript API clients from OpenAPI specs",
5
5
  "type": "module",
6
6
  "bin": {