openapi-sync 1.0.24 → 2.0.0

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
@@ -1,37 +1,138 @@
1
1
  # Openapi-sync
2
2
 
3
- **Openapi-sync** is a developer-friendly tool designed to keep your API up-to-date by leveraging OpenAPI schemas. It automates the generation of endpoint URIs and type definitions, including shared types, directly from your OpenAPI specification. Whether you need real-time synchronization before commits or periodic updates, openapi-sync ensures your API structure is always current and consistent. With an easy-to-use CLI, this tool integrates seamlessly into your development workflow, making API maintenance simpler and more reliable.
3
+ [![NPM Version](https://img.shields.io/npm/v/openapi-sync.svg)](https://www.npmjs.com/package/openapi-sync)
4
+ [![License](https://img.shields.io/npm/l/openapi-sync.svg)](https://github.com/akintomiwa-fisayo/openapi-sync/blob/main/LICENSE)
5
+
6
+ **Openapi-sync** is a powerful developer tool that automates the synchronization of your API documentation with your codebase using OpenAPI (formerly Swagger) specifications. It generates TypeScript types and endpoint definitions from your OpenAPI schema, ensuring your API documentation stays up-to-date with your code.
7
+
8
+ ## Features
9
+
10
+ - 🔄 Real-time API Synchronization
11
+ - 📝 Automatic Type Generation
12
+ - 🔄 Periodic API Refetching
13
+ - 📁 Configurable Output Directory
14
+ - 🔄 Customizable Naming Conventions
15
+ - 🔄 Endpoint URL Transformation
16
+ - 🔄 Schema Validation
17
+ - 🔄 CLI Integration
18
+ - 🔄 TypeScript Support
19
+ - 🔄 YAML and JSON Support
4
20
 
5
21
  ## Installation
6
22
 
7
- To install `openapi-sync`, run the following command:
23
+ Install the package using npm:
8
24
 
9
25
  ```bash
10
26
  npm install openapi-sync
11
27
  ```
12
28
 
29
+ Or use it directly via npx:
30
+
31
+ ```bash
32
+ npx openapi-sync
33
+ ```
34
+
13
35
  ## Configuration
14
36
 
15
- Create an `openapi.sync.json` file at the root of your project to configure openapi-sync. You can use the provided [`openapi.sync.sample.json`](https://github.com/akintomiwa-fisayo/openapi-sync/blob/master/openapi.sync.sample.json) as reference.
37
+ Create a `openapi.sync.json` file in your project root with the following structure:
38
+
39
+ ```json
40
+ {
41
+ "refetchInterval": 5000, // milliseconds between API refetches
42
+ "folder": "/path/to/output", // output directory for generated files
43
+ "api": {
44
+ "example1": "https://api.example.com/openapi.json",
45
+ "example2": "https://api.example.com/openapi.yaml"
46
+ },
47
+ "naming": {
48
+ "replaceWords": [
49
+ {
50
+ "replace": "Api",
51
+ "with": "",
52
+ "type": "endpoint"
53
+ }
54
+ ]
55
+ },
56
+ "endpoints": {
57
+ "value": {
58
+ "replaceWords": [
59
+ {
60
+ "replace": "/api/v\\d/",
61
+ "with": ""
62
+ }
63
+ ]
64
+ }
65
+ }
66
+ }
67
+ ```
16
68
 
17
69
  ## Usage
18
70
 
19
- To start using openapi-sync, simply run the following command in your terminal:
71
+ ### CLI Commands
20
72
 
21
73
  ```bash
74
+ # Basic usage
22
75
  npx openapi-sync
76
+
77
+ # With custom refetch interval
78
+ npx openapi-sync --refreshinterval 30000
23
79
  ```
24
80
 
25
- You can also add it as a script in your package.json for easy access:
81
+ ### Programmatic Usage
26
82
 
27
- ```json
28
- "scripts": {
29
- "api-sync": "npx openapi-sync",
30
- }
83
+ ```typescript
84
+ import { Init } from "openapi-sync";
85
+
86
+ // Initialize with custom options
87
+ await Init({
88
+ refetchInterval: 30000, // optional, defaults to config value
89
+ });
31
90
  ```
32
91
 
33
- ## Features
92
+ ## Output Generation
93
+
94
+ The tool generates:
95
+
96
+ 1. TypeScript interfaces for API endpoints
97
+ 2. Type definitions for request/response bodies
98
+ 3. Shared component types
99
+ 4. Endpoint URL constants
100
+
101
+ ## Type Generation
102
+
103
+ The tool supports:
104
+
105
+ - Primitive types (string, number, boolean, etc.)
106
+ - Complex types (objects, arrays)
107
+ - Enums
108
+ - Nullable types
109
+ - Any types
110
+ - Shared components
111
+ - Request/response bodies
112
+
113
+ ## Error Handling
114
+
115
+ The tool includes:
116
+
117
+ - Network error retries
118
+ - Schema validation
119
+ - Type generation error handling
120
+ - State persistence
121
+
122
+ ## API Documentation
123
+
124
+ For detailed API documentation, please refer to the [OpenAPI specification](https://spec.openapis.org/oas/v3.0.3).
125
+
126
+ ## License
127
+
128
+ This project is licensed under the ISC License - see the [LICENSE](LICENSE) file for details.
129
+
130
+ ## Support
131
+
132
+ For support, please open an issue in the GitHub repository.
133
+
134
+ ## Acknowledgments
34
135
 
35
- - Automated Endpoint URI Generation: Effortlessly generate endpoint URIs from your OpenAPI schema.
36
- - Type Generation: Automatically create all types defined in your API schema, including shared types, for better code consistency.
136
+ - Thanks to the OpenAPI Initiative for the OpenAPI specification
137
+ - Thanks to all contributors and users of this package
37
138
  - Flexible CLI Commands: Sync your API at any point in the development process on app start, pre-commit, or via manual triggers.
@@ -22,14 +22,10 @@ var __importStar = (this && this.__importStar) || function (mod) {
22
22
  __setModuleDefault(result, mod);
23
23
  return result;
24
24
  };
25
- var __importDefault = (this && this.__importDefault) || function (mod) {
26
- return (mod && mod.__esModule) ? mod : { "default": mod };
27
- };
28
25
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.parseSchemaToType = exports.getEndpointDetails = exports.getSharedComponentName = exports.capitalize = exports.yamlStringToJson = exports.isYamlString = exports.isJson = void 0;
26
+ exports.JSONStringify = exports.getEndpointDetails = exports.capitalize = exports.yamlStringToJson = exports.isYamlString = exports.isJson = void 0;
30
27
  const regex_1 = require("./regex");
31
28
  const yaml = __importStar(require("js-yaml"));
32
- const lodash_1 = __importDefault(require("lodash"));
33
29
  const isJson = (value) => {
34
30
  return ["object"].includes(typeof value) && !(value instanceof Blob);
35
31
  };
@@ -64,8 +60,6 @@ const capitalize = (text) => {
64
60
  return capitalizedWord;
65
61
  };
66
62
  exports.capitalize = capitalize;
67
- const getSharedComponentName = (componentName) => `IApi${(0, exports.capitalize)(componentName)}`;
68
- exports.getSharedComponentName = getSharedComponentName;
69
63
  const getEndpointDetails = (path, method) => {
70
64
  const pathParts = path.split("/");
71
65
  let name = `${(0, exports.capitalize)(method)}`;
@@ -105,114 +99,24 @@ const getEndpointDetails = (path, method) => {
105
99
  return { name, variables, pathParts };
106
100
  };
107
101
  exports.getEndpointDetails = getEndpointDetails;
108
- const parseSchemaToType = (apiDoc, schema, name, isRequired, options) => {
109
- let typeName = name ? `\t"${name}"${isRequired ? "" : "?"}: ` : "";
110
- let type = "";
111
- if (schema) {
112
- if (schema.$ref) {
113
- if (schema.$ref[0] === "#") {
114
- let pathToComponentParts = (schema.$ref || "").split("/");
115
- pathToComponentParts.shift();
116
- const pathToComponent = pathToComponentParts.join(".");
117
- const component = lodash_1.default.get(apiDoc, pathToComponent, null);
118
- if (component) {
119
- const componentName = pathToComponentParts[pathToComponentParts.length - 1];
120
- // Reference component via import instead of parsing
121
- type += `${(options === null || options === void 0 ? void 0 : options.noSharedImport) ? "" : "Shared."}${(0, exports.getSharedComponentName)(componentName)}`;
122
- // type += `${parseSchemaToType(apiDoc, component, "", isRequired)}`;
123
- }
124
- }
125
- else {
126
- type += "";
127
- //TODO $ref is a uri - use axios to fetch doc
128
- }
129
- }
130
- else if (schema.anyOf) {
131
- type += `(${schema.anyOf
132
- .map((v) => (0, exports.parseSchemaToType)(apiDoc, v, "", isRequired, options))
133
- .join("|")})`;
134
- }
135
- else if (schema.oneOf) {
136
- type += `(${schema.oneOf
137
- .map((v) => (0, exports.parseSchemaToType)(apiDoc, v, "", isRequired, options))
138
- .join("|")})`;
139
- }
140
- else if (schema.allOf) {
141
- type += `(${schema.allOf
142
- .map((v) => (0, exports.parseSchemaToType)(apiDoc, v, "", isRequired, options))
143
- .join("&")})`;
102
+ const JSONStringify = (obj) => {
103
+ let result = "{";
104
+ const keys = Object.keys(obj);
105
+ for (let i = 0; i < keys.length; i++) {
106
+ const key = keys[i];
107
+ const value = obj[key];
108
+ result += key + ": ";
109
+ if (typeof value === "object" && value !== null) {
110
+ result += (0, exports.JSONStringify)(value);
144
111
  }
145
- else if (schema.items) {
146
- type += `${(0, exports.parseSchemaToType)(apiDoc, schema.items, "", false, options)}[]`;
147
- }
148
- else if (schema.properties) {
149
- //parse object key one at a time
150
- const objKeys = Object.keys(schema.properties);
151
- const requiredKeys = schema.required || [];
152
- let typeCnt = "";
153
- objKeys.forEach((key) => {
154
- var _a;
155
- typeCnt += `${(0, exports.parseSchemaToType)(apiDoc, (_a = schema.properties) === null || _a === void 0 ? void 0 : _a[key], key, requiredKeys.includes(key), options)}`;
156
- });
157
- if (typeCnt.length > 0) {
158
- type += `{\n${typeCnt}}`;
159
- }
160
- else {
161
- type += "{[k: string]: any}";
162
- }
112
+ else {
113
+ result += value;
163
114
  }
164
- else if (schema.type) {
165
- if (schema.enum && schema.enum.length > 0) {
166
- if (schema.enum.length > 1)
167
- type += "(";
168
- type += schema.enum
169
- .map((v) => `"${v}"`)
170
- .join("|")
171
- .toString();
172
- if (schema.enum.length > 1)
173
- type += ")";
174
- }
175
- else if (["string", "integer", "number", "array", "boolean"].includes(schema.type)) {
176
- if (["integer", "number"].includes(schema.type)) {
177
- type += `number`;
178
- }
179
- else if (schema.type === "array") {
180
- //Since we would have already parsed the arrays keys above "schema.items" if it exists
181
- type += "any[]";
182
- /* if (schema.items) {
183
- type += `${parseSchemaToType(
184
- apiDoc,
185
- schema.items,
186
- "",
187
- false,
188
- options
189
- )}[]`;
190
- } else {
191
- type += "any[]";
192
- } */
193
- }
194
- else {
195
- type += schema.type;
196
- }
197
- }
198
- else if (schema.type === "object") {
199
- //Since we would have already parsed the object keys above "schema.properties" if it exists
200
- if (schema.additionalProperties) {
201
- type += `{[k: string]: ${(0, exports.parseSchemaToType)(apiDoc, schema.additionalProperties, "", true, options) || "any"}}`;
202
- }
203
- else {
204
- type += "{[k: string]: any}";
205
- }
206
- }
115
+ if (i < keys.length - 1) {
116
+ result += ", ";
207
117
  }
208
118
  }
209
- else {
210
- //Default type to string if no schema provided
211
- type = "string";
212
- }
213
- const nullable = (schema === null || schema === void 0 ? void 0 : schema.nullable) ? " | null" : "";
214
- return type.length > 0
215
- ? `${typeName}${type}${nullable}${name ? ";\n" : ""}`
216
- : "";
119
+ result += "}";
120
+ return result;
217
121
  };
218
- exports.parseSchemaToType = parseSchemaToType;
122
+ exports.JSONStringify = JSONStringify;
@@ -14,8 +14,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const fs_1 = __importDefault(require("fs"));
16
16
  const path_1 = __importDefault(require("path"));
17
+ const lodash_1 = __importDefault(require("lodash"));
17
18
  const helpers_1 = require("./components/helpers");
18
- const lodash_1 = require("lodash");
19
+ const lodash_2 = require("lodash");
19
20
  const axios_1 = __importDefault(require("axios"));
20
21
  const axios_retry_1 = __importDefault(require("axios-retry"));
21
22
  const openapi_core_1 = require("@redocly/openapi-core");
@@ -37,7 +38,8 @@ const apiClient = axios_1.default.create({
37
38
  return retryCount * 1000; // Exponential back-off: 1s, 2s, 3s, etc.
38
39
  },
39
40
  });
40
- const OpenapiSync = (apiUrl, apiName, refetchInterval) => __awaiter(void 0, void 0, void 0, function* () {
41
+ const OpenapiSync = (apiUrl, apiName, config, refetchInterval) => __awaiter(void 0, void 0, void 0, function* () {
42
+ var _a, _b, _c, _d;
41
43
  const specResponse = yield apiClient.get(apiUrl);
42
44
  const redoclyConfig = yield (0, openapi_core_1.createConfig)({
43
45
  extends: ["minimal"],
@@ -49,10 +51,160 @@ const OpenapiSync = (apiUrl, apiName, refetchInterval) => __awaiter(void 0, void
49
51
  source,
50
52
  config: redoclyConfig,
51
53
  });
52
- // Load config file
53
- const config = require(path_1.default.join(rootUsingCwd, "openapi.sync.json"));
54
54
  const folderPath = path_1.default.join(config.folder || "", apiName);
55
55
  const spec = lintResults.bundle.parsed;
56
+ const typePrefix = typeof ((_b = (_a = config === null || config === void 0 ? void 0 : config.types) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.prefix) === "string"
57
+ ? config.types.name.prefix
58
+ : "I";
59
+ const endpointPrefix = typeof ((_d = (_c = config === null || config === void 0 ? void 0 : config.endpoints) === null || _c === void 0 ? void 0 : _c.name) === null || _d === void 0 ? void 0 : _d.prefix) === "string"
60
+ ? config.endpoints.name.prefix
61
+ : "";
62
+ const getSharedComponentName = (componentName, componentType) => {
63
+ var _a, _b;
64
+ if ((_b = (_a = config === null || config === void 0 ? void 0 : config.types) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.format) {
65
+ const formattedName = config.types.name.format("shared", {
66
+ name: componentName,
67
+ });
68
+ if (formattedName)
69
+ return `${typePrefix}${formattedName}`;
70
+ }
71
+ return `${typePrefix}${(0, helpers_1.capitalize)(componentName)}`;
72
+ };
73
+ const parseSchemaToType = (apiDoc, schema, name, isRequired, options) => {
74
+ let overrideName = "";
75
+ let componentName = "";
76
+ let type = "";
77
+ if (schema) {
78
+ if (schema.$ref) {
79
+ if (schema.$ref[0] === "#") {
80
+ let pathToComponentParts = (schema.$ref || "").split("/");
81
+ pathToComponentParts.shift();
82
+ const partsClone = [...pathToComponentParts];
83
+ partsClone.pop();
84
+ const pathToComponent = pathToComponentParts;
85
+ const component = lodash_1.default.get(apiDoc, pathToComponent, null);
86
+ if (component) {
87
+ if (component === null || component === void 0 ? void 0 : component.name) {
88
+ overrideName = component.name;
89
+ }
90
+ componentName =
91
+ pathToComponentParts[pathToComponentParts.length - 1];
92
+ let name = getSharedComponentName(componentName);
93
+ if (name.includes(".")) {
94
+ const nameParts = name.split(".");
95
+ name = nameParts
96
+ .map((part, i) => {
97
+ if (i === 0) {
98
+ return part;
99
+ }
100
+ return `["${part}"]`;
101
+ })
102
+ .join("");
103
+ }
104
+ // Reference component via import instead of parsing
105
+ type += `${(options === null || options === void 0 ? void 0 : options.noSharedImport) ? "" : "Shared."}${name}`;
106
+ // type += `${parseSchemaToType(apiDoc, component, "", isRequired)}`;
107
+ }
108
+ }
109
+ else {
110
+ type += "";
111
+ //TODO $ref is a uri - use axios to fetch doc
112
+ }
113
+ }
114
+ else if (schema.anyOf) {
115
+ type += `(${schema.anyOf
116
+ .map((v) => parseSchemaToType(apiDoc, v, "", isRequired, options))
117
+ .join("|")})`;
118
+ }
119
+ else if (schema.oneOf) {
120
+ type += `(${schema.oneOf
121
+ .map((v) => parseSchemaToType(apiDoc, v, "", isRequired, options))
122
+ .join("|")})`;
123
+ }
124
+ else if (schema.allOf) {
125
+ type += `(${schema.allOf
126
+ .map((v) => parseSchemaToType(apiDoc, v, "", isRequired, options))
127
+ .join("&")})`;
128
+ }
129
+ else if (schema.items) {
130
+ type += `${parseSchemaToType(apiDoc, schema.items, "", false, options)}[]`;
131
+ }
132
+ else if (schema.properties) {
133
+ //parse object key one at a time
134
+ const objKeys = Object.keys(schema.properties);
135
+ const requiredKeys = schema.required || [];
136
+ let typeCnt = "";
137
+ objKeys.forEach((key) => {
138
+ var _a;
139
+ typeCnt += `${parseSchemaToType(apiDoc, (_a = schema.properties) === null || _a === void 0 ? void 0 : _a[key], key, requiredKeys.includes(key), options)}`;
140
+ });
141
+ if (typeCnt.length > 0) {
142
+ type += `{\n${typeCnt}}`;
143
+ }
144
+ else {
145
+ type += "{[k: string]: any}";
146
+ }
147
+ }
148
+ else if (schema.enum && schema.enum.length > 0) {
149
+ if (schema.enum.length > 1)
150
+ type += "(";
151
+ schema.enum.forEach((v) => {
152
+ let val = JSON.stringify(v);
153
+ if (val)
154
+ type += `|${val}`;
155
+ });
156
+ if (schema.enum.length > 1)
157
+ type += ")";
158
+ }
159
+ else if (schema.type) {
160
+ if (["string", "integer", "number", "array", "boolean"].includes(schema.type)) {
161
+ if (["integer", "number"].includes(schema.type)) {
162
+ type += `number`;
163
+ }
164
+ else if (schema.type === "array") {
165
+ //Since we would have already parsed the arrays keys above "schema.items" if it exists
166
+ type += "any[]";
167
+ /* if (schema.items) {
168
+ type += `${parseSchemaToType(
169
+ apiDoc,
170
+ schema.items,
171
+ "",
172
+ false,
173
+ options
174
+ )}[]`;
175
+ } else {
176
+ type += "any[]";
177
+ } */
178
+ }
179
+ else {
180
+ type += schema.type;
181
+ }
182
+ }
183
+ else if (schema.type === "object") {
184
+ //Since we would have already parsed the object keys above "schema.properties" if it exists
185
+ if (schema.additionalProperties) {
186
+ type += `{[k: string]: ${parseSchemaToType(apiDoc, schema.additionalProperties, "", true, options) || "any"}}`;
187
+ }
188
+ else {
189
+ type += "{[k: string]: any}";
190
+ }
191
+ }
192
+ }
193
+ }
194
+ else {
195
+ //Default type to string if no schema provided
196
+ type = "string";
197
+ }
198
+ let _name = overrideName || name;
199
+ if ((options === null || options === void 0 ? void 0 : options.useComponentName) && !_name) {
200
+ _name = componentName;
201
+ }
202
+ let typeName = _name ? `\t"${_name}"${isRequired ? "" : "?"}: ` : "";
203
+ const nullable = (schema === null || schema === void 0 ? void 0 : schema.nullable) ? " | null" : "";
204
+ return type.length > 0
205
+ ? `${typeName}${type}${nullable}${_name ? ";\n" : ""}`
206
+ : "";
207
+ };
56
208
  // auto update only on dev
57
209
  if (refetchInterval && !isNaN(refetchInterval) && refetchInterval > 0) {
58
210
  if (!(process.env.NODE_ENV &&
@@ -61,28 +213,84 @@ const OpenapiSync = (apiUrl, apiName, refetchInterval) => __awaiter(void 0, void
61
213
  if (fetchTimeout[apiName])
62
214
  clearTimeout(fetchTimeout[apiName]);
63
215
  // set next request timeout
64
- fetchTimeout[apiName] = setTimeout(() => OpenapiSync(apiUrl, apiName, refetchInterval), refetchInterval);
216
+ fetchTimeout[apiName] = setTimeout(() => OpenapiSync(apiUrl, apiName, config, refetchInterval), refetchInterval);
65
217
  }
66
218
  }
67
219
  // compare new spec with old spec, continuing only if spec it different
68
220
  const prevSpec = (0, state_1.getState)(apiName);
69
- if ((0, lodash_1.isEqual)(prevSpec, spec))
221
+ if ((0, lodash_2.isEqual)(prevSpec, spec))
70
222
  return;
71
223
  (0, state_1.setState)(apiName, spec);
72
224
  let endpointsFileContent = "";
73
225
  let typesFileContent = "";
74
- let sharedTypesFileContent = "";
75
- if (spec.components && spec.components.schemas) {
76
- // Create components (shared) types
77
- const components = spec.components.schemas;
78
- const contentKeys = Object.keys(components);
79
- // only need 1 schema so will us the first schema provided
80
- contentKeys.forEach((key) => {
81
- const typeCnt = `${(0, helpers_1.parseSchemaToType)(spec, components[key], "", true, {
82
- noSharedImport: true,
83
- })}`;
84
- if (typeCnt) {
85
- sharedTypesFileContent += `export type ${(0, helpers_1.getSharedComponentName)(key)} = ${typeCnt};\n`;
226
+ let sharedTypesFileContent = {};
227
+ if (spec.components) {
228
+ Object.keys(spec.components).forEach((key) => {
229
+ if ([
230
+ "schemas",
231
+ "responses",
232
+ "parameters",
233
+ "examples",
234
+ "requestBodies",
235
+ "headers",
236
+ "links",
237
+ "callbacks",
238
+ ].includes(key)) {
239
+ // Create components (shared) types
240
+ const components = spec.components[key];
241
+ const componentInterfaces = {};
242
+ const contentKeys = Object.keys(components);
243
+ // only need 1 schema so will us the first schema provided
244
+ contentKeys.forEach((contentKey) => {
245
+ var _a;
246
+ /* const schema = (() => {
247
+ switch (key) {
248
+ case "parameters":
249
+ return components[contentKey].schema;
250
+ default:
251
+ return components[contentKey];
252
+ }
253
+ })() as IOpenApSchemaSpec; */
254
+ const schema = (((_a = components[contentKey]) === null || _a === void 0 ? void 0 : _a.schema)
255
+ ? components[contentKey].schema
256
+ : components[contentKey]);
257
+ const typeCnt = `${parseSchemaToType(spec, schema, "", true, {
258
+ noSharedImport: true,
259
+ useComponentName: ["parameters"].includes(key),
260
+ })}`;
261
+ if (typeCnt) {
262
+ const parts = contentKey.split(".");
263
+ let currentLevel = componentInterfaces;
264
+ // Navigate or create the nested structure
265
+ for (let i = 0; i < parts.length; i++) {
266
+ const part = parts[i];
267
+ if (i < parts.length - 1) {
268
+ // If it's not the last part, create a nested object if it doesn't exist
269
+ if (!(part in currentLevel)) {
270
+ currentLevel[part] = {}; //<== This rely on js ability to assign value to origianl object by reference, so this assignment will be reflected in componentInterfaces
271
+ }
272
+ currentLevel = currentLevel[part]; //<== This rely on js ability to assign value to origianl object by reference, so this assignment will be reflected in componentInterfaces
273
+ }
274
+ else {
275
+ // This is the last part, assign the original schema value
276
+ currentLevel[part] = typeCnt; //<== This rely on js ability to assign value to origianl object by reference, so this assignment will be reflected in componentInterfaces
277
+ }
278
+ }
279
+ }
280
+ });
281
+ // Generate TypeScript interfaces for each component
282
+ Object.keys(componentInterfaces).forEach((key) => {
283
+ var _a;
284
+ const name = getSharedComponentName(key);
285
+ const cnt = componentInterfaces[key];
286
+ sharedTypesFileContent[key] =
287
+ ((_a = sharedTypesFileContent[key]) !== null && _a !== void 0 ? _a : "") +
288
+ "export type " +
289
+ name +
290
+ " = " +
291
+ (typeof cnt === "string" ? cnt : (0, helpers_1.JSONStringify)(cnt)) +
292
+ ";\n";
293
+ });
86
294
  }
87
295
  });
88
296
  }
@@ -92,17 +300,17 @@ const OpenapiSync = (apiUrl, apiName, refetchInterval) => __awaiter(void 0, void
92
300
  const contentKeys = Object.keys(requestBody.content);
93
301
  // only need 1 schema so will us the first schema provided
94
302
  if (contentKeys[0] && requestBody.content[contentKeys[0]].schema) {
95
- typeCnt += `${(0, helpers_1.parseSchemaToType)(spec, requestBody.content[contentKeys[0]].schema, "")}`;
303
+ typeCnt += `${parseSchemaToType(spec, requestBody.content[contentKeys[0]].schema, "")}`;
96
304
  }
97
305
  }
98
306
  return typeCnt;
99
307
  };
100
308
  const treatEndpointUrl = (endpointUrl) => {
101
- var _a, _b;
309
+ var _a, _b, _c, _d, _e;
102
310
  if (((_b = (_a = config === null || config === void 0 ? void 0 : config.endpoints) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.replaceWords) &&
103
311
  Array.isArray(config.endpoints.value.replaceWords)) {
104
312
  let newEndpointUrl = endpointUrl;
105
- config.endpoints.value.replaceWords.forEach((replaceWord, indx) => {
313
+ (_e = (_d = (_c = config === null || config === void 0 ? void 0 : config.endpoints) === null || _c === void 0 ? void 0 : _c.value) === null || _d === void 0 ? void 0 : _d.replaceWords) === null || _e === void 0 ? void 0 : _e.forEach((replaceWord, indx) => {
106
314
  const regexp = new RegExp(replaceWord.replace, "g");
107
315
  newEndpointUrl = newEndpointUrl.replace(regexp, replaceWord.with || "");
108
316
  });
@@ -114,9 +322,11 @@ const OpenapiSync = (apiUrl, apiName, refetchInterval) => __awaiter(void 0, void
114
322
  };
115
323
  Object.keys(spec.paths || {}).forEach((endpointPath) => {
116
324
  const endpointSpec = spec.paths[endpointPath];
325
+ // console.log("Endpoint Path:", { endpointPath, endpointSpec });
117
326
  const endpointMethods = Object.keys(endpointSpec);
118
- endpointMethods.forEach((method) => {
119
- var _a, _b, _c, _d, _e, _f;
327
+ endpointMethods.forEach((_method) => {
328
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
329
+ const method = _method;
120
330
  const endpoint = (0, helpers_1.getEndpointDetails)(endpointPath, method);
121
331
  const endpointUrlTxt = endpoint.pathParts
122
332
  .map((part) => {
@@ -145,38 +355,85 @@ const OpenapiSync = (apiUrl, apiName, refetchInterval) => __awaiter(void 0, void
145
355
  }
146
356
  //treat endpoint url
147
357
  endpointUrl = treatEndpointUrl(endpointUrl);
358
+ let name = `${endpoint.name}`;
359
+ if ((_b = (_a = config === null || config === void 0 ? void 0 : config.endpoints) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.format) {
360
+ const formattedName = config.endpoints.name.format({
361
+ method,
362
+ path: endpointPath,
363
+ summary: (_c = endpointSpec[method]) === null || _c === void 0 ? void 0 : _c.summary,
364
+ });
365
+ if (formattedName)
366
+ name = formattedName;
367
+ }
148
368
  // Add the endpoint url
149
- endpointsFileContent += `export const ${endpoint.name} = ${endpointUrl};
369
+ endpointsFileContent += `export const ${endpointPrefix}${name} = ${endpointUrl};
150
370
  `;
151
- if ((_a = endpointSpec[method]) === null || _a === void 0 ? void 0 : _a.parameters) {
371
+ if ((_d = endpointSpec[method]) === null || _d === void 0 ? void 0 : _d.parameters) {
152
372
  // create query parameters types
153
- const parameters = (_b = endpointSpec[method]) === null || _b === void 0 ? void 0 : _b.parameters;
373
+ const parameters = (_e = endpointSpec[method]) === null || _e === void 0 ? void 0 : _e.parameters;
154
374
  let typeCnt = "";
155
- parameters.forEach((param) => {
156
- if (param.in === "query" && param.name) {
157
- typeCnt += `${(0, helpers_1.parseSchemaToType)(spec, param.schema, param.name, param.required)}`;
375
+ parameters.forEach((param, i) => {
376
+ if (param.$ref || (param.in === "query" && param.name)) {
377
+ typeCnt += `${parseSchemaToType(spec, param.$ref ? param : param.schema, param.name || "", param.required)}`;
158
378
  }
159
379
  });
160
380
  if (typeCnt) {
161
- typesFileContent += `export type I${endpoint.name}Query = {\n${typeCnt}};\n`;
381
+ let name = `${endpoint.name}Query`;
382
+ if ((_g = (_f = config === null || config === void 0 ? void 0 : config.types) === null || _f === void 0 ? void 0 : _f.name) === null || _g === void 0 ? void 0 : _g.format) {
383
+ const formattedName = config.types.name.format("endpoint", {
384
+ code: "",
385
+ type: "query",
386
+ method,
387
+ path: endpointPath,
388
+ summary: (_h = endpointSpec[method]) === null || _h === void 0 ? void 0 : _h.summary,
389
+ });
390
+ if (formattedName)
391
+ name = formattedName;
392
+ }
393
+ typesFileContent += `export type ${typePrefix}${name} = {\n${typeCnt}};\n`;
162
394
  }
163
395
  }
164
- if ((_c = endpointSpec[method]) === null || _c === void 0 ? void 0 : _c.requestBody) {
396
+ if ((_j = endpointSpec[method]) === null || _j === void 0 ? void 0 : _j.requestBody) {
165
397
  //create requestBody types
166
- const requestBody = (_d = endpointSpec[method]) === null || _d === void 0 ? void 0 : _d.requestBody;
398
+ const requestBody = (_k = endpointSpec[method]) === null || _k === void 0 ? void 0 : _k.requestBody;
167
399
  let typeCnt = getBodySchemaType(requestBody);
168
400
  if (typeCnt) {
169
- typesFileContent += `export type I${endpoint.name}DTO = ${typeCnt};\n`;
401
+ let name = `${endpoint.name}DTO`;
402
+ if ((_m = (_l = config === null || config === void 0 ? void 0 : config.types) === null || _l === void 0 ? void 0 : _l.name) === null || _m === void 0 ? void 0 : _m.format) {
403
+ const formattedName = config.types.name.format("endpoint", {
404
+ code: "",
405
+ type: "dto",
406
+ method,
407
+ path: endpointPath,
408
+ summary: (_o = endpointSpec[method]) === null || _o === void 0 ? void 0 : _o.summary,
409
+ });
410
+ if (formattedName)
411
+ name = formattedName;
412
+ }
413
+ typesFileContent += `export type ${typePrefix}${name} = ${typeCnt};\n`;
170
414
  }
171
415
  }
172
- if ((_e = endpointSpec[method]) === null || _e === void 0 ? void 0 : _e.responses) {
416
+ if ((_p = endpointSpec[method]) === null || _p === void 0 ? void 0 : _p.responses) {
173
417
  // create request response types
174
- const responses = (_f = endpointSpec[method]) === null || _f === void 0 ? void 0 : _f.responses;
418
+ const responses = (_q = endpointSpec[method]) === null || _q === void 0 ? void 0 : _q.responses;
175
419
  const resCodes = Object.keys(responses);
176
420
  resCodes.forEach((code) => {
421
+ var _a, _b, _c;
177
422
  let typeCnt = getBodySchemaType(responses[code]);
178
423
  if (typeCnt) {
179
- typesFileContent += `export type I${endpoint.name}${code}Response = ${typeCnt};\n`;
424
+ let name = `${endpoint.name}${code}Response`;
425
+ if ((_b = (_a = config === null || config === void 0 ? void 0 : config.types) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.format) {
426
+ const formattedName = config.types.name.format("endpoint", {
427
+ code,
428
+ type: "response",
429
+ method,
430
+ path: endpointPath,
431
+ summary: (_c = endpointSpec[method]) === null || _c === void 0 ? void 0 : _c.summary,
432
+ });
433
+ if (formattedName)
434
+ name = formattedName;
435
+ }
436
+ typesFileContent += `export type ${typePrefix}${name} = ${typeCnt};\n`;
180
437
  }
181
438
  });
182
439
  }
@@ -187,21 +444,21 @@ const OpenapiSync = (apiUrl, apiName, refetchInterval) => __awaiter(void 0, void
187
444
  yield fs_1.default.promises.mkdir(path_1.default.dirname(endpointsFilePath), { recursive: true });
188
445
  // Create the file asynchronously
189
446
  yield fs_1.default.promises.writeFile(endpointsFilePath, endpointsFileContent);
190
- if (sharedTypesFileContent.length > 0) {
447
+ if (Object.values(sharedTypesFileContent).length > 0) {
191
448
  // Create the necessary directories
192
449
  const sharedTypesFilePath = path_1.default.join(rootUsingCwd, folderPath, "types", "shared.ts");
193
450
  yield fs_1.default.promises.mkdir(path_1.default.dirname(sharedTypesFilePath), {
194
451
  recursive: true,
195
452
  });
196
453
  // Create the file asynchronously
197
- yield fs_1.default.promises.writeFile(sharedTypesFilePath, sharedTypesFileContent);
454
+ yield fs_1.default.promises.writeFile(sharedTypesFilePath, Object.values(sharedTypesFileContent).join("\n"));
198
455
  }
199
456
  if (typesFileContent.length > 0) {
200
457
  // Create the necessary directories
201
458
  const typesFilePath = path_1.default.join(rootUsingCwd, folderPath, "types", "index.ts");
202
459
  yield fs_1.default.promises.mkdir(path_1.default.dirname(typesFilePath), { recursive: true });
203
460
  // Create the file asynchronously
204
- yield fs_1.default.promises.writeFile(typesFilePath, `${sharedTypesFileContent.length > 0
461
+ yield fs_1.default.promises.writeFile(typesFilePath, `${Object.values(sharedTypesFileContent).length > 0
205
462
  ? `import * as Shared from "./shared";\n\n`
206
463
  : ""}${typesFileContent}`);
207
464
  }
package/dist/index.js CHANGED
@@ -21,7 +21,20 @@ dotenv_1.default.config();
21
21
  const rootUsingCwd = process.cwd();
22
22
  const Init = (options) => __awaiter(void 0, void 0, void 0, function* () {
23
23
  // Load config file
24
- const config = require(path_1.default.join(rootUsingCwd, "openapi.sync.json"));
24
+ let configJS, configJson;
25
+ try {
26
+ configJS = require(path_1.default.join(rootUsingCwd, "openapi.sync.js"));
27
+ }
28
+ catch (e) {
29
+ // console.log(e);
30
+ }
31
+ try {
32
+ configJson = require(path_1.default.join(rootUsingCwd, "openapi.sync.json"));
33
+ }
34
+ catch (e) {
35
+ // console.log(e);
36
+ }
37
+ const config = configJS || configJson;
25
38
  const apiNames = Object.keys(config.api);
26
39
  const refetchInterval = options &&
27
40
  "refetchInterval" in options &&
@@ -32,7 +45,7 @@ const Init = (options) => __awaiter(void 0, void 0, void 0, function* () {
32
45
  for (let i = 0; i < apiNames.length; i += 1) {
33
46
  const apiName = apiNames[i];
34
47
  const apiUrl = config.api[apiName];
35
- (0, Openapi_sync_1.default)(apiUrl, apiName, refetchInterval);
48
+ (0, Openapi_sync_1.default)(apiUrl, apiName, config, refetchInterval);
36
49
  }
37
50
  });
38
51
  exports.Init = Init;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openapi-sync",
3
- "version": "1.0.24",
3
+ "version": "2.0.0",
4
4
  "description": "A developer-friendly tool designed to keep your API up-to-date by leveraging OpenAPI schemas. It automates the generation of endpoint URIs and type definitions, including shared types, directly from your OpenAPI specification.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -24,6 +24,7 @@
24
24
  "bin",
25
25
  "dist",
26
26
  "db.json",
27
+ "types.ts",
27
28
  "LICENSE",
28
29
  "README.md",
29
30
  "package.json"
@@ -31,7 +32,7 @@
31
32
  "scripts": {
32
33
  "test": "echo \"Error: no test specified\"",
33
34
  "build": "tsc",
34
- "publish-package": "npm run build && npm version patch && npm publish",
35
+ "publish-package": "npm run build && npm version major && npm publish",
35
36
  "start": "npm run build && openapi-sync"
36
37
  },
37
38
  "author": "P-Technologies",
package/types.ts ADDED
@@ -0,0 +1,93 @@
1
+ import { Method } from "axios";
2
+
3
+ export type IOpenApiSpec = Record<"openapi", string> & Record<string, any>;
4
+
5
+ export type IOpenApSchemaSpec = {
6
+ nullable?: boolean;
7
+ type: "string" | "integer" | "number" | "array" | "object" | "boolean";
8
+ example?: any;
9
+ enum?: string[];
10
+ format?: string;
11
+ items?: IOpenApSchemaSpec;
12
+ required?: string[];
13
+ $ref?: string;
14
+ properties?: Record<string, IOpenApSchemaSpec>;
15
+ additionalProperties?: IOpenApSchemaSpec;
16
+ anyOf?: IOpenApSchemaSpec[];
17
+ oneOf?: IOpenApSchemaSpec[];
18
+ allOf?: IOpenApSchemaSpec[];
19
+ };
20
+
21
+ export type IOpenApiParameterSpec = {
22
+ $ref?: string;
23
+ name: string;
24
+ in: string;
25
+ enum?: string[];
26
+ description?: string;
27
+ required?: boolean;
28
+ deprecated?: boolean;
29
+ allowEmptyValue?: boolean;
30
+ style?: string;
31
+ explode?: boolean;
32
+ allowReserved?: boolean;
33
+ schema?: IOpenApSchemaSpec;
34
+ example?: any;
35
+ examples?: any[];
36
+ };
37
+
38
+ export type IOpenApiMediaTypeSpec = {
39
+ schema?: IOpenApSchemaSpec;
40
+ example?: any;
41
+ examples?: any[];
42
+ encoding?: any;
43
+ };
44
+
45
+ export type IOpenApiRequestBodySpec = {
46
+ description?: string;
47
+ required?: boolean;
48
+ content: Record<string, IOpenApiMediaTypeSpec>;
49
+ };
50
+
51
+ export type IOpenApiResponseSpec = Record<string, IOpenApiRequestBodySpec>;
52
+
53
+ export type IConfigReplaceWord = {
54
+ /** string and regular expression as a string*/
55
+ replace: string;
56
+ with: string;
57
+ type?: "endpoint" | "type";
58
+ };
59
+
60
+ export type IConfig = {
61
+ refetchInterval?: number;
62
+ folder?: string;
63
+ api: Record<string, string>;
64
+ types?: {
65
+ name?: {
66
+ prefix?: string;
67
+ format?: (
68
+ source: "shared" | "endpoint",
69
+ data: {
70
+ name?: string;
71
+ type?: "response" | "dto" | "query";
72
+ code?: string;
73
+ method?: Method;
74
+ path?: string;
75
+ summary?: string;
76
+ }
77
+ ) => string | null | undefined;
78
+ };
79
+ };
80
+ endpoints?: {
81
+ value?: {
82
+ replaceWords?: IConfigReplaceWord[];
83
+ };
84
+ name?: {
85
+ format?: (data: {
86
+ method: Method;
87
+ path: string;
88
+ summary: string;
89
+ }) => string | null;
90
+ prefix?: string;
91
+ };
92
+ };
93
+ };
File without changes