diff --git a/Dockerfile b/example/Dockerfile similarity index 96% rename from Dockerfile rename to example/Dockerfile index 5dd4369..18d2464 100644 --- a/Dockerfile +++ b/example/Dockerfile @@ -25,6 +25,6 @@ RUN --mount=type=bind,target=./package.json,src=./package.json \ COPY ./src src/ COPY ./example example/ -EXPOSE 8000 -CMD ["node", "example/server.js"] +EXPOSE 8000 +ENTRYPOINT ["node"] diff --git a/example/kafka-rest-router.js b/example/kafka-rest-router.js new file mode 100644 index 0000000..aac788e --- /dev/null +++ b/example/kafka-rest-router.js @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const commander = require("commander"); +const { program } = require("commander"); +const express = require("express"); +const bodyParser = require("body-parser"); + +// Functions. +const myParseInt = (value, _dummyPrevious) => { + // parseInt takes a string and a radix + const parsedValue = parseInt(value, 10); + if (isNaN(parsedValue)) { + throw new commander.InvalidArgumentError("Not a number."); + } + return parsedValue; +}; + +const configureCors = (req, res, next) => { + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Methods", "POST,OPTIONS"); + res.header( + "Access-Control-Allow-Headers", + "Content-Type, Authorization, Content-Length, X-Requested-With", + ); + + // intercept OPTIONS method + if ("OPTIONS" == req.method) { + res.sendStatus(200); + } else { + next(); + } +}; + +const transformAndForward = async (req, res) => { + try { + const body = typeof req.body === "string" ? JSON.parse(req.body) : req.body; + + // Sometimes UserALE sends empty buffers or empty objects, these need to be filtered out + const isEmptyArray = Array.isArray(body) && body.length === 0; + const isEmptyObject = + typeof body === "object" && + body !== null && + Object.keys(body).length === 0; + if (isEmptyArray || isEmptyObject) return; + + if (options.verbose) console.log(JSON.stringify(body, null, 3)); + + const transformedPayload = { + records: body.map((log) => ({ + key: log["sessionId"], + value: log, + })), + }; + + if (options.verbose) + console.log(JSON.stringify(transformedPayload, null, 3)); + + const response = await fetch(options.forwardTo, { + method: "POST", + body: JSON.stringify(transformedPayload), + headers: { "Content-Type": "application/json" }, + }); + + const statusCode = response.status; + + res.status(statusCode).send(`Forwarded status code: ${statusCode}`); + } catch (error) { + console.error("Error: ", error); + res.status(500).send("Internal service error"); + } +}; + +const gracefulShutdown = () => { + process.exit(); +}; + +// Args +program + .requiredOption("-f, --forward-to ", "Forwarding address") + .option("-p, --port ", "Port number", myParseInt, 8000) + .option("-v, --verbose", "Enable verbose mode", false) + .parse(process.argv); +const options = program.opts(); +console.log(options); + +// Configure app +const app = express(); + +app.set("port", options.port); +app.use(configureCors); +app.use(bodyParser.urlencoded({ extended: true, limit: "100mb" })); +app.use(bodyParser.json({ limit: "100mb" })); +app.use(bodyParser.text()); + +// Routes +app.post("/", transformAndForward); + +// Server +app.listen(app.get("port"), () => { + console.log( + "UserALE Karapace Router started...", + `\n\tPort: ${app.get("port")}`, + `\n\tForwarding address: ${options.forwardTo}`, + `\n\tVerbose: ${options.verbose}`, + ); +}); + +process.on("SIGTERM", () => gracefulShutdown()); +process.on("SIGINT", () => gracefulShutdown()); diff --git a/package-lock.json b/package-lock.json index ccf853c..0ab7165 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@typescript/lib-dom": "npm:@types/web@^0.0.144", "babel-jest": "^29.7.0", "body-parser": "^1.20.2", + "commander": "^12.1.0", "cypress": "^13.6.0", "cz-conventional-changelog": "^3.3.0", "detect-browser": "^5.3.0", @@ -4498,11 +4499,13 @@ } }, "node_modules/commander": { - "version": "6.2.1", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=18" } }, "node_modules/commenting": { @@ -4972,6 +4975,16 @@ "dev": true, "license": "MIT" }, + "node_modules/cypress/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/cypress/node_modules/has-flag": { "version": "4.0.0", "dev": true, diff --git a/package.json b/package.json index df03057..d53742e 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "@typescript/lib-dom": "npm:@types/web@^0.0.144", "babel-jest": "^29.7.0", "body-parser": "^1.20.2", + "commander": "^12.1.0", "cypress": "^13.6.0", "cz-conventional-changelog": "^3.3.0", "detect-browser": "^5.3.0", diff --git a/tsconfig.json b/tsconfig.json index 853b8a4..7b86f3b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,11 @@ "isolatedModules": true, "incremental": true, "jsx": "preserve", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "module": "esnext", "moduleResolution": "node", "noEmit": true, @@ -22,7 +26,9 @@ "noUnusedLocals": true, "noUnusedParameters": false, "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] }, "pretty": true, "resolveJsonModule": true, @@ -30,8 +36,18 @@ "sourceMap": true, "strict": true, "target": "es2015", - "typeRoots": ["node_modules/@types/"], + "typeRoots": [ + "node_modules/@types/" + ], }, - "include": ["src", "test", "src/types.d.ts"], - "exclude": ["node_modules", "build", "logs", "example"], + "include": [ + "src", + "test", + "src/types.d.ts" + ], + "exclude": [ + "node_modules", + "build", + "logs" + ], }