api-farmer 0.0.19 → 0.0.21

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
@@ -210,6 +210,10 @@ export interface ApiModulePayload {
210
210
  * such as User, Comment, Post, etc.
211
211
  */
212
212
  entity: string
213
+ /**
214
+ * The request content type of the API endpoint, such as 'application/json', 'application/x-www-form-urlencoded'.
215
+ */
216
+ requestContentType?: string
213
217
  /**
214
218
  * The type name of the API endpoint,
215
219
  * such as ApiGetUsers, ApiCreatePost, ApiUpdateComment, etc.
@@ -0,0 +1,285 @@
1
+ import {
2
+ CWD,
3
+ SUPPORTED_HTTP_METHODS,
4
+ getRequestBodyContentType,
5
+ getResponseMetadataItems,
6
+ hasQueryParameter,
7
+ isRequiredRequestBody,
8
+ readSchema,
9
+ readTemplateFile
10
+ } from "./chunk-PTGO2Y5X.js";
11
+
12
+ // src/generate.ts
13
+ import { resolve } from "path";
14
+ import ejs from "ejs";
15
+ import fse from "fs-extra";
16
+ import openapiTS, { astToString } from "openapi-typescript";
17
+ import prettier from "prettier";
18
+ import { groupBy, isArray, merge } from "rattail";
19
+ import { logger } from "rslog";
20
+
21
+ // src/config.ts
22
+ import { loadConfig } from "unconfig";
23
+ function defineConfig(config) {
24
+ return config;
25
+ }
26
+ async function getConfig() {
27
+ const { config } = await loadConfig({
28
+ sources: [
29
+ {
30
+ files: "api-farmer.config"
31
+ }
32
+ ]
33
+ });
34
+ return config ?? {};
35
+ }
36
+
37
+ // src/transformer.ts
38
+ import pluralize from "pluralize";
39
+ import { camelize, pascalCase } from "rattail";
40
+ function transformModuleName({ name }) {
41
+ return camelize(name);
42
+ }
43
+ function transformUrl({ path, base }) {
44
+ return (base ? path.replace(base, "") : path).replace(/{/g, ":").replace(/}/g, "");
45
+ }
46
+ function transformVerb({ method }) {
47
+ switch (method) {
48
+ case "post":
49
+ return "Create";
50
+ case "put":
51
+ return "Update";
52
+ default:
53
+ return pascalCase(method);
54
+ }
55
+ }
56
+ function transformEntity({ path, method, base }) {
57
+ path = base ? path.replace(base, "") : path;
58
+ const words = path.split("/").filter(Boolean);
59
+ return words.reduce((entity, word, index) => {
60
+ if (word.includes("{")) {
61
+ return entity;
62
+ }
63
+ word = word.replace(/\.([a-z])/g, (_, p) => p.toUpperCase());
64
+ word = pluralize.singular(pascalCase(word));
65
+ if (method === "get" && index === words.length - 1) {
66
+ word = pluralize.plural(word);
67
+ }
68
+ return `${entity}${word}`;
69
+ }, "");
70
+ }
71
+ function transformFn({ verb, entity }) {
72
+ return `api${verb}${entity}`;
73
+ }
74
+ function transformType({ verb, entity }) {
75
+ return `Api${verb}${entity}`;
76
+ }
77
+ function transformTypeValue({ path, method }) {
78
+ return `paths['${path}']['${method}']`;
79
+ }
80
+ function transformTypeQuery({ type }) {
81
+ return `${type}Query`;
82
+ }
83
+ function transformTypeQueryValue({
84
+ type
85
+ }) {
86
+ return `${type}['parameters']['query']`;
87
+ }
88
+ function transformTypeRequestBody({
89
+ type
90
+ }) {
91
+ return `${type}RequestBody`;
92
+ }
93
+ function transformTypeRequestBodyValue({
94
+ type,
95
+ required,
96
+ requestContentType
97
+ }) {
98
+ return required ? `${type}['requestBody']['content']['${requestContentType}']` : `NonNullable<${type}['requestBody']>['content']['${requestContentType}'] | undefined`;
99
+ }
100
+ function transformTypeResponseBody({
101
+ type
102
+ }) {
103
+ return `${type}ResponseBody`;
104
+ }
105
+ function transformTypeResponseBodyValue({
106
+ type,
107
+ responseMetadataItems
108
+ }) {
109
+ return responseMetadataItems.map(({ status, responseContentType }) => `${type}['responses']['${status}']['content']['${responseContentType}']`).join(" | ");
110
+ }
111
+ function createTransformer() {
112
+ return {
113
+ moduleName: transformModuleName,
114
+ verb: transformVerb,
115
+ url: transformUrl,
116
+ entity: transformEntity,
117
+ fn: transformFn,
118
+ type: transformType,
119
+ typeValue: transformTypeValue,
120
+ typeQuery: transformTypeQuery,
121
+ typeQueryValue: transformTypeQueryValue,
122
+ typeRequestBody: transformTypeRequestBody,
123
+ typeRequestBodyValue: transformTypeRequestBodyValue,
124
+ typeResponseBody: transformTypeResponseBody,
125
+ typeResponseBodyValue: transformTypeResponseBodyValue
126
+ };
127
+ }
128
+
129
+ // src/generate.ts
130
+ function transformPayloads(pathItems, options) {
131
+ const { transformer, path, base, validateStatus } = options;
132
+ return Object.entries(pathItems).filter(([key]) => SUPPORTED_HTTP_METHODS.includes(key)).reduce((payloads, [method, operation]) => {
133
+ const url = transformer.url({ path, base });
134
+ const args = { path, base, url, method, operation };
135
+ const entity = transformer.entity(args);
136
+ const verb = transformer.verb(args);
137
+ const requestContentType = operation.requestBody ? getRequestBodyContentType(operation.requestBody) : void 0;
138
+ const responseMetadataItems = getResponseMetadataItems(operation, validateStatus);
139
+ const fn = transformer.fn({ ...args, verb, entity });
140
+ const type = transformer.type({ ...args, verb, entity });
141
+ const typeValue = transformer.typeValue({ ...args, verb, entity });
142
+ const typeQuery = transformer.typeQuery({ ...args, type, verb, entity });
143
+ const typeQueryValue = hasQueryParameter(operation) ? transformer.typeQueryValue({ ...args, type, verb, entity }) : "undefined";
144
+ const typeRequestBody = transformer.typeRequestBody({ ...args, type, verb, entity });
145
+ const typeRequestBodyValue = operation.requestBody && requestContentType ? transformer.typeRequestBodyValue({
146
+ ...args,
147
+ type,
148
+ verb,
149
+ entity,
150
+ required: isRequiredRequestBody(operation.requestBody),
151
+ requestContentType
152
+ }) : "undefined";
153
+ const typeResponseBody = transformer.typeResponseBody({ ...args, type, verb, entity });
154
+ const typeResponseBodyValue = responseMetadataItems.length > 0 ? transformer.typeResponseBodyValue({ ...args, type, verb, entity, responseMetadataItems }) : "undefined";
155
+ payloads.push({
156
+ fn,
157
+ url,
158
+ method,
159
+ verb,
160
+ entity,
161
+ requestContentType,
162
+ type,
163
+ typeValue,
164
+ typeQuery,
165
+ typeQueryValue,
166
+ typeRequestBody,
167
+ typeRequestBodyValue,
168
+ typeResponseBody,
169
+ typeResponseBodyValue
170
+ });
171
+ return payloads;
172
+ }, []);
173
+ }
174
+ function partitionApiModules(schema, options) {
175
+ const { base, transformer, validateStatus } = options;
176
+ const schemaPaths = schema.paths ?? {};
177
+ const schemaPathKeys = base ? Object.keys(schemaPaths).map((key) => key.replace(base, "")) : Object.keys(schemaPaths);
178
+ const keyToPaths = groupBy(schemaPathKeys, (key) => key.split("/")[1]);
179
+ const apiModules = Object.entries(keyToPaths).reduce((apiModules2, [name, paths]) => {
180
+ const payloads = paths.reduce((payloads2, path) => {
181
+ const pathItems = schemaPaths[path];
182
+ payloads2.push(
183
+ ...transformPayloads(pathItems, { ...options, path: base ? base + path : path, transformer, validateStatus })
184
+ );
185
+ return payloads2;
186
+ }, []);
187
+ apiModules2.push({ name: transformer.moduleName({ name }), payloads });
188
+ return apiModules2;
189
+ }, []);
190
+ return apiModules;
191
+ }
192
+ function renderApiModules(apiModules, options) {
193
+ const { output, ts, typesOnly, overrides, preset } = options;
194
+ const templateFile = readTemplateFile(preset);
195
+ const typesFilename = options.typesFilename.replace(".ts", "");
196
+ return Promise.all(
197
+ apiModules.map(
198
+ (apiModule) => new Promise((promiseResolve) => {
199
+ const data = {
200
+ apiModule,
201
+ typesFilename,
202
+ ts,
203
+ typesOnly
204
+ };
205
+ prettier.format(ejs.render(templateFile, data), {
206
+ parser: "typescript",
207
+ semi: false,
208
+ singleQuote: true,
209
+ printWidth: 120
210
+ }).then((content) => {
211
+ const path = resolve(output, `${apiModule.name}.${ts ? "ts" : "js"}`);
212
+ const shouldSkip = (!overrides || isArray(overrides) && !overrides.includes(apiModule.name)) && fse.existsSync(path);
213
+ if (shouldSkip) {
214
+ logger.warn(`File already exists, skip: ${path}`);
215
+ promiseResolve(content);
216
+ return;
217
+ }
218
+ fse.outputFileSync(path, content);
219
+ logger.success(`Generated ${path}`);
220
+ promiseResolve(content);
221
+ });
222
+ })
223
+ )
224
+ );
225
+ }
226
+ async function generateTypes(schema, output, typesFilename) {
227
+ const ast = await openapiTS(schema);
228
+ const contents = astToString(ast);
229
+ const typesFilepath = resolve(CWD, output, typesFilename);
230
+ fse.outputFileSync(typesFilepath, contents);
231
+ logger.success(`Generated ${typesFilepath}`);
232
+ }
233
+ async function generate(userOptions = {}) {
234
+ const config = await getConfig();
235
+ const options = merge(config, userOptions);
236
+ const {
237
+ base,
238
+ ts = true,
239
+ typesOnly = false,
240
+ overrides = true,
241
+ preset = "axle",
242
+ input = "./schema.json",
243
+ output = "./src/apis/generated",
244
+ typesFilename = "_types.ts",
245
+ validateStatus = (status) => status >= 200 && status < 300,
246
+ transformer = {}
247
+ } = options;
248
+ const mergedTransformer = { ...createTransformer(), ...transformer };
249
+ const schema = await readSchema(input);
250
+ logger.info("Generating API modules...");
251
+ if (ts) {
252
+ await generateTypes(schema, output, typesFilename);
253
+ }
254
+ const apiModules = partitionApiModules(schema, {
255
+ base,
256
+ transformer: mergedTransformer,
257
+ validateStatus
258
+ });
259
+ await renderApiModules(apiModules, { output, typesFilename, ts, typesOnly, overrides, preset });
260
+ logger.success("Done");
261
+ }
262
+
263
+ export {
264
+ defineConfig,
265
+ getConfig,
266
+ transformModuleName,
267
+ transformUrl,
268
+ transformVerb,
269
+ transformEntity,
270
+ transformFn,
271
+ transformType,
272
+ transformTypeValue,
273
+ transformTypeQuery,
274
+ transformTypeQueryValue,
275
+ transformTypeRequestBody,
276
+ transformTypeRequestBodyValue,
277
+ transformTypeResponseBody,
278
+ transformTypeResponseBodyValue,
279
+ createTransformer,
280
+ transformPayloads,
281
+ partitionApiModules,
282
+ renderApiModules,
283
+ generateTypes,
284
+ generate
285
+ };
@@ -1,6 +1,9 @@
1
- import {
2
- __dirname
3
- } from "./chunk-6OIOYGN7.js";
1
+ // node_modules/.pnpm/tsup@8.3.5_jiti@2.4.2_postcss@8.5.1_tsx@4.19.2_typescript@5.3.3_yaml@2.7.0/node_modules/tsup/assets/esm_shims.js
2
+ import { fileURLToPath } from "url";
3
+ import path from "path";
4
+ var getFilename = () => fileURLToPath(import.meta.url);
5
+ var getDirname = () => path.dirname(getFilename());
6
+ var __dirname = /* @__PURE__ */ getDirname();
4
7
 
5
8
  // src/constants.ts
6
9
  import { resolve } from "path";
@@ -11,7 +14,10 @@ var SUPPORTED_HTTP_METHODS = ["get", "post", "put", "delete", "patch", "options"
11
14
 
12
15
  // src/utils.ts
13
16
  import { resolve as resolve2 } from "path";
17
+ import { createAxle } from "@varlet/axle";
14
18
  import fse from "fs-extra";
19
+ import { tryParseJSON } from "rattail";
20
+ import { logger } from "rslog";
15
21
  import swagger from "swagger2openapi";
16
22
  import yaml from "yaml";
17
23
  function createStatusCodesByStrategy(strategy) {
@@ -46,13 +52,29 @@ function createStatusCodesByStrategy(strategy) {
46
52
  }[strategy];
47
53
  }
48
54
  async function readSchema(input) {
49
- const isYaml = input.endsWith(".yaml");
50
- const path = resolve2(CWD, input);
51
- const content = fse.readFileSync(path, "utf-8");
52
- const swaggerOrOpenapiSchema = isYaml ? yaml.parse(content) : JSON.parse(content);
55
+ const content = await readSchemaContent(input);
56
+ const jsonSchema = tryParseJSON(content);
57
+ const swaggerOrOpenapiSchema = jsonSchema ? jsonSchema : yaml.parse(content);
53
58
  const schema = swaggerOrOpenapiSchema.swagger ? (await swagger.convert(swaggerOrOpenapiSchema, {})).openapi : swaggerOrOpenapiSchema;
54
59
  return schema;
55
60
  }
61
+ async function readSchemaContent(input) {
62
+ if (isRemoteSchema(input)) {
63
+ try {
64
+ logger.info("Fetching remote schema...");
65
+ const { data } = await createAxle().get(input);
66
+ return JSON.stringify(data);
67
+ } catch {
68
+ throw new Error("Failed to fetch remote schema");
69
+ }
70
+ }
71
+ const path2 = resolve2(CWD, input);
72
+ const content = fse.readFileSync(path2, "utf-8");
73
+ return content;
74
+ }
75
+ function isRemoteSchema(path2) {
76
+ return path2.startsWith("http://") || path2.startsWith("https://");
77
+ }
56
78
  function readTemplateFile(preset = "axle") {
57
79
  if (fse.existsSync(CUSTOM_TEMPLATE_FILE)) {
58
80
  return fse.readFileSync(CUSTOM_TEMPLATE_FILE, "utf-8");
@@ -68,18 +90,27 @@ function getCliVersion() {
68
90
  function isRequiredRequestBody(value) {
69
91
  return "required" in value && value.required === true;
70
92
  }
71
- function getValidResponseMetadataItems(operation, validateStatus) {
93
+ function getRequestBodyContentType(value) {
94
+ if (!("content" in value)) {
95
+ return "";
96
+ }
97
+ return value.content["application/json"] ? "application/json" : value.content["application/x-www-form-urlencoded"] ? "application/x-www-form-urlencoded" : void 0;
98
+ }
99
+ function getResponseMetadataItems(operation, validateStatus) {
72
100
  const responses = operation.responses ?? {};
73
101
  const validStatusResults = Object.keys(responses).sort((a, b) => Number(a) - Number(b)).filter((key) => validateStatus(Number(key))).map(Number);
74
- const results = validStatusResults.map((status) => {
75
- const content = operation.responses?.[status]?.content;
76
- const mime = content?.["application/json"] ? "application/json" : content?.["*/*"] ? "*/*" : void 0;
102
+ const metadataItems = validStatusResults.map((status) => {
103
+ const content = operation.responses?.[status]?.content ?? {};
104
+ const responseContentType = findResponseContentType(content);
77
105
  return {
78
106
  status,
79
- mime
107
+ responseContentType
80
108
  };
81
- }).filter((result) => result.mime);
82
- return results;
109
+ }).filter((result) => result.responseContentType);
110
+ function findResponseContentType(content) {
111
+ return content["application/json"] ? "application/json" : content["*/*"] ? "*/*" : void 0;
112
+ }
113
+ return metadataItems;
83
114
  }
84
115
 
85
116
  export {
@@ -87,9 +118,12 @@ export {
87
118
  SUPPORTED_HTTP_METHODS,
88
119
  createStatusCodesByStrategy,
89
120
  readSchema,
121
+ readSchemaContent,
122
+ isRemoteSchema,
90
123
  readTemplateFile,
91
124
  hasQueryParameter,
92
125
  getCliVersion,
93
126
  isRequiredRequestBody,
94
- getValidResponseMetadataItems
127
+ getRequestBodyContentType,
128
+ getResponseMetadataItems
95
129
  };