diff --git a/README.md b/README.md index 3a027c3..bb19ea2 100644 --- a/README.md +++ b/README.md @@ -92,9 +92,12 @@ docker-compose down ├── .build # Project (TypeScript) build directory └── src # Source files ├── constants # Constant values + ├── controllers # API controllers + ├── lib # 3rd party libraries (initialized/extended etc) ├── models # Database models ├── routes # API endpoints - └── scripts # Project scripts + ├── scripts # Project scripts + └── utils # Project utilities ``` ## Versioning @@ -156,6 +159,13 @@ This section includes the issues, changes & improvements I've made, with the tho > I used `Decimal128` as suggested in [this](https://docs.mongodb.com/manual/tutorial/model-monetary-data) official `MongoDB` document. > For mathematical calculations in the project we can use [bignumber.js](https://www.npmjs.com/package/bignumber.js), > to handle the precision appropriately. + - Unused/Redundant usage of `express()` & `cors()` in the `src/routes/simulator.router.ts` file. + - Redundant `/api` prefix used in the routes. I instead used a more useful `/v{major}` prefix. (`/v1`) + > Using `/v{major}` route prefix allows to possibility of deploying breaking changes without loosing backward compatibility. + - Unuseful `console.log` usage. for logging purposes, it's better use a proper logging library or service such as `Sentry`. + - Inconsistent API response format. + > Some controllers directly send the database record(s) as the response, some send them wrapped inside an object. + - No request validation was in place for any of the controllers. ### Improvements @@ -174,12 +184,26 @@ This section includes the issues, changes & improvements I've made, with the tho > for this purpose, I used `imports` property in `package.json` to avoid using unnecessary third-party application, > which improves both the startup time and security, due to the fact that `Node.js` will only apply these path aliases to the current package, > meaning no installed dependency can use these path aliases (which could cause security issues as well) +- Improvements in the `src` directory: - Added the `src/models/index.ts` file to provide easier & cleaner access to all database models throughout the entire project. + > I didn't use `export default` for the models, because in the `index.ts` I wanted to be able to easily use `export * from "some-model"`. - Added the `src/constants` directory to move all constant values there instead of being scattered all over the project. > This ensures that the project is using the same consistent values everywhere. > `MODEL` & `COLLECTION` constants are also added, due to the fact that they can be useful in scenarios such as `$lookup` aggregation stages. > Also In case of need to change the said values, it just needs to be updated in one places only. + - Added the `src/routes/index.ts` file to provide cleaner api endpoint management. + > I exported the routes as `default` in this case, due to only having one job, which is exporting the express `Router`. + - Renamed the router filenames. The `.router` part of the name was redundant, + since they're already under the `routes` directory. + - Moved the controllers to the `controllers` directory & each controller to be in a separate file. + > This makes code cleaner and easier to maintain. not the directory `routes` only manages the routing & + > the `controllers` directory manages route behaviors. + > By putting controllers in separate files, it will become easier to detect which dependencies are used in which controller + > and the number of controllers won't affect the readability of the code, thus easier to improve, debug & maintain. + - Added the `wrapController` utility. + > `express` doesn't catch async errors properly, so I added this utility to wrap the controllers before passing them + > to the `Router` instance. diff --git a/package-lock.json b/package-lock.json index e7fd007..1d9ee3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "body-parser": "1.19.2", + "celebrate": "15.0.1", "cors": "2.8.5", "dotenv": "16.0.0", "express": "4.17.3", @@ -57,6 +58,19 @@ "node": ">= 4" } }, + "node_modules/@hapi/hoek": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz", + "integrity": "sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -112,6 +126,24 @@ "node": ">= 8" } }, + "node_modules/@sideway/address": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz", + "integrity": "sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", + "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -676,6 +708,16 @@ "node": ">=6" } }, + "node_modules/celebrate": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/celebrate/-/celebrate-15.0.1.tgz", + "integrity": "sha512-K2y221k10u+K2t9w25802qXh8h1mVWZf+6pl7zHdlhhwzrOSQFnnw+GsR8k17oyn4Y3fVErBGsO/+CeW8N7aRQ==", + "dependencies": { + "escape-html": "1.0.3", + "joi": "17.x.x", + "lodash": "4.17.x" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1610,6 +1652,18 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "node_modules/joi": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz", + "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==", + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -1652,6 +1706,11 @@ "node": ">= 0.8.0" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2789,6 +2848,19 @@ } } }, + "@hapi/hoek": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz", + "integrity": "sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw==" + }, + "@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, "@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -2832,6 +2904,24 @@ "fastq": "^1.6.0" } }, + "@sideway/address": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz", + "integrity": "sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", + "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -3232,6 +3322,16 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "celebrate": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/celebrate/-/celebrate-15.0.1.tgz", + "integrity": "sha512-K2y221k10u+K2t9w25802qXh8h1mVWZf+6pl7zHdlhhwzrOSQFnnw+GsR8k17oyn4Y3fVErBGsO/+CeW8N7aRQ==", + "requires": { + "escape-html": "1.0.3", + "joi": "17.x.x", + "lodash": "4.17.x" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3941,6 +4041,18 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "joi": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz", + "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==", + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" + } + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3977,6 +4089,11 @@ "type-check": "~0.4.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", diff --git a/package.json b/package.json index e26adea..ef31c5d 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "body-parser": "1.19.2", + "celebrate": "15.0.1", "cors": "2.8.5", "dotenv": "16.0.0", "express": "4.17.3", diff --git a/src/api.ts b/src/api.ts index 5a0e250..db3905c 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,8 +1,7 @@ import { CORS_ORIGINS, DBURL, PORT } from "#src/config.js"; -import { router as favoriteRouter } from "#src/routes/favorite.router.js"; -import { router as profileRouter } from "#src/routes/profile.router.js"; -import { router as simulatorRouter } from "#src/routes/simulator.router.js"; +import routes from "#src/routes/index.js"; import bodyParser from "body-parser"; +import { errors } from "celebrate"; import cors from "cors"; import express from "express"; import mongoose from "mongoose"; @@ -17,9 +16,9 @@ const app = express(); app.use(cors({ origin: CORS_ORIGINS })); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); -app.use(favoriteRouter); -app.use(profileRouter); -app.use(simulatorRouter); +app.use(routes); + +app.use(errors()); app.listen(PORT, () => console.log(`✅ Ready on port http://localhost:${PORT}`), diff --git a/src/controllers/index.ts b/src/controllers/index.ts new file mode 100644 index 0000000..7092767 --- /dev/null +++ b/src/controllers/index.ts @@ -0,0 +1 @@ +export * from "./v1/index.js"; diff --git a/src/controllers/v1/favorites/index.ts b/src/controllers/v1/favorites/index.ts new file mode 100644 index 0000000..329ae87 --- /dev/null +++ b/src/controllers/v1/favorites/index.ts @@ -0,0 +1 @@ +export * from "./list.js"; diff --git a/src/controllers/v1/favorites/list.ts b/src/controllers/v1/favorites/list.ts new file mode 100644 index 0000000..67057bc --- /dev/null +++ b/src/controllers/v1/favorites/list.ts @@ -0,0 +1,26 @@ +import { Joi } from "#src/lib/index.js"; +import { Favorite, FavoriteI } from "#src/models/index.js"; +import { celebrate, Segments } from "celebrate"; +import { RequestHandler } from "express"; +import { FilterQuery } from "mongoose"; + +export const listFavoritesV1: RequestHandler[] = [ + celebrate({ + [Segments.PARAMS]: Joi.object() + .keys({ + profile_id: Joi.string().objectId().required(), + }) + .required(), + }), + async (req, res) => { + const { profile_id } = req.params; + + const query: FilterQuery = {}; + + if (profile_id != null) query.profile_id = profile_id; + + const favorites = await Favorite.find(query).lean(); + + res.json({ favorites }); + }, +]; diff --git a/src/controllers/v1/index.ts b/src/controllers/v1/index.ts new file mode 100644 index 0000000..4b93b8a --- /dev/null +++ b/src/controllers/v1/index.ts @@ -0,0 +1,3 @@ +export * from "./favorites/index.js"; +export * from "./profiles/index.js"; +export * from "./simulators/index.js"; diff --git a/src/controllers/v1/profiles/index.ts b/src/controllers/v1/profiles/index.ts new file mode 100644 index 0000000..a4c28f8 --- /dev/null +++ b/src/controllers/v1/profiles/index.ts @@ -0,0 +1,2 @@ +export * from "./list.js"; +export * from "./post.js"; diff --git a/src/controllers/v1/profiles/list.ts b/src/controllers/v1/profiles/list.ts new file mode 100644 index 0000000..b77ee9b --- /dev/null +++ b/src/controllers/v1/profiles/list.ts @@ -0,0 +1,10 @@ +import { Profile } from "#src/models/index.js"; +import { RequestHandler } from "express"; + +export const listProfilesV1: RequestHandler[] = [ + async (req, res) => { + const profiles = await Profile.find().lean(); + + res.json({ profiles }); + }, +]; diff --git a/src/controllers/v1/profiles/post.ts b/src/controllers/v1/profiles/post.ts new file mode 100644 index 0000000..5df7b66 --- /dev/null +++ b/src/controllers/v1/profiles/post.ts @@ -0,0 +1,29 @@ +import { Joi } from "#src/lib/index.js"; +import { Profile } from "#src/models/index.js"; +import { celebrate, Segments } from "celebrate"; +import { RequestHandler } from "express"; + +export const postProfileV1: RequestHandler[] = [ + celebrate({ + [Segments.BODY]: Joi.object() + .keys({ + email: Joi.string().email().required(), + name: Joi.string().min(3).required(), + nickname: Joi.string().min(3).required(), + }) + .required(), + }), + async (req, res) => { + const { email, name, nickname } = req.body; + + let profile = await Profile.findOne({ + $or: [{ email }, { nickname }], + }); + + if (!profile) { + profile = await Profile.create({ name, email, nickname }); + } + + res.json({ profile }); + }, +]; diff --git a/src/controllers/v1/simulators/index.ts b/src/controllers/v1/simulators/index.ts new file mode 100644 index 0000000..a4c28f8 --- /dev/null +++ b/src/controllers/v1/simulators/index.ts @@ -0,0 +1,2 @@ +export * from "./list.js"; +export * from "./post.js"; diff --git a/src/controllers/v1/simulators/list.ts b/src/controllers/v1/simulators/list.ts new file mode 100644 index 0000000..1327207 --- /dev/null +++ b/src/controllers/v1/simulators/list.ts @@ -0,0 +1,26 @@ +import { Joi } from "#src/lib/index.js"; +import { Simulator, SimulatorI } from "#src/models/index.js"; +import { celebrate, Segments } from "celebrate"; +import { RequestHandler } from "express"; +import { FilterQuery } from "mongoose"; + +export const listSimulatorsV1: RequestHandler[] = [ + celebrate({ + [Segments.PARAMS]: Joi.object() + .keys({ + profile_id: Joi.string().objectId().required(), + }) + .required(), + }), + async (req, res) => { + const { profile_id } = req.params; + + const query: FilterQuery = {}; + + if (profile_id != null) query.profile_id = profile_id; + + const simulators = await Simulator.find(query).lean(); + + res.json({ simulators }); + }, +]; diff --git a/src/controllers/v1/simulators/post.ts b/src/controllers/v1/simulators/post.ts new file mode 100644 index 0000000..19fab54 --- /dev/null +++ b/src/controllers/v1/simulators/post.ts @@ -0,0 +1,33 @@ +import { Joi } from "#src/lib/index.js"; +import { Simulator } from "#src/models/index.js"; +import { celebrate, Segments } from "celebrate"; +import { RequestHandler } from "express"; + +export const postSimulatorV1: RequestHandler[] = [ + celebrate({ + [Segments.PARAMS]: Joi.object() + .keys({ + profile_id: Joi.string().objectId().required(), + }) + .required(), + [Segments.BODY]: Joi.object() + .keys({ + recorded_at: Joi.date().required(), + cryptocurrency: Joi.string().required(), // TODO: Use only accepted values. needs information. + euros: Joi.string().decimal128().required(), + price: Joi.string().decimal128().required(), + quantity: Joi.string().decimal128().required(), + }) + .required(), + }), + async (req, res) => { + const { profile_id } = req.params; + + const simulator = await Simulator.create({ + ...req.body, + profile_id, + }); + + res.json({ simulator }); + }, +]; diff --git a/src/lib/Joi.ts b/src/lib/Joi.ts new file mode 100644 index 0000000..f7ca200 --- /dev/null +++ b/src/lib/Joi.ts @@ -0,0 +1,54 @@ +import { Joi as BaseJoi } from "celebrate"; +import type { Root, StringSchema } from "joi"; +import { Types } from "mongoose"; + +/* ------------------------- Library ------------------------- */ + +export const Joi: JoiI = BaseJoi.extend({ + base: BaseJoi.string(), + type: "string", + messages: { + "string.objectId": "needs to be a valid ObjectId", + "string.decimal128": "needs to be a valid decimal128", + }, + rules: { + objectId: { + method() { + return this.$_addRule("objectId"); + }, + validate(value, helpers) { + if (!Types.ObjectId.isValid(value)) return helpers.error("string.objectId"); + + return new Types.ObjectId(value); + }, + }, + decimal128: { + method() { + return this.$_addRule("decimal128"); + }, + validate(value, helpers) { + if (isNaN(parseFloat(value))) return helpers.error("string.decimal128"); + + return Types.Decimal128.fromString(value); + }, + }, + }, +}); + +/* ------------------------- Interfaces ------------------------- */ + +export interface JoiI extends Root { + string(): JoiStringSchemaI; +} + +export interface JoiStringSchemaI extends StringSchema { + /** + * Requires the string value to be a valid ObjectId. + */ + objectId(): this; + + /** + * Requires the string value to be a valid Decimal128. + */ + decimal128(): this; +} diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..fa2429e --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +export * from "./Joi.js"; diff --git a/src/models/Profile.ts b/src/models/Profile.ts index 441a367..824ba52 100644 --- a/src/models/Profile.ts +++ b/src/models/Profile.ts @@ -14,6 +14,7 @@ const schema = new Schema( }, nickname: { type: String, + required: true, }, capital: { type: Schema.Types.Decimal128, @@ -40,7 +41,7 @@ export const Profile = model(MODEL.PROFILE, schem export interface ProfileI { email: `${string}@${string}.${string}`; name: string; - nickname?: string; + nickname: string; capital: Types.Decimal128; divisa?: string; prefered_cryptocurrency?: string; diff --git a/src/routes/favorite.router.ts b/src/routes/favorite.router.ts deleted file mode 100644 index fbd1296..0000000 --- a/src/routes/favorite.router.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Favorite } from "#src/models/index.js"; -import express from "express"; - -export const router = express.Router(); - -router.get("/api/favorite", async (req, res) => { - const favorite = await Favorite.find().lean(); - console.log(favorite); - res.json({ favorite }); -}); - -router.get("/api/favorite/:profile_id", async (req, res) => { - console.log(req.params); - let query = {}; - const { profile_id } = req.params; - query = { profile_id }; - console.log(query); - const data = await Favorite.find(query); - res.json(data); -}); diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..766cc71 --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,9 @@ +import { Router } from "express"; +import v1 from "./v1/index.js"; + +const router = Router(); + +router + .use(v1); + +export default router; diff --git a/src/routes/profile.router.ts b/src/routes/profile.router.ts deleted file mode 100644 index 5cfb709..0000000 --- a/src/routes/profile.router.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Profile } from "#src/models/index.js"; -import express from "express"; - -export const router = express.Router(); - -router.get("/api/profile", async (req, res) => { - const profile = await Profile.find().lean(); - console.log(profile); - res.json({ profile }); -}); - -router.post("/api/profile", async (req, res) => { - const { email, name, nickname } = req.body; - - let profile = await Profile.findOne({ - $or: [{ email }, { nickname }], - }).exec(); - - if (!profile) { - profile = await Profile.create({ name, email, nickname }); - } - - res.json(profile); -}); diff --git a/src/routes/simulator.router.ts b/src/routes/simulator.router.ts deleted file mode 100644 index 47c39d6..0000000 --- a/src/routes/simulator.router.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Simulator } from "#src/models/index.js"; -import cors from "cors"; -import express from "express"; - -const app = express(); -app.use(cors()); - -export const router = express.Router(); - -router.get("/api/simulator", async (req, res) => { - const simulator = await Simulator.find().lean(); - console.log(simulator); - res.json({ simulator }); -}); - -router.get("/api/simulator/:profile_id", async (req, res) => { - console.log("========== "); - let query = {}; - const { profile_id } = req.params; - console.log({ profile_id }); - query = { profile_id }; - const data = await Simulator.find(query); - res.json(data); -}); - -router.post("/api/simulator/:profile_id", async (req, res) => { - const { profile_id } = req.params; - const newData = { - ...req.body, - profile_id, - }; - console.log(newData); - const simulator = await Simulator.create(newData); - res.json(simulator); -}); diff --git a/src/routes/v1/favorites.ts b/src/routes/v1/favorites.ts new file mode 100644 index 0000000..39643ac --- /dev/null +++ b/src/routes/v1/favorites.ts @@ -0,0 +1,13 @@ +import { listFavoritesV1 } from "#src/controllers/index.js"; +import { wrapRequestHandlers } from "#src/utils/index.js"; +import { Router } from "express"; + +const router = Router(); + +router.route("/favorite") + .get(...wrapRequestHandlers(listFavoritesV1)); + +router.route("/favorite/:profile_id") + .get(...wrapRequestHandlers(listFavoritesV1)); + +export default router; diff --git a/src/routes/v1/index.ts b/src/routes/v1/index.ts new file mode 100644 index 0000000..ee5dfe8 --- /dev/null +++ b/src/routes/v1/index.ts @@ -0,0 +1,13 @@ +import { Router } from "express"; +import favorites from "./favorites.js"; +import profiles from "./profiles.js"; +import simulators from "./simulators.js"; + +const router = Router(); + +router + .use(favorites) + .use(profiles) + .use(simulators); + +export default Router().use("/v1", router); diff --git a/src/routes/v1/profiles.ts b/src/routes/v1/profiles.ts new file mode 100644 index 0000000..5b03460 --- /dev/null +++ b/src/routes/v1/profiles.ts @@ -0,0 +1,11 @@ +import { listProfilesV1, postProfileV1 } from "#src/controllers/index.js"; +import { wrapRequestHandlers } from "#src/utils/index.js"; +import { Router } from "express"; + +const router = Router(); + +router.route("/profile") + .get(...wrapRequestHandlers(listProfilesV1)) + .post(...wrapRequestHandlers(postProfileV1)); + +export default router; diff --git a/src/routes/v1/simulators.ts b/src/routes/v1/simulators.ts new file mode 100644 index 0000000..c2cf811 --- /dev/null +++ b/src/routes/v1/simulators.ts @@ -0,0 +1,14 @@ +import { listSimulatorsV1, postSimulatorV1 } from "#src/controllers/index.js"; +import { wrapRequestHandlers } from "#src/utils/index.js"; +import { Router } from "express"; + +const router = Router(); + +router.route("/simulator") + .get(...wrapRequestHandlers(listSimulatorsV1)); + +router.route("/simulator/:profile_id") + .get(...wrapRequestHandlers(listSimulatorsV1)) + .post(...wrapRequestHandlers(postSimulatorV1)); + +export default router; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..119116e --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./wrap-request-handlers.js"; diff --git a/src/utils/wrap-request-handlers.ts b/src/utils/wrap-request-handlers.ts new file mode 100644 index 0000000..8bf8954 --- /dev/null +++ b/src/utils/wrap-request-handlers.ts @@ -0,0 +1,13 @@ +import { Handler } from "express"; + +export function wrapRequestHandlers(handlers: Handler[]): Handler[] { + return handlers.map(handler => (req, res, next) => { + try { + const result: unknown = handler(req, res, next); + + if (result instanceof Promise) result.catch(error => next(error)); + } catch (error) { + next(error); + } + }); +}