openapi-sync 1.0.0 → 1.0.1

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.
@@ -0,0 +1,150 @@
1
+ import { IOpenApiSpec, IOpenApSchemaSpec } from "../types";
2
+ import { variableNameChar } from "./regex";
3
+ import propertyExpr from "property-expr";
4
+ import * as yaml from "js-yaml";
5
+
6
+ export const isJson = (value: any) => {
7
+ return ["object"].includes(typeof value) && !(value instanceof Blob);
8
+ };
9
+ export const isYamlString = (fileContent: string) => {
10
+ try {
11
+ yaml.load(fileContent);
12
+ return true;
13
+ } catch (en) {
14
+ const e = en as any;
15
+ if (e instanceof yaml.YAMLException) {
16
+ return false;
17
+ } else {
18
+ throw e;
19
+ }
20
+ }
21
+ };
22
+
23
+ export const yamlStringToJson = (fileContent: string) => {
24
+ if (isYamlString(fileContent)) {
25
+ const content = yaml.load(fileContent);
26
+
27
+ const jsonString = JSON.stringify(content, null, 2);
28
+ const json = JSON.parse(jsonString);
29
+ return json;
30
+ }
31
+ };
32
+
33
+ export const capitalize = (text: string) => {
34
+ const capitalizedWord =
35
+ text.substring(0, 1).toUpperCase() + text.substring(1);
36
+ return capitalizedWord;
37
+ };
38
+
39
+ export const getSharedComponentName = (componentName: string) =>
40
+ `IApi${capitalize(componentName)}`;
41
+
42
+ export const getEndpointDetails = (path: string, method: string) => {
43
+ const pathParts = path.split("/");
44
+ let name = `${capitalize(method)}`;
45
+ const variables: string[] = [];
46
+ pathParts.forEach((part) => {
47
+ // check if part is a variable
48
+ if (part[0] === "{" && part[part.length - 1] === "}") {
49
+ const s = part.replace(/{/, "").replace(/}/, "");
50
+ variables.push(s);
51
+ part = `$${s}`;
52
+ }
53
+
54
+ // parse to variable name
55
+ let partVal = "";
56
+ part.split("").forEach((char) => {
57
+ let c = char;
58
+ if (!variableNameChar.test(char)) c = "/";
59
+ partVal += c;
60
+ });
61
+
62
+ partVal.split("/").forEach((val) => {
63
+ name += capitalize(val);
64
+ });
65
+ });
66
+
67
+ return { name, variables, pathParts };
68
+ };
69
+
70
+ export const parseSchemaToType = (
71
+ apiDoc: IOpenApiSpec,
72
+ schema: IOpenApSchemaSpec,
73
+ name: string,
74
+ isRequired?: boolean,
75
+ options?: {
76
+ noSharedImport?: boolean;
77
+ }
78
+ ) => {
79
+ let typeName = name ? `\t${name}${isRequired ? "" : "?"}: ` : "";
80
+ let type = "";
81
+ if (schema.$ref) {
82
+ if (schema.$ref[0] === "#") {
83
+ let pathToComponentParts = (schema.$ref || "").split("/");
84
+ pathToComponentParts.shift();
85
+ const pathToComponent = pathToComponentParts.join(".");
86
+ const component = propertyExpr.getter(pathToComponent)(
87
+ apiDoc
88
+ ) as IOpenApSchemaSpec;
89
+
90
+ if (component) {
91
+ const componentName =
92
+ pathToComponentParts[pathToComponentParts.length - 1];
93
+ // Reference component via import instead of parsing
94
+ type += `${
95
+ options?.noSharedImport ? "" : "Shared."
96
+ }${getSharedComponentName(componentName)}`;
97
+ // type += `${parseSchemaToType(apiDoc, component, "", isRequired)}`;
98
+ }
99
+ } else {
100
+ type += "";
101
+ //TODO $ref is a uri - use axios to fetch doc
102
+ }
103
+ } else if (schema.type) {
104
+ if (schema.enum && schema.enum.length > 0) {
105
+ if (schema.enum.length > 1) type += "(";
106
+ type += schema.enum
107
+ .map((v) => `"${v}"`)
108
+ .join("|")
109
+ .toString();
110
+ if (schema.enum.length > 1) type += ")";
111
+ } else if (["string", "integer", "number", "array"].includes(schema.type)) {
112
+ if (schema.type === "string") {
113
+ type += `string`;
114
+ } else if (["integer", "number"].includes(schema.type)) {
115
+ type += `number`;
116
+ } else if (schema.type === "array") {
117
+ if (schema.items) {
118
+ type += `${parseSchemaToType(
119
+ apiDoc,
120
+ schema.items,
121
+ "",
122
+ false,
123
+ options
124
+ )}[]`;
125
+ } else {
126
+ type += "any[]";
127
+ }
128
+ }
129
+ } else if (schema.type === "object") {
130
+ if (schema.properties) {
131
+ //parse object key one at a time
132
+ const objKeys = Object.keys(schema.properties);
133
+ const requiredKeys = schema.required || [];
134
+ type += "{\n";
135
+ objKeys.forEach((key) => {
136
+ type += `${parseSchemaToType(
137
+ apiDoc,
138
+ schema.properties?.[key] as IOpenApSchemaSpec,
139
+ key,
140
+ requiredKeys.includes(key),
141
+ options
142
+ )}`;
143
+ });
144
+ type += "}";
145
+ }
146
+ }
147
+ }
148
+
149
+ return type.length > 0 ? `${typeName}${type}${name ? ";\n" : ""}` : "";
150
+ };
@@ -0,0 +1,2 @@
1
+ export const variableName = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
2
+ export const variableNameChar = /[A-Za-z0-9_$]/;
@@ -0,0 +1,252 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import {
4
+ getEndpointDetails,
5
+ getSharedComponentName,
6
+ isJson,
7
+ isYamlString,
8
+ parseSchemaToType,
9
+ yamlStringToJson,
10
+ } from "./components/helpers";
11
+ import {
12
+ IOpenApiMediaTypeSpec,
13
+ IOpenApiParameterSpec,
14
+ IOpenApiRequestBodySpec,
15
+ IOpenApiResponseSpec,
16
+ IOpenApiSpec,
17
+ IOpenApSchemaSpec,
18
+ } from "./types";
19
+ import { isEqual } from "lodash";
20
+ import axios from "axios";
21
+ import axiosRetry from "axios-retry";
22
+ import { bundleFromString, createConfig } from "@redocly/openapi-core";
23
+ import { getState, setState } from "./state";
24
+
25
+ const rootUsingCwd = process.cwd();
26
+ let fetchTimeout: null | NodeJS.Timeout = null;
27
+
28
+ // Create an Axios instance
29
+ const apiClient = axios.create({
30
+ timeout: 30000, // Timeout after 30 seconds
31
+ });
32
+
33
+ // Configure axios-retry
34
+ axiosRetry(apiClient, {
35
+ retries: 20, // Number of retry attempts
36
+ retryCondition: (error) => {
37
+ // Retry on network error
38
+ return (
39
+ error.code === "ECONNABORTED" || error.message.includes("Network Error")
40
+ );
41
+ },
42
+ retryDelay: (retryCount) => {
43
+ return retryCount * 1000; // Exponential back-off: 1s, 2s, 3s, etc.
44
+ },
45
+ });
46
+
47
+ const OpenapiSync = async (apiUrl: string, apiName: string) => {
48
+ const specResponse = await apiClient.get(apiUrl);
49
+
50
+ const redoclyConfig = await createConfig({
51
+ extends: ["minimal"],
52
+ });
53
+
54
+ const source = JSON.stringify(
55
+ isJson(specResponse.data)
56
+ ? specResponse.data
57
+ : yamlStringToJson(specResponse.data)
58
+ );
59
+
60
+ const lintResults = await bundleFromString({
61
+ source,
62
+ config: redoclyConfig,
63
+ });
64
+
65
+ // Load config file
66
+ const config = require(path.join(rootUsingCwd, "openapi.sync.json"));
67
+ const folderPath = path.join(config.folder || "", apiName);
68
+
69
+ const spec: IOpenApiSpec = lintResults.bundle.parsed;
70
+ // auto update only on dev
71
+ if (
72
+ !(
73
+ process.env.NODE_ENV &&
74
+ ["production", "prod", "test", "staging"].includes(process.env.NODE_ENV)
75
+ )
76
+ ) {
77
+ // compare new spec with old spec, continuing only if spec it different
78
+ // auto sync at interval
79
+ if (fetchTimeout) clearTimeout(fetchTimeout);
80
+
81
+ if (!isNaN(config.refetchInterval) && config.refetchInterval) {
82
+ // use config interval or 1 hour
83
+ fetchTimeout = setTimeout(
84
+ () => OpenapiSync(apiUrl, apiName),
85
+ config.refetchInterval || 60000
86
+ );
87
+ }
88
+
89
+ const prevSpec = getState(apiName);
90
+ if (isEqual(prevSpec, spec)) return;
91
+
92
+ setState(apiName, spec);
93
+ }
94
+
95
+ let endpointsFileContent = "";
96
+ let typesFileContent = "";
97
+ let sharedTypesFileContent = "";
98
+
99
+ if (spec.components && spec.components.schemas) {
100
+ // Create components (shared) types
101
+ const components: Record<string, IOpenApiMediaTypeSpec> =
102
+ spec.components.schemas;
103
+ const contentKeys = Object.keys(components);
104
+ // only need 1 schema so will us the first schema provided
105
+ contentKeys.forEach((key) => {
106
+ const typeCnt = `${parseSchemaToType(
107
+ spec,
108
+ components[key] as IOpenApSchemaSpec,
109
+ "",
110
+ true,
111
+ {
112
+ noSharedImport: true,
113
+ }
114
+ )}`;
115
+ if (typeCnt) {
116
+ sharedTypesFileContent += `export type ${getSharedComponentName(
117
+ key
118
+ )} = ${typeCnt};\n`;
119
+ }
120
+ });
121
+ }
122
+
123
+ const getBodySchemaType = (requestBody: IOpenApiRequestBodySpec) => {
124
+ let typeCnt = "";
125
+ if (requestBody.content) {
126
+ const contentKeys = Object.keys(requestBody.content);
127
+ // only need 1 schema so will us the first schema provided
128
+ if (contentKeys[0] && requestBody.content[contentKeys[0]].schema) {
129
+ typeCnt += `${parseSchemaToType(
130
+ spec,
131
+ requestBody.content[contentKeys[0]].schema as IOpenApSchemaSpec,
132
+ ""
133
+ )}`;
134
+ }
135
+ }
136
+ return typeCnt;
137
+ };
138
+
139
+ Object.keys(spec.paths || {}).forEach((endpointPath) => {
140
+ const endpointSpec = spec.paths[endpointPath];
141
+ const endpointMethods = Object.keys(endpointSpec);
142
+ endpointMethods.forEach((method: string) => {
143
+ const endpoint = getEndpointDetails(endpointPath, method);
144
+
145
+ const endpointUrlTxt = endpoint.pathParts
146
+ .map((part) => {
147
+ // check if part is a variable
148
+ if (part[0] === "{" && part[part.length - 1] === "}") {
149
+ const s = part.replace(/{/, "").replace(/}/, "");
150
+ part = `\${${s}}`;
151
+ }
152
+ return part;
153
+ })
154
+ .join("/");
155
+
156
+ let endpointUrl = `"${endpointUrlTxt}"`;
157
+ if (endpoint.variables.length > 0) {
158
+ const params = endpoint.variables.map((v) => `${v}:string`).join(",");
159
+ endpointUrl = `(${params})=> \`${endpointUrlTxt}\``;
160
+ }
161
+ // Add the endpoint url
162
+ endpointsFileContent += `export const ${endpoint.name} = ${endpointUrl};
163
+ `;
164
+
165
+ if (endpointSpec[method]?.parameters) {
166
+ // create query parameters types
167
+ const parameters: IOpenApiParameterSpec[] =
168
+ endpointSpec[method]?.parameters;
169
+ let typeCnt = "";
170
+ parameters.forEach((param) => {
171
+ if (param.in === "query" && param.name) {
172
+ typeCnt += `${parseSchemaToType(
173
+ spec,
174
+ param.schema as any,
175
+ param.name,
176
+ param.required
177
+ )}`;
178
+ }
179
+ });
180
+
181
+ if (typeCnt) {
182
+ typesFileContent += `export type I${endpoint.name}Query = {\n${typeCnt}};\n`;
183
+ }
184
+ }
185
+
186
+ if (endpointSpec[method]?.requestBody) {
187
+ //create requestBody types
188
+ const requestBody: IOpenApiRequestBodySpec =
189
+ endpointSpec[method]?.requestBody;
190
+
191
+ let typeCnt = getBodySchemaType(requestBody);
192
+ if (typeCnt) {
193
+ typesFileContent += `export type I${endpoint.name}DTO = ${typeCnt};\n`;
194
+ }
195
+ }
196
+
197
+ if (endpointSpec[method]?.responses) {
198
+ // create request response types
199
+ const responses: IOpenApiResponseSpec = endpointSpec[method]?.responses;
200
+ const resCodes = Object.keys(responses);
201
+ resCodes.forEach((code) => {
202
+ let typeCnt = getBodySchemaType(responses[code]);
203
+ if (typeCnt) {
204
+ typesFileContent += `export type I${endpoint.name}${code}Response = ${typeCnt};\n`;
205
+ }
206
+ });
207
+ }
208
+ });
209
+ });
210
+
211
+ // Create the necessary directories
212
+ const endpointsFilePath = path.join(rootUsingCwd, folderPath, "endpoints.ts");
213
+ await fs.promises.mkdir(path.dirname(endpointsFilePath), { recursive: true });
214
+ // Create the file asynchronously
215
+ await fs.promises.writeFile(endpointsFilePath, endpointsFileContent);
216
+
217
+ if (sharedTypesFileContent.length > 0) {
218
+ // Create the necessary directories
219
+ const sharedTypesFilePath = path.join(
220
+ rootUsingCwd,
221
+ folderPath,
222
+ "types",
223
+ "shared.ts"
224
+ );
225
+ await fs.promises.mkdir(path.dirname(sharedTypesFilePath), {
226
+ recursive: true,
227
+ });
228
+ // Create the file asynchronously
229
+ await fs.promises.writeFile(sharedTypesFilePath, sharedTypesFileContent);
230
+ }
231
+
232
+ if (typesFileContent.length > 0) {
233
+ // Create the necessary directories
234
+ const typesFilePath = path.join(
235
+ rootUsingCwd,
236
+ folderPath,
237
+ "types",
238
+ "index.ts"
239
+ );
240
+ await fs.promises.mkdir(path.dirname(typesFilePath), { recursive: true });
241
+ // Create the file asynchronously
242
+ await fs.promises.writeFile(
243
+ typesFilePath,
244
+ `${
245
+ sharedTypesFileContent.length > 0
246
+ ? `import * as Shared from "./shared";\n\n`
247
+ : ""
248
+ }${typesFileContent}`
249
+ );
250
+ }
251
+ };
252
+ export default OpenapiSync;
@@ -0,0 +1,15 @@
1
+ import { IOpenApiSpec } from "./types";
2
+
3
+ let state: Record<string, IOpenApiSpec> = {};
4
+
5
+ export const setState = (key: string, value: IOpenApiSpec) => {
6
+ state[key] = value;
7
+ };
8
+
9
+ export const getState = (key: string): IOpenApiSpec | undefined => {
10
+ return state[key];
11
+ };
12
+
13
+ export const resetState = () => {
14
+ state = {};
15
+ };
@@ -0,0 +1,43 @@
1
+ export type IOpenApiSpec = Record<"openapi", string> & Record<string, any>;
2
+
3
+ export type IOpenApSchemaSpec = {
4
+ type: "string" | "integer" | "number" | "array" | "object";
5
+ example?: any;
6
+ enum?: string[];
7
+ format?: string;
8
+ items?: IOpenApSchemaSpec;
9
+ required?: string[];
10
+ $ref?: string;
11
+ properties?: Record<string, IOpenApSchemaSpec>;
12
+ };
13
+
14
+ export type IOpenApiParameterSpec = {
15
+ name: string;
16
+ in: string;
17
+ enum?: string[];
18
+ description?: string;
19
+ required?: boolean;
20
+ deprecated?: boolean;
21
+ allowEmptyValue?: boolean;
22
+ style?: string;
23
+ explode?: boolean;
24
+ allowReserved?: boolean;
25
+ schema?: IOpenApSchemaSpec;
26
+ example?: any;
27
+ examples?: any[];
28
+ };
29
+
30
+ export type IOpenApiMediaTypeSpec = {
31
+ schema?: IOpenApSchemaSpec;
32
+ example?: any;
33
+ examples?: any[];
34
+ encoding?: any;
35
+ };
36
+
37
+ export type IOpenApiRequestBodySpec = {
38
+ description?: string;
39
+ required?: boolean;
40
+ content: Record<string, IOpenApiMediaTypeSpec>;
41
+ };
42
+
43
+ export type IOpenApiResponseSpec = Record<string, IOpenApiRequestBodySpec>;
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ ## Install Openapi-sync
2
+
3
+ `npm i openapi-sync`
4
+
5
+ ## Add a Openapi-sync config file
6
+
7
+ Add a `openapi.sync.json` file at the root of your project (check out `openapi.sync.sample.json`)
8
+
9
+ ## import `openapi-sync`
10
+
11
+ import openapi-sync anywhere in your project (preferably the entry point)
12
+ `import 'openapi-sync'`
13
+
14
+ ## Run your app
package/bin/cli ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env sh
2
+
3
+
4
+ npm run start
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.parseSchemaToType = exports.getEndpointDetails = exports.getSharedComponentName = exports.capitalize = exports.yamlStringToJson = exports.isYamlString = exports.isJson = void 0;
30
+ const regex_1 = require("./regex");
31
+ const property_expr_1 = __importDefault(require("property-expr"));
32
+ const yaml = __importStar(require("js-yaml"));
33
+ const isJson = (value) => {
34
+ return ["object"].includes(typeof value) && !(value instanceof Blob);
35
+ };
36
+ exports.isJson = isJson;
37
+ const isYamlString = (fileContent) => {
38
+ try {
39
+ yaml.load(fileContent);
40
+ return true;
41
+ }
42
+ catch (en) {
43
+ const e = en;
44
+ if (e instanceof yaml.YAMLException) {
45
+ return false;
46
+ }
47
+ else {
48
+ throw e;
49
+ }
50
+ }
51
+ };
52
+ exports.isYamlString = isYamlString;
53
+ const yamlStringToJson = (fileContent) => {
54
+ if ((0, exports.isYamlString)(fileContent)) {
55
+ const content = yaml.load(fileContent);
56
+ const jsonString = JSON.stringify(content, null, 2);
57
+ const json = JSON.parse(jsonString);
58
+ return json;
59
+ }
60
+ };
61
+ exports.yamlStringToJson = yamlStringToJson;
62
+ const capitalize = (text) => {
63
+ const capitalizedWord = text.substring(0, 1).toUpperCase() + text.substring(1);
64
+ return capitalizedWord;
65
+ };
66
+ exports.capitalize = capitalize;
67
+ const getSharedComponentName = (componentName) => `IApi${(0, exports.capitalize)(componentName)}`;
68
+ exports.getSharedComponentName = getSharedComponentName;
69
+ const getEndpointDetails = (path, method) => {
70
+ const pathParts = path.split("/");
71
+ let name = `${(0, exports.capitalize)(method)}`;
72
+ const variables = [];
73
+ pathParts.forEach((part) => {
74
+ // check if part is a variable
75
+ if (part[0] === "{" && part[part.length - 1] === "}") {
76
+ const s = part.replace(/{/, "").replace(/}/, "");
77
+ variables.push(s);
78
+ part = `$${s}`;
79
+ }
80
+ // parse to variable name
81
+ let partVal = "";
82
+ part.split("").forEach((char) => {
83
+ let c = char;
84
+ if (!regex_1.variableNameChar.test(char))
85
+ c = "/";
86
+ partVal += c;
87
+ });
88
+ partVal.split("/").forEach((val) => {
89
+ name += (0, exports.capitalize)(val);
90
+ });
91
+ });
92
+ return { name, variables, pathParts };
93
+ };
94
+ exports.getEndpointDetails = getEndpointDetails;
95
+ const parseSchemaToType = (apiDoc, schema, name, isRequired, options) => {
96
+ let typeName = name ? `\t${name}${isRequired ? "" : "?"}: ` : "";
97
+ let type = "";
98
+ if (schema.$ref) {
99
+ if (schema.$ref[0] === "#") {
100
+ let pathToComponentParts = (schema.$ref || "").split("/");
101
+ pathToComponentParts.shift();
102
+ const pathToComponent = pathToComponentParts.join(".");
103
+ const component = property_expr_1.default.getter(pathToComponent)(apiDoc);
104
+ if (component) {
105
+ const componentName = pathToComponentParts[pathToComponentParts.length - 1];
106
+ // Reference component via import instead of parsing
107
+ type += `${(options === null || options === void 0 ? void 0 : options.noSharedImport) ? "" : "Shared."}${(0, exports.getSharedComponentName)(componentName)}`;
108
+ // type += `${parseSchemaToType(apiDoc, component, "", isRequired)}`;
109
+ }
110
+ }
111
+ else {
112
+ type += "";
113
+ //TODO $ref is a uri - use axios to fetch doc
114
+ }
115
+ }
116
+ else if (schema.type) {
117
+ if (schema.enum && schema.enum.length > 0) {
118
+ if (schema.enum.length > 1)
119
+ type += "(";
120
+ type += schema.enum
121
+ .map((v) => `"${v}"`)
122
+ .join("|")
123
+ .toString();
124
+ if (schema.enum.length > 1)
125
+ type += ")";
126
+ }
127
+ else if (["string", "integer", "number", "array"].includes(schema.type)) {
128
+ if (schema.type === "string") {
129
+ type += `string`;
130
+ }
131
+ else if (["integer", "number"].includes(schema.type)) {
132
+ type += `number`;
133
+ }
134
+ else if (schema.type === "array") {
135
+ if (schema.items) {
136
+ type += `${(0, exports.parseSchemaToType)(apiDoc, schema.items, "", false, options)}[]`;
137
+ }
138
+ else {
139
+ type += "any[]";
140
+ }
141
+ }
142
+ }
143
+ else if (schema.type === "object") {
144
+ if (schema.properties) {
145
+ //parse object key one at a time
146
+ const objKeys = Object.keys(schema.properties);
147
+ const requiredKeys = schema.required || [];
148
+ type += "{\n";
149
+ objKeys.forEach((key) => {
150
+ var _a;
151
+ type += `${(0, exports.parseSchemaToType)(apiDoc, (_a = schema.properties) === null || _a === void 0 ? void 0 : _a[key], key, requiredKeys.includes(key), options)}`;
152
+ });
153
+ type += "}";
154
+ }
155
+ }
156
+ }
157
+ return type.length > 0 ? `${typeName}${type}${name ? ";\n" : ""}` : "";
158
+ };
159
+ exports.parseSchemaToType = parseSchemaToType;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.variableNameChar = exports.variableName = void 0;
4
+ exports.variableName = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
5
+ exports.variableNameChar = /[A-Za-z0-9_$]/;
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const helpers_1 = require("./components/helpers");
18
+ const lodash_1 = require("lodash");
19
+ const axios_1 = __importDefault(require("axios"));
20
+ const axios_retry_1 = __importDefault(require("axios-retry"));
21
+ const openapi_core_1 = require("@redocly/openapi-core");
22
+ const state_1 = require("./state");
23
+ const rootUsingCwd = process.cwd();
24
+ let fetchTimeout = null;
25
+ // Create an Axios instance
26
+ const apiClient = axios_1.default.create({
27
+ timeout: 30000, // Timeout after 30 seconds
28
+ });
29
+ // Configure axios-retry
30
+ (0, axios_retry_1.default)(apiClient, {
31
+ retries: 20, // Number of retry attempts
32
+ retryCondition: (error) => {
33
+ // Retry on network error
34
+ return (error.code === "ECONNABORTED" || error.message.includes("Network Error"));
35
+ },
36
+ retryDelay: (retryCount) => {
37
+ return retryCount * 1000; // Exponential back-off: 1s, 2s, 3s, etc.
38
+ },
39
+ });
40
+ const OpenapiSync = (apiUrl, apiName) => __awaiter(void 0, void 0, void 0, function* () {
41
+ const specResponse = yield apiClient.get(apiUrl);
42
+ const redoclyConfig = yield (0, openapi_core_1.createConfig)({
43
+ extends: ["minimal"],
44
+ });
45
+ const source = JSON.stringify((0, helpers_1.isJson)(specResponse.data)
46
+ ? specResponse.data
47
+ : (0, helpers_1.yamlStringToJson)(specResponse.data));
48
+ const lintResults = yield (0, openapi_core_1.bundleFromString)({
49
+ source,
50
+ config: redoclyConfig,
51
+ });
52
+ // Load config file
53
+ const config = require(path_1.default.join(rootUsingCwd, "openapi.sync.json"));
54
+ const folderPath = path_1.default.join(config.folder || "", apiName);
55
+ const spec = lintResults.bundle.parsed;
56
+ // auto update only on dev
57
+ if (!(process.env.NODE_ENV &&
58
+ ["production", "prod", "test", "staging"].includes(process.env.NODE_ENV))) {
59
+ // compare new spec with old spec, continuing only if spec it different
60
+ // auto sync at interval
61
+ if (fetchTimeout)
62
+ clearTimeout(fetchTimeout);
63
+ if (!isNaN(config.refetchInterval) && config.refetchInterval) {
64
+ // use config interval or 1 hour
65
+ fetchTimeout = setTimeout(() => OpenapiSync(apiUrl, apiName), config.refetchInterval || 60000);
66
+ }
67
+ const prevSpec = (0, state_1.getState)(apiName);
68
+ if ((0, lodash_1.isEqual)(prevSpec, spec))
69
+ return;
70
+ (0, state_1.setState)(apiName, spec);
71
+ }
72
+ let endpointsFileContent = "";
73
+ 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`;
86
+ }
87
+ });
88
+ }
89
+ const getBodySchemaType = (requestBody) => {
90
+ let typeCnt = "";
91
+ if (requestBody.content) {
92
+ const contentKeys = Object.keys(requestBody.content);
93
+ // only need 1 schema so will us the first schema provided
94
+ if (contentKeys[0] && requestBody.content[contentKeys[0]].schema) {
95
+ typeCnt += `${(0, helpers_1.parseSchemaToType)(spec, requestBody.content[contentKeys[0]].schema, "")}`;
96
+ }
97
+ }
98
+ return typeCnt;
99
+ };
100
+ Object.keys(spec.paths || {}).forEach((endpointPath) => {
101
+ const endpointSpec = spec.paths[endpointPath];
102
+ const endpointMethods = Object.keys(endpointSpec);
103
+ endpointMethods.forEach((method) => {
104
+ var _a, _b, _c, _d, _e, _f;
105
+ const endpoint = (0, helpers_1.getEndpointDetails)(endpointPath, method);
106
+ const endpointUrlTxt = endpoint.pathParts
107
+ .map((part) => {
108
+ // check if part is a variable
109
+ if (part[0] === "{" && part[part.length - 1] === "}") {
110
+ const s = part.replace(/{/, "").replace(/}/, "");
111
+ part = `\${${s}}`;
112
+ }
113
+ return part;
114
+ })
115
+ .join("/");
116
+ let endpointUrl = `"${endpointUrlTxt}"`;
117
+ if (endpoint.variables.length > 0) {
118
+ const params = endpoint.variables.map((v) => `${v}:string`).join(",");
119
+ endpointUrl = `(${params})=> \`${endpointUrlTxt}\``;
120
+ }
121
+ // Add the endpoint url
122
+ endpointsFileContent += `export const ${endpoint.name} = ${endpointUrl};
123
+ `;
124
+ if ((_a = endpointSpec[method]) === null || _a === void 0 ? void 0 : _a.parameters) {
125
+ // create query parameters types
126
+ const parameters = (_b = endpointSpec[method]) === null || _b === void 0 ? void 0 : _b.parameters;
127
+ let typeCnt = "";
128
+ parameters.forEach((param) => {
129
+ if (param.in === "query" && param.name) {
130
+ typeCnt += `${(0, helpers_1.parseSchemaToType)(spec, param.schema, param.name, param.required)}`;
131
+ }
132
+ });
133
+ if (typeCnt) {
134
+ typesFileContent += `export type I${endpoint.name}Query = {\n${typeCnt}};\n`;
135
+ }
136
+ }
137
+ if ((_c = endpointSpec[method]) === null || _c === void 0 ? void 0 : _c.requestBody) {
138
+ //create requestBody types
139
+ const requestBody = (_d = endpointSpec[method]) === null || _d === void 0 ? void 0 : _d.requestBody;
140
+ let typeCnt = getBodySchemaType(requestBody);
141
+ if (typeCnt) {
142
+ typesFileContent += `export type I${endpoint.name}DTO = ${typeCnt};\n`;
143
+ }
144
+ }
145
+ if ((_e = endpointSpec[method]) === null || _e === void 0 ? void 0 : _e.responses) {
146
+ // create request response types
147
+ const responses = (_f = endpointSpec[method]) === null || _f === void 0 ? void 0 : _f.responses;
148
+ const resCodes = Object.keys(responses);
149
+ resCodes.forEach((code) => {
150
+ let typeCnt = getBodySchemaType(responses[code]);
151
+ if (typeCnt) {
152
+ typesFileContent += `export type I${endpoint.name}${code}Response = ${typeCnt};\n`;
153
+ }
154
+ });
155
+ }
156
+ });
157
+ });
158
+ // Create the necessary directories
159
+ const endpointsFilePath = path_1.default.join(rootUsingCwd, folderPath, "endpoints.ts");
160
+ yield fs_1.default.promises.mkdir(path_1.default.dirname(endpointsFilePath), { recursive: true });
161
+ // Create the file asynchronously
162
+ yield fs_1.default.promises.writeFile(endpointsFilePath, endpointsFileContent);
163
+ if (sharedTypesFileContent.length > 0) {
164
+ // Create the necessary directories
165
+ const sharedTypesFilePath = path_1.default.join(rootUsingCwd, folderPath, "types", "shared.ts");
166
+ yield fs_1.default.promises.mkdir(path_1.default.dirname(sharedTypesFilePath), {
167
+ recursive: true,
168
+ });
169
+ // Create the file asynchronously
170
+ yield fs_1.default.promises.writeFile(sharedTypesFilePath, sharedTypesFileContent);
171
+ }
172
+ if (typesFileContent.length > 0) {
173
+ // Create the necessary directories
174
+ const typesFilePath = path_1.default.join(rootUsingCwd, folderPath, "types", "index.ts");
175
+ yield fs_1.default.promises.mkdir(path_1.default.dirname(typesFilePath), { recursive: true });
176
+ // Create the file asynchronously
177
+ yield fs_1.default.promises.writeFile(typesFilePath, `${sharedTypesFileContent.length > 0
178
+ ? `import * as Shared from "./shared";\n\n`
179
+ : ""}${typesFileContent}`);
180
+ }
181
+ });
182
+ exports.default = OpenapiSync;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resetState = exports.getState = exports.setState = void 0;
4
+ let state = {};
5
+ const setState = (key, value) => {
6
+ state[key] = value;
7
+ };
8
+ exports.setState = setState;
9
+ const getState = (key) => {
10
+ return state[key];
11
+ };
12
+ exports.getState = getState;
13
+ const resetState = () => {
14
+ state = {};
15
+ };
16
+ exports.resetState = resetState;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/index.js CHANGED
@@ -1,6 +1,32 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
2
14
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.add = add;
4
- function add(x, y) {
5
- return x + y;
6
- }
15
+ const Openapi_sync_1 = __importDefault(require("./Openapi-sync"));
16
+ const dotenv_1 = __importDefault(require("dotenv"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const state_1 = require("./Openapi-sync/state");
19
+ dotenv_1.default.config();
20
+ const rootUsingCwd = process.cwd();
21
+ const Init = () => __awaiter(void 0, void 0, void 0, function* () {
22
+ // Load config file
23
+ const config = require(path_1.default.join(rootUsingCwd, "openapi.sync.json"));
24
+ const apiNames = Object.keys(config.api);
25
+ (0, state_1.resetState)();
26
+ for (let i = 0; i < apiNames.length; i += 1) {
27
+ const apiName = apiNames[i];
28
+ const apiUrl = config.api[apiName];
29
+ (0, Openapi_sync_1.default)(apiUrl, apiName);
30
+ }
31
+ });
32
+ Init();
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./Openapi-sync/types";
package/index.ts CHANGED
@@ -1,3 +1,24 @@
1
- export function add(x: number, y: number): number {
2
- return x + y;
3
- }
1
+ import OpenapiSync from "./Openapi-sync";
2
+ import dotenv from "dotenv";
3
+ import path from "path";
4
+ import { resetState } from "./Openapi-sync/state";
5
+
6
+ dotenv.config();
7
+
8
+ const rootUsingCwd = process.cwd();
9
+
10
+ const Init = async () => {
11
+ // Load config file
12
+ const config = require(path.join(rootUsingCwd, "openapi.sync.json"));
13
+ const apiNames = Object.keys(config.api);
14
+
15
+ resetState();
16
+ for (let i = 0; i < apiNames.length; i += 1) {
17
+ const apiName = apiNames[i];
18
+ const apiUrl = config.api[apiName];
19
+
20
+ OpenapiSync(apiUrl, apiName);
21
+ }
22
+ };
23
+
24
+ Init();
@@ -0,0 +1,8 @@
1
+ {
2
+ "refetchInterval": 5000,
3
+ "folder": "inputed/path",
4
+ "api": {
5
+ "example1": "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.json",
6
+ "example2": "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml"
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,17 +1,34 @@
1
1
  {
2
2
  "name": "openapi-sync",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "sync openapi variables",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "openapi-sync": "./bin/cli"
9
+ },
7
10
  "scripts": {
8
11
  "test": "echo \"Error: no test specified\" && exit 1",
9
- "build": "tsc"
12
+ "build": "tsc",
13
+ "publish": "npm run build && npm version patch && npm publish",
14
+ "start": "ts-node index.ts"
10
15
  },
11
16
  "author": "P-Technologies",
12
17
  "license": "ISC",
13
18
  "devDependencies": {
19
+ "@types/js-yaml": "^4.0.9",
20
+ "@types/lodash": "^4.17.7",
14
21
  "@types/node": "^22.1.0",
22
+ "ts-node": "^10.9.2",
15
23
  "typescript": "^5.5.4"
24
+ },
25
+ "dependencies": {
26
+ "@redocly/openapi-core": "^1.19.0",
27
+ "axios": "^1.7.3",
28
+ "axios-retry": "^4.5.0",
29
+ "dotenv": "^16.4.5",
30
+ "js-yaml": "^4.1.0",
31
+ "lodash": "^4.17.21",
32
+ "property-expr": "^2.0.6"
16
33
  }
17
34
  }
package/tsconfig.json CHANGED
@@ -39,7 +39,7 @@
39
39
  // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
40
40
  // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
41
41
  // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
42
- // "resolveJsonModule": true, /* Enable importing .json files. */
42
+ "resolveJsonModule": true, /* Enable importing .json files. */
43
43
  // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
44
44
  // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
45
45