Skip to content

Commit

Permalink
enhance nodejs-openapi action
Browse files Browse the repository at this point in the history
enhance nodejs action
enhance utils file
  • Loading branch information
TimurRin committed Sep 20, 2024
1 parent ebadc78 commit 8ee803b
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 55 deletions.
3 changes: 2 additions & 1 deletion anca.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"dataVersion": 0,
"version": {
"latest": "0.1.0-dev.3",
"timestamp": 1726653430
"latestNext": "0.1.0-dev.3+next.20240920_102636",
"timestamp": 1726827996
},
"files": [
{
Expand Down
138 changes: 101 additions & 37 deletions src/actions/nodejs-openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ interface ModelsByFile {

const prettierPlugins = [prettierEstree, prettierTypescript];

const contentTypesExpressMapping: Record<string, string> = {
"application/json": "json",
"application/xml": "xml",
};

/**
* Generate a fallback file name based on the method and api path
* @param operationId
Expand Down Expand Up @@ -127,7 +132,7 @@ function getCodeModelPath(development: AncaDevelopment, model: string) {
* @param development
*/
async function generateTypeScriptEssentialModels(development: AncaDevelopment) {
const modelsLocation = path.resolve(development.fullPath, "./src/models");
const modelsLocation = path.resolve(development.fullPath, "./src/types");

console.log(
"generateTypeScriptEssentialModels",
Expand All @@ -136,9 +141,9 @@ async function generateTypeScriptEssentialModels(development: AncaDevelopment) {
);

await fs.writeFile(
path.join(modelsLocation, "ServiceResponse.ts"),
path.join(modelsLocation, "anca.ts"),
await prettier.format(
`export interface ServiceResponse<D, S, T = undefined> {
`export interface ServiceAmbiguousResponse<D, S, T = undefined> {
code: S;
data: D;
type?: T;
Expand Down Expand Up @@ -419,9 +424,14 @@ export async function generateNodejsOpenapiFiles(development: AncaDevelopment) {
const controllersDir = path.join(development.fullPath, "src", "controllers");
const servicesDir = path.join(development.fullPath, "src", "services");
const routesFile = path.join(development.fullPath, "src", "routes.ts");
const typesDir = path.join(development.fullPath, "src", "types", "anca.ts");
// const typesFile = path.join(typesDir, "anca.ts");

fs.mkdir(controllersDir, { recursive: true });
fs.mkdir(servicesDir, { recursive: true });
fs.mkdir(typesDir, { recursive: true });

// let typesInterfaces = "";

let routesImports = "";
let routesAuth = false;
Expand All @@ -433,10 +443,12 @@ export async function generateNodejsOpenapiFiles(development: AncaDevelopment) {
firstResponseCode: number;
firstResponseContent: string;
functionName: string;
isAmbiguous: boolean;
method: string;
operation: any;
responseCodes: string;
responseContent: string;
responseCodesCode: string;
responseContent: string[];
responseContentCode: string;
}[] = [];

if (openapi.paths) {
Expand Down Expand Up @@ -481,10 +493,14 @@ export async function generateNodejsOpenapiFiles(development: AncaDevelopment) {
firstResponseCode: sortedResponseCodes[0],
firstResponseContent: responseContentArray[0],
functionName,
isAmbiguous:
sortedResponseCodes.length > 1 ||
responseContentArray.length > 1,
method,
operation,
responseCodes: sortedResponseCodes.join(" | "),
responseContent:
responseCodesCode: sortedResponseCodes.join(" | "),
responseContent: responseContentArray,
responseContentCode:
responseContentArray.length > 0
? ', "' + responseContentArray.join(`" | "`) + '"'
: "",
Expand All @@ -495,7 +511,9 @@ export async function generateNodejsOpenapiFiles(development: AncaDevelopment) {
routesImports += `import { isAuthenticated } from "./middleware/isAuthenticated.js";\n`;
}

routesFunctions += `router.${method}("${apiPath}", ${operation.security ? "isAuthenticated, " : ""}${functionName});\n`;
const apiPathExpress = apiPath.replace(/{([^}]+)}/g, ":$1");

routesFunctions += `router.${method}("${apiPathExpress}", ${operation.security ? "isAuthenticated, " : ""}${functionName});\n`;
});
});
}
Expand All @@ -508,9 +526,11 @@ export async function generateNodejsOpenapiFiles(development: AncaDevelopment) {
firstResponseCode,
firstResponseContent,
functionName,
isAmbiguous,
operation,
responseCodes,
responseCodesCode,
responseContent,
responseContentCode,
}) => {
routesImports += `import ${functionName} from "./controllers/${fileName}.js";\n`;

Expand All @@ -519,15 +539,27 @@ export async function generateNodejsOpenapiFiles(development: AncaDevelopment) {

const fileModelData = modelsByFile[fileName];

let responsesImportContent = `import { ServiceResponse } from "../models/ServiceResponse.js";\n`;
fileModelData?.response?.forEach((responseModelData) => {
responsesImportContent += `import { ${responseModelData.name} } from "${getCodeModelPath(development, responseModelData.name)}";\n`;
});
const filterDuplicates = (value: any, index: number, self: any[]) =>
self.indexOf(value) === index;

let responsesImportContent = isAmbiguous
? `import { ServiceAmbiguousResponse } from "../types/anca.js";\n`
: "";
fileModelData?.response
?.map((value) => value.name)
.filter(filterDuplicates)
.forEach((responseModelDataName) => {
responsesImportContent += `import { ${responseModelDataName} } from "${getCodeModelPath(development, responseModelDataName)}";\n`;
});
const responsesTypesContent =
fileModelData?.response?.map(getModelData).join(" | ") || "unknown";
fileModelData?.response
?.map(getModelData)
.filter(filterDuplicates)
.join(" | ") || "unknown";

let serviceArgumentsFunc = "";
let serviceArgumentsCall = "";
let serviceArgumentsCallCnt = "";
let serviceArgumentsCallSrv = "";
let serviceArgumentsJsdoc = "";

let controllerContent = "";
Expand All @@ -539,19 +571,22 @@ export async function generateNodejsOpenapiFiles(development: AncaDevelopment) {
responsesImportContent += `import { ${fileModelData.request.name} } from "${getCodeModelPath(development, fileModelData.request.name)}";\n`;
serviceArgumentsFunc += `request: ${fileModelData.request.name}`;
serviceArgumentsJsdoc += ` * @param request\n`;
serviceArgumentsCall += `req.body`;
serviceArgumentsCallSrv += `request`;
serviceArgumentsCallCnt += `req.body`;
}
if (fileModelData?.params) {
responsesImportContent += `import { ${fileModelData.params.name} } from "${getCodeModelPath(development, fileModelData.params.name)}";\n`;
serviceArgumentsFunc += `${serviceArgumentsFunc ? ", " : ""}params: ${fileModelData.params.name}`;
serviceArgumentsJsdoc += ` * @param params\n`;
serviceArgumentsCall += `${serviceArgumentsCall ? ", " : ""}req.params`;
serviceArgumentsCallSrv += `${serviceArgumentsCallSrv ? ", " : ""}params`;
serviceArgumentsCallCnt += `${serviceArgumentsCallCnt ? ", " : ""}req.params`;
}
if (fileModelData?.query) {
responsesImportContent += `import { ${fileModelData.query.name} } from "${getCodeModelPath(development, fileModelData.query.name)}";\n`;
serviceArgumentsFunc += `${serviceArgumentsFunc ? ", " : ""}query: ${fileModelData.query.name}`;
serviceArgumentsJsdoc += ` * @param query\n`;
serviceArgumentsCall += `${serviceArgumentsCall ? ", " : ""}req.query`;
serviceArgumentsCallSrv += `${serviceArgumentsCallSrv ? ", " : ""}query`;
serviceArgumentsCallCnt += `${serviceArgumentsCallCnt ? ", " : ""}req.query`;
}

if (responsesImportContent) {
Expand All @@ -570,14 +605,26 @@ export async function generateNodejsOpenapiFiles(development: AncaDevelopment) {
serviceContent += ` * ${operation.summary || ""}\n`;
serviceContent += serviceArgumentsJsdoc;
serviceContent += " */\n";
if (serviceArgumentsFunc) {
serviceContent +=
"// eslint-disable-next-line @typescript-eslint/no-unused-vars\n";
if (isAmbiguous) {
serviceContent += `export async function ${fileName}(${serviceArgumentsFunc}): Promise<ServiceAmbiguousResponse<${responsesTypesContent}, ${responseCodesCode}${responseContentCode}>> {\n`;
} else {
serviceContent += `export async function ${fileName}(${serviceArgumentsFunc}): Promise<${responsesTypesContent}> {\n`;
}
serviceContent += `export async function ${fileName}(${serviceArgumentsFunc}): Promise<ServiceResponse<${responsesTypesContent}, ${responseCodes}${responseContent}>> {\n`;

serviceContent += ` // This stub is generated if this file doesn't exist.\n`;
serviceContent += ` // You can change body of this function, but it should comply with controllers' call.\n`;
serviceContent += ` return { code: ${firstResponseCode}, type: ${firstResponseContent ? '"' + firstResponseContent + '"' : "undefined"}, data: ${responsesTypesContent.includes("[]") ? "[]" : responsesTypesContent !== "unknown" ? "{}" : "null"} };\n`;
serviceContent += ` // You can change body of this function, but it should comply with controllers' call.\n\n`;
serviceContent += ` console.log("${fileName}", ${serviceArgumentsCallSrv});\n\n`;

const codeData = responsesTypesContent.includes("[]")
? "[]"
: responsesTypesContent !== "unknown"
? "{}"
: "null";
if (isAmbiguous) {
serviceContent += ` return { code: ${firstResponseCode}, type: ${firstResponseContent ? '"' + firstResponseContent + '"' : "undefined"}, data: ${codeData} };\n`;
} else {
serviceContent += ` return ${codeData};\n`;
}
serviceContent += `}\n`;

await fs.writeFile(
Expand All @@ -587,6 +634,8 @@ export async function generateNodejsOpenapiFiles(development: AncaDevelopment) {
plugins: prettierPlugins,
}),
);
} else {
console.log("Service file already exists", serviceFile);
}

controllerContent += `\n`;
Expand All @@ -597,20 +646,35 @@ export async function generateNodejsOpenapiFiles(development: AncaDevelopment) {
controllerContent += ` */\n`;
controllerContent += `export default async function (req: Request<${getModelData(fileModelData?.params)}, ${"unknown"}, ${getModelData(fileModelData?.request)}, ${getModelData(fileModelData?.query)}>, res: Response<${responsesTypesContent}>) {\n`;
controllerContent += ` try {\n`;
controllerContent += ` const result: ServiceResponse<${responsesTypesContent}, ${responseCodes}${responseContent}> = await ${fileName}(${serviceArgumentsCall});\n`;
switch (
operation.responses &&
operation.responses[firstResponseCode]?.content?.type
) {
case "application/json":
controllerContent += ` res.status(result.code).json(result.data);\n`;
break;
case "application/xml":
controllerContent += ` res.status(result.code).xml(result.data);\n`;
break;
default:
controllerContent += ` res.status(result.code).send(result.data);\n`;
if (isAmbiguous) {
controllerContent += ` const result: ServiceAmbiguousResponse<${responsesTypesContent}, ${responseCodesCode}${responseContentCode}> = await ${fileName}(${serviceArgumentsCallCnt});\n`;
controllerContent += ` switch (result.type) {\n`;
responseContent.forEach((responseContentType) => {
controllerContent += ` case "${responseContentType}":\n`;
if (contentTypesExpressMapping[responseContentType]) {
controllerContent += ` res.status(result.code).${contentTypesExpressMapping[responseContentType]}(result.data);\n`;
} else {
controllerContent += ` res.status(result.code).contentType("${responseContentType}").send(result.data);\n`;
}
controllerContent += ` break;\n`;
});
controllerContent += ` default:\n`;
controllerContent += ` res.status(result.code).send(result.data);\n`;
controllerContent += ` }\n`;
} else {
controllerContent += ` const result: ${responsesTypesContent} = await ${fileName}(${serviceArgumentsCallCnt});\n`;
switch (firstResponseContent) {
case "application/json":
controllerContent += ` res.status(${firstResponseCode}).json(result);\n`;
break;
case "application/xml":
controllerContent += ` res.status(${firstResponseCode}).xml(result);\n`;
break;
default:
controllerContent += ` res.status(${firstResponseCode}).send(result);\n`;
}
}

controllerContent += ` } catch (error) {\n`;
controllerContent += ` console.error(error);\n`;
controllerContent += ` res.end();\n`;
Expand Down
3 changes: 1 addition & 2 deletions src/actions/nodejs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const SCRIPTS_LIB: Record<string, string> = {
const DEPENDENCIES_API: string[] = [
"dotenv",
"express",
"express-session",
"helmet",
"jsonwebtoken",
"knex",
"winston",
Expand All @@ -136,7 +136,6 @@ const DEPENDENCIES_API: string[] = [
const DEV_DEPENDENCIES_API: string[] = [
"@cinnabar-forge/eslint-plugin",
"@types/express",
"@types/express-session",
"@types/jsonwebtoken",
"esbuild",
"typescript",
Expand Down
4 changes: 2 additions & 2 deletions src/cinnabar.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This file was generated by Cinnabar Meta. Do not edit.

export const CINNABAR_PROJECT_TIMESTAMP = 1726653430;
export const CINNABAR_PROJECT_VERSION = "0.1.0-dev.3";
export const CINNABAR_PROJECT_TIMESTAMP = 1726827996;
export const CINNABAR_PROJECT_VERSION = "0.1.0-dev.3+next.20240920_102636";
22 changes: 9 additions & 13 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Ajv, { AnySchema } from "ajv";
import fs from "fs";
import fs from "fs/promises";
import MarkdownIt from "markdown-it";
import path from "path";

Expand Down Expand Up @@ -90,7 +90,7 @@ export function getHttpCodeFunctionText(code: number | string) {
*/
export async function checkExistence(filePath: string) {
try {
await fs.promises.access(filePath);
await fs.access(filePath);
return true;
} catch {
return false;
Expand All @@ -103,7 +103,7 @@ export async function checkExistence(filePath: string) {
*/
export async function readJsonFile(jsonPath: string): Promise<null | object> {
try {
return JSON.parse(await fs.promises.readFile(jsonPath, "utf-8"));
return JSON.parse(await fs.readFile(jsonPath, "utf-8"));
} catch {
return null;
}
Expand All @@ -119,7 +119,7 @@ export async function readFolderFile(
filePath: string,
): Promise<null | string> {
try {
return await fs.promises.readFile(path.resolve(folder, filePath), "utf-8");
return await fs.readFile(path.resolve(folder, filePath), "utf-8");
} catch {
return null;
}
Expand All @@ -137,11 +137,7 @@ export async function writeFolderFile(
data: string,
): Promise<void> {
try {
await fs.promises.writeFile(
path.resolve(folder, filePath),
data || "",
"utf-8",
);
await fs.writeFile(path.resolve(folder, filePath), data || "", "utf-8");
} catch (error) {
console.error(
`Failed to write file at ${path.resolve(folder, filePath)}:`,
Expand Down Expand Up @@ -171,7 +167,7 @@ export async function writeFolderJsonFile(
data: object,
): Promise<void> {
try {
await fs.promises.writeFile(
await fs.writeFile(
path.resolve(folder, filePath),
stringifyJson(data),
"utf-8",
Expand All @@ -196,7 +192,7 @@ export async function readFolderJsonFile(
): Promise<null | object> {
try {
return JSON.parse(
await fs.promises.readFile(path.resolve(folder, jsonPath), "utf-8"),
await fs.readFile(path.resolve(folder, jsonPath), "utf-8"),
);
} catch {
return null;
Expand Down Expand Up @@ -247,8 +243,8 @@ export async function isJsonFileSubset(firstPath: string, secondPath: string) {
* @param secondPath
*/
export async function isFileSubset(firstPath: string, secondPath: string) {
const firstContent = await fs.promises.readFile(firstPath, "utf-8");
const secondContent = await fs.promises.readFile(secondPath, "utf-8");
const firstContent = await fs.readFile(firstPath, "utf-8");
const secondContent = await fs.readFile(secondPath, "utf-8");

if (firstContent == null || secondContent == null) {
return null;
Expand Down

0 comments on commit 8ee803b

Please sign in to comment.