From 2c450fab71c922a2af6532ec9736f5b884c93ca5 Mon Sep 17 00:00:00 2001 From: "Brett C. Dudo" Date: Mon, 6 Jan 2025 17:39:16 -0800 Subject: [PATCH 1/3] Try Typescript --- package-lock.json | 41 +++++++++++++++++++++ package.json | 1 + src/app.js | 2 - src_ts/app.ts | 23 ++++++++++++ src_ts/clients/greetingClient.ts | 38 +++++++++++++++++++ src_ts/clients/options.ts | 8 ++++ src_ts/clients/personClient.ts | 31 ++++++++++++++++ src_ts/controllers/v1/errorController.ts | 7 ++++ src_ts/controllers/v1/greetingController.ts | 28 ++++++++++++++ src_ts/index.ts | 10 +++++ src_ts/lib/log.ts | 24 ++++++++++++ src_ts/middleware/errorHandler.ts | 5 +++ src_ts/middleware/loggingMiddleware.ts | 34 +++++++++++++++++ src_ts/middleware/metadataInterceptor.ts | 21 +++++++++++ src_ts/middleware/notFoundHandler.ts | 4 ++ src_ts/routes/index.ts | 11 ++++++ src_ts/routes/v1/errorRoutes.ts | 10 +++++ src_ts/routes/v1/greetingRoutes.ts | 11 ++++++ src_ts/routes/v1/index.ts | 13 +++++++ 19 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 src_ts/app.ts create mode 100644 src_ts/clients/greetingClient.ts create mode 100644 src_ts/clients/options.ts create mode 100644 src_ts/clients/personClient.ts create mode 100644 src_ts/controllers/v1/errorController.ts create mode 100644 src_ts/controllers/v1/greetingController.ts create mode 100644 src_ts/index.ts create mode 100644 src_ts/lib/log.ts create mode 100644 src_ts/middleware/errorHandler.ts create mode 100644 src_ts/middleware/loggingMiddleware.ts create mode 100644 src_ts/middleware/metadataInterceptor.ts create mode 100644 src_ts/middleware/notFoundHandler.ts create mode 100644 src_ts/routes/index.ts create mode 100644 src_ts/routes/v1/errorRoutes.ts create mode 100644 src_ts/routes/v1/greetingRoutes.ts create mode 100644 src_ts/routes/v1/index.ts diff --git a/package-lock.json b/package-lock.json index 90a6ad6..219ba17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@grpc/grpc-js": "^1.12.4", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.55.0", + "@types/express-serve-static-core": "^5.0.3", "express": "^4.21.1" }, "devDependencies": { @@ -1698,6 +1699,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.3.tgz", + "integrity": "sha512-JEhMNwUJt7bw728CydvYzntD0XJeTmDnvwLlbfbAhE7Tbslm/ax6bdIiUwTgeVlZTsJQPwZwKpAkyDtIjsvx3g==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1714,6 +1727,12 @@ "@types/node": "*" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, "node_modules/@types/mysql": { "version": "2.15.26", "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", @@ -1752,6 +1771,28 @@ "@types/pg": "*" } }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/shimmer": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", diff --git a/package.json b/package.json index 12fc522..33a722d 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@grpc/grpc-js": "^1.12.4", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.55.0", + "@types/express-serve-static-core": "^5.0.3", "express": "^4.21.1" }, "devDependencies": { diff --git a/src/app.js b/src/app.js index 661a314..977f177 100644 --- a/src/app.js +++ b/src/app.js @@ -2,7 +2,6 @@ import express from "express"; import routeHandler from "./routes/index.js"; import { loggingMiddleware } from "./middleware/loggingMiddleware.js"; -// import { baggageExtractorMiddleware } from "./middleware/baggageExtractorMiddleware.js"; import { notFoundHandler } from "./middleware/notFoundHandler.js"; import { errorHandler } from "./middleware/errorHandler.js"; @@ -12,7 +11,6 @@ const app = express(); const stack = [ express.json(), loggingMiddleware, - // baggageExtractorMiddleware, routeHandler, notFoundHandler, errorHandler, diff --git a/src_ts/app.ts b/src_ts/app.ts new file mode 100644 index 0000000..e21919e --- /dev/null +++ b/src_ts/app.ts @@ -0,0 +1,23 @@ +import express from "express"; +import { RequestHandler, Application, Express } from "express-serve-static-core"; + +import routeHandler from "./routes/index.ts"; +import { loggingMiddleware } from "./middleware/loggingMiddleware.ts"; +import { notFoundHandler } from "./middleware/notFoundHandler.ts"; +import { errorHandler } from "./middleware/errorHandler.ts"; + +const app: Express = express(); + +// Middleware array, including primary routes +const stack: RequestHandler[] = [ + express.json(), + loggingMiddleware, + routeHandler, + notFoundHandler, + errorHandler, +]; + +// Attach each middleware in the stack to the app +stack.forEach((middleware: RequestHandler) => app.use(middleware)); + +export default app; diff --git a/src_ts/clients/greetingClient.ts b/src_ts/clients/greetingClient.ts new file mode 100644 index 0000000..f71c467 --- /dev/null +++ b/src_ts/clients/greetingClient.ts @@ -0,0 +1,38 @@ +// Converted to TypeScript - Add meaningful types below +import grpc from "@grpc/grpc-js"; +import protoLoader from "@grpc/proto-loader"; + +import { protoLoaderOptions } from "./options.js"; +import { metadataInterceptor } from "../middleware/metadataInterceptor.js"; + +const protoPath = "./proto/com/acme/schema/v1/greeting.proto"; +const packageDefinition = protoLoader.loadSync(protoPath, protoLoaderOptions); +const proto = grpc.loadPackageDefinition(packageDefinition); +const serviceURL = process.env.GREETING_SERVICE_URL; +const { GreetingService, Language } = proto.com.acme.schema.v1; + +const client = new GreetingService( + serviceURL, + grpc.credentials.createInsecure(), + { + interceptors: [metadataInterceptor], + } +); + +export const fetchGreeting = async (acceptedLanguages): any => { + return new Promise((resolve, reject): any => { + const languageEnum = Language.type.value.reduce((acc, item): any => { + acc[item.name] = item.number; + return acc; + }, {}); + const preferredLanguage = acceptedLanguages.find((lang) => !!languageEnum[lang]); + const language = languageEnum[preferredLanguage] || Language.UNKNOWN; + + client.Fetch({ language }, (error, response): any => { + if (error) { + return reject(error); + } + resolve(response); + }); + }); +}; diff --git a/src_ts/clients/options.ts b/src_ts/clients/options.ts new file mode 100644 index 0000000..40dcfd8 --- /dev/null +++ b/src_ts/clients/options.ts @@ -0,0 +1,8 @@ +// Converted to TypeScript - Add meaningful types below +export const protoLoaderOptions = { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, +}; diff --git a/src_ts/clients/personClient.ts b/src_ts/clients/personClient.ts new file mode 100644 index 0000000..e3b328f --- /dev/null +++ b/src_ts/clients/personClient.ts @@ -0,0 +1,31 @@ +// Converted to TypeScript - Add meaningful types below +import grpc from "@grpc/grpc-js"; +import protoLoader from "@grpc/proto-loader"; + +import { protoLoaderOptions } from "./options.js"; +import { metadataInterceptor } from "../middleware/metadataInterceptor.js"; + +const protoPath = "./proto/com/acme/schema/v1/person.proto"; +const packageDefinition = protoLoader.loadSync(protoPath, protoLoaderOptions); +const proto = grpc.loadPackageDefinition(packageDefinition); +const serviceURL = process.env.PERSON_SERVICE_URL; +const { PersonService } = proto.com.acme.schema.v1; + +const client = new PersonService( + serviceURL, + grpc.credentials.createInsecure(), + { + interceptors: [metadataInterceptor], + } +); + +export const fetchPerson = async (uuid): any => { + return new Promise((resolve, reject): any => { + client.Fetch({ uuid }, (error, response): any => { + if (error) { + return reject(error); + } + resolve(response); + }); + }); +}; diff --git a/src_ts/controllers/v1/errorController.ts b/src_ts/controllers/v1/errorController.ts new file mode 100644 index 0000000..8f53356 --- /dev/null +++ b/src_ts/controllers/v1/errorController.ts @@ -0,0 +1,7 @@ +import { Request, Response, NextFunction } from "express-serve-static-core" + +export const simulateError = (req: Request, res: Response, next: NextFunction): void => { + const error = new Error("Test error"); + + next(error); +}; diff --git a/src_ts/controllers/v1/greetingController.ts b/src_ts/controllers/v1/greetingController.ts new file mode 100644 index 0000000..1d4f8fa --- /dev/null +++ b/src_ts/controllers/v1/greetingController.ts @@ -0,0 +1,28 @@ +import { Request, Response, NextFunction } from "express-serve-static-core" + +import { fetchGreeting } from "../../clients/greetingClient.ts"; +import { fetchPerson } from "../../clients/personClient.ts"; + +export const greet = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + let acceptedLanguages = req.headers["accept-language"]?.split(",") || []; + acceptedLanguages = acceptedLanguages.map((lang) => lang.split(";")[0].trim().toUpperCase().replace(/-/g, "_")); // Normalize languages + + const id = req.query.personID || req.params.personID; + + const [greetingResponse, personResponse] = await Promise.all([ + fetchGreeting(acceptedLanguages), + fetchPerson(id), + ]); + + res.status(200).json({ + data: { + language: greetingResponse.language, + greeting: greetingResponse.greeting, + audience: personResponse.name + }, + }); + } catch (error) { + next(error); + } +}; diff --git a/src_ts/index.ts b/src_ts/index.ts new file mode 100644 index 0000000..137c5e8 --- /dev/null +++ b/src_ts/index.ts @@ -0,0 +1,10 @@ +// Converted to TypeScript - Add meaningful types below +import { LOG_LEVEL } from "./lib/log.ts"; +import app from "./app.ts"; + +const PORT = process.env.PORT || 8080; + +app.listen(PORT, (): any => { + console.info(`Log level is set to ${LOG_LEVEL}`); + console.info(`Server is running on port ${PORT}`); +}); diff --git a/src_ts/lib/log.ts b/src_ts/lib/log.ts new file mode 100644 index 0000000..10b2671 --- /dev/null +++ b/src_ts/lib/log.ts @@ -0,0 +1,24 @@ +enum Levels { + debug = 0, + info = 1, + warn = 2, + error = 3, +} + +const levels = Levels; + +export const LOG_LEVEL = process.env.LOG_LEVEL || Object.keys(levels)[levels.info]; + +const currentLevel = parseInt(levels[LOG_LEVEL.toLowerCase()]); + +if (currentLevel > levels.debug) { + console.debug = (): any => {}; +} + +if (currentLevel > levels.info) { + console.info = (): any => {}; +} + +if (currentLevel > levels.warn) { + console.warn = (): any => {}; +} diff --git a/src_ts/middleware/errorHandler.ts b/src_ts/middleware/errorHandler.ts new file mode 100644 index 0000000..278300e --- /dev/null +++ b/src_ts/middleware/errorHandler.ts @@ -0,0 +1,5 @@ +// Converted to TypeScript - Add meaningful types below +export const errorHandler = (err, req, res): any => { + console.error(err.stack); + res.status(500).send("Something went wrong!"); +}; diff --git a/src_ts/middleware/loggingMiddleware.ts b/src_ts/middleware/loggingMiddleware.ts new file mode 100644 index 0000000..f80c42c --- /dev/null +++ b/src_ts/middleware/loggingMiddleware.ts @@ -0,0 +1,34 @@ +// Converted to TypeScript - Add meaningful types below +export const loggingMiddleware = (req, res, next): any => { + const start = Date.now(); + + const base = { + timestamp: new Date().toISOString(), + method: req.method, + path: `${req.baseUrl}${req.path}`, + params: req.query || req.params, + }; + + // Hook into the response lifecycle to log when it finishes + res.on("finish", (): any => { + const duration = Date.now() - start; + const log = { + status: res.statusCode, + duration: `${duration}ms`, + }; + console.info(JSON.stringify({ ...base, ...log })); + }); + + // Hook into the response lifecycle to log errors + res.on("error", (err): any => { + const duration = Date.now() - start; + const log = { + status: res.statusCode || 500, + duration: `${duration}ms`, + error: err.message, + }; + console.error(JSON.stringify({ ...base, ...log })); + }); + + next(); +}; diff --git a/src_ts/middleware/metadataInterceptor.ts b/src_ts/middleware/metadataInterceptor.ts new file mode 100644 index 0000000..df84580 --- /dev/null +++ b/src_ts/middleware/metadataInterceptor.ts @@ -0,0 +1,21 @@ +// Converted to TypeScript - Add meaningful types below +import grpc from "@grpc/grpc-js"; +import { context, propagation } from "@opentelemetry/api"; + +export const metadataInterceptor = (options, nextCall): any => { + return new grpc.InterceptingCall(nextCall(options), { + start(metadata, listener, next) { + const currentContext = context.active(); + const baggage = propagation.getBaggage(currentContext); + + if (baggage) { + const entries = baggage.getAllEntries(); + const baggageString = entries.map(([key, value]): any => `${key}=${value.value}`): any.join(","): any; + + metadata.set("baggage", baggageString); + } + + next(metadata, listener); + }, + }); +}; diff --git a/src_ts/middleware/notFoundHandler.ts b/src_ts/middleware/notFoundHandler.ts new file mode 100644 index 0000000..c5ef503 --- /dev/null +++ b/src_ts/middleware/notFoundHandler.ts @@ -0,0 +1,4 @@ +// Converted to TypeScript - Add meaningful types below +export const notFoundHandler = (req, res): any => { + res.status(404).send("Route not found"); +}; diff --git a/src_ts/routes/index.ts b/src_ts/routes/index.ts new file mode 100644 index 0000000..058f6df --- /dev/null +++ b/src_ts/routes/index.ts @@ -0,0 +1,11 @@ +// Converted to TypeScript - Add meaningful types below +import { Router } from "express"; + +import v1Routes from "./v1/index.js"; + +const router = Router(); + +// Mount versioned routes +router.use("/api/v1", v1Routes); + +export default router; diff --git a/src_ts/routes/v1/errorRoutes.ts b/src_ts/routes/v1/errorRoutes.ts new file mode 100644 index 0000000..b254036 --- /dev/null +++ b/src_ts/routes/v1/errorRoutes.ts @@ -0,0 +1,10 @@ +// Converted to TypeScript - Add meaningful types below +import { Router } from "express"; + +import { simulateError } from "../../controllers/v1/errorController.js"; + +const router = Router(); + +router.get("/boom", simulateError); + +export default router; diff --git a/src_ts/routes/v1/greetingRoutes.ts b/src_ts/routes/v1/greetingRoutes.ts new file mode 100644 index 0000000..048e32e --- /dev/null +++ b/src_ts/routes/v1/greetingRoutes.ts @@ -0,0 +1,11 @@ +import { Router as r } from "express"; +import { Router, RequestHandler } from "express-serve-static-core" + +import { greet } from "../../controllers/v1/greetingController.js"; + +const router: Router = r(); +const greetHandler: RequestHandler = greet; + +router.get("/hello", greetHandler); + +export default router; diff --git a/src_ts/routes/v1/index.ts b/src_ts/routes/v1/index.ts new file mode 100644 index 0000000..5e46287 --- /dev/null +++ b/src_ts/routes/v1/index.ts @@ -0,0 +1,13 @@ +// Converted to TypeScript - Add meaningful types below +import { Router } from "express"; + +import greetingRoutes from "./greetingRoutes.js"; +import errorRoutes from "./errorRoutes.js"; + +const router = Router(); + +// Mount routes +router.use(greetingRoutes); +router.use(errorRoutes); + +export default router; From 4d07377659cb67576da3af74b15c0edcc54f0c2a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 14 Jan 2025 16:03:40 +0000 Subject: [PATCH 2/3] Bump version to 0.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9ba004..bb358ff 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "acme-node", "type": "module", - "version": "0.7.1", + "version": "0.8.0", "description": "Sample Application with developer friendly boilerplate", "scripts": { "start": "node ./src/index.js", From d5e7bd7a5a094e51447e9ad4356a0f1d10dccbd6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 14 Jan 2025 18:52:32 +0000 Subject: [PATCH 3/3] Bump version to 0.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb358ff..22f3d70 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "acme-node", "type": "module", - "version": "0.8.0", + "version": "0.7.2", "description": "Sample Application with developer friendly boilerplate", "scripts": { "start": "node ./src/index.js",