diff --git a/package.json b/package.json index c8fdb30..b4660f5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "module-3": "nodemon --exec babel-node ./src/module-3/index.ts --extensions .ts", + "module-4": "nodemon --exec babel-node ./src/module-3/index.ts --extensions .ts", "lint": "eslint . --ext .ts" }, "author": "", diff --git a/src/module-3/controllers/groups.ts b/src/module-3/controllers/groups.ts new file mode 100644 index 0000000..29ce3e8 --- /dev/null +++ b/src/module-3/controllers/groups.ts @@ -0,0 +1,82 @@ +import express from "express"; +import { createValidator } from "express-joi-validation"; +import * as GroupsService from "../services/groups"; +import { Errors, GroupModel } from "../types"; +import { isNull } from "./utils"; +import groupValidationSchema from "./validation/groups"; + +const validator = createValidator(); +const router = express.Router(); + +/* Get all groups */ + +router.get("/", async (req, res) => { + try { + const groups = await GroupsService.findAll(); + res.status(200).send(groups); + } catch (e) { + res.status(500).send(e.message); + } +}); + +/* Find group by id */ + +router.get("/:id", async (req, res) => { + try { + const id = req.params.id; + const group = await GroupsService.find(id); + if (group) { + return res.status(200).send(group); + } + res.status(404).send("Group not found"); + } catch (e) { + res.status(500).send(e.message); + } +}); + +/* Create new group */ + +router.post("/", validator.body(groupValidationSchema), async (req, res) => { + try { + const body: GroupModel = req.body; + const isSuccess = await GroupsService.create(body); + if (!isSuccess) { + return res.status(400).send("Group with this name is already exists"); + } + res.redirect("/api/groups"); + } catch (e) { + res.status(500).send(e.message); + } +}); + +/* Delete group */ + +router.delete("/:id", async (req, res) => { + try { + const id = req.params.id; + const isSuccess = await GroupsService.remove(id); + if (isNull(isSuccess)) { + return res.status(404).send("Something wrong"); + } + res.redirect("/api/groups"); + } catch (e) { + res.status(500).send(e.message); + } +}); + +/* Update group */ + +router.put("/:id", validator.body(groupValidationSchema), async (req, res) => { + try { + const id = req.params.id; + const status = await GroupsService.update(id, req.body); + if ((status as Errors).type === "error") { + return res.status(404).send((status as Errors).message); + } + res.redirect("/api/groups"); + } catch (e) { + res.status(500).send(e.message); + } +}); + +export default router; diff --git a/src/module-3/controllers/user-groups.ts b/src/module-3/controllers/user-groups.ts new file mode 100644 index 0000000..e83b280 --- /dev/null +++ b/src/module-3/controllers/user-groups.ts @@ -0,0 +1,37 @@ +import express from "express"; +import { createValidator } from "express-joi-validation"; +import { AddUsersToGroupModel } from "../services/types"; +import * as UserGroupsService from "../services/user-groups"; +import userGroupValidationSchema from "./validation/user-groups"; + +const validator = createValidator(); +const router = express.Router(); + +/* Get user-groups list */ + +router.get("/", async (req, res) => { + try { + const userGroups = await UserGroupsService.findAll(); + res.status(200).send(userGroups); + } catch (e) { + res.status(500).send(e.message); + } +}); + +/* Add user to any group by id*/ + +router.post( + "/", + validator.body(userGroupValidationSchema), + async (req, res) => { + try { + const body: AddUsersToGroupModel = req.body; + await UserGroupsService.addUsersToGroup(body.groupId, body.userIds); + res.redirect("/api/user-groups"); + } catch (e) { + res.status(500).send(e.message); + } + } +); + +export default router; diff --git a/src/module-3/controllers/users.ts b/src/module-3/controllers/users.ts index a64a098..f9b8b9c 100644 --- a/src/module-3/controllers/users.ts +++ b/src/module-3/controllers/users.ts @@ -1,7 +1,7 @@ import express from "express"; import { createValidator } from "express-joi-validation"; -import { UserModel } from "../types"; import * as UsersService from "../services/users"; +import { UserModel } from "../types"; import { isNull } from "./utils"; import userValidationSchema from "./validation/users"; diff --git a/src/module-3/controllers/validation/groups.ts b/src/module-3/controllers/validation/groups.ts new file mode 100644 index 0000000..dc8136c --- /dev/null +++ b/src/module-3/controllers/validation/groups.ts @@ -0,0 +1,6 @@ +import joi from "joi"; + +export default joi.object().keys({ + name: joi.string().alphanum().min(3).max(10).required(), + permissions: joi.array().required() +}); diff --git a/src/module-3/controllers/validation/user-groups.ts b/src/module-3/controllers/validation/user-groups.ts new file mode 100644 index 0000000..b8cbfc4 --- /dev/null +++ b/src/module-3/controllers/validation/user-groups.ts @@ -0,0 +1,6 @@ +import joi from "joi"; + +export default joi.object().keys({ + userIds: joi.array().required(), + groupId: joi.string().required() +}); diff --git a/src/module-3/controllers/validation/users.ts b/src/module-3/controllers/validation/users.ts index 51527c8..0917992 100644 --- a/src/module-3/controllers/validation/users.ts +++ b/src/module-3/controllers/validation/users.ts @@ -1,9 +1,7 @@ import joi from "joi"; -const userValidationSchema = joi.object().keys({ +export default joi.object().keys({ login: joi.string().alphanum().min(3).max(10).required(), password: joi.string().alphanum().required(), age: joi.number().min(4).max(130).required() }); - -export default userValidationSchema; diff --git a/src/module-3/data-access/index.ts b/src/module-3/data-access/index.ts index daaef92..0271528 100644 --- a/src/module-3/data-access/index.ts +++ b/src/module-3/data-access/index.ts @@ -9,4 +9,10 @@ if (!process.env.DB_URL) { const DB_URL = process.env.DB_URL; -export default new Sequelize(DB_URL); +export default new Sequelize(DB_URL, { + pool: { + max: 5, + min: 0, + idle: 10000 + } +}); diff --git a/src/module-3/index.ts b/src/module-3/index.ts index 75b727d..c79e912 100644 --- a/src/module-3/index.ts +++ b/src/module-3/index.ts @@ -1,4 +1,6 @@ import express from "express"; +import groupsRouter from "./controllers/groups"; +import userGroupsRouter from "./controllers/user-groups"; import usersRouter from "./controllers/users"; import db from "./data-access"; @@ -13,6 +15,10 @@ app.listen(3000, () => ) ) .then(() => app.use(express.json())) - .then(() => app.use("/api/users", usersRouter)) + .then(() => { + app.use("/api/groups", groupsRouter); + app.use("/api/users", usersRouter); + app.use("/api/user-groups", userGroupsRouter); + }) .catch((err) => console.error(err)) ); diff --git a/src/module-3/models/group.ts b/src/module-3/models/group.ts new file mode 100644 index 0000000..6a66a99 --- /dev/null +++ b/src/module-3/models/group.ts @@ -0,0 +1,20 @@ +import { DataTypes, Model, ModelCtor } from "sequelize"; +import db from "../data-access"; +import { GroupModel } from "../types"; + +const Group: ModelCtor> = db.define( + "groups", + { + name: { + type: DataTypes.STRING + }, + permissions: { + type: DataTypes.ARRAY(DataTypes.CHAR) + } + }, + { + timestamps: false + } +); + +export default Group; diff --git a/src/module-3/models/user-group.ts b/src/module-3/models/user-group.ts new file mode 100644 index 0000000..aa73e5f --- /dev/null +++ b/src/module-3/models/user-group.ts @@ -0,0 +1,20 @@ +import { DataTypes, Model, ModelCtor } from "sequelize"; +import db from "../data-access"; +import { UserGroupModel } from "../types"; + +const UserGroup: ModelCtor> = db.define( + "user-groups", + { + groupId: { + type: DataTypes.STRING + }, + userId: { + type: DataTypes.STRING + } + }, + { + timestamps: false + } +); + +export default UserGroup; diff --git a/src/module-3/services/groups.ts b/src/module-3/services/groups.ts new file mode 100644 index 0000000..5dd1bdf --- /dev/null +++ b/src/module-3/services/groups.ts @@ -0,0 +1,85 @@ +import { Model } from "sequelize/types"; +import sequelize from "../data-access"; +import Group from "../models/group"; +import { Errors, GroupModel } from "../types"; +import * as UserGroupsService from "./user-groups"; +import { isEqualsObjects } from "./utils"; + +export const find = async (id: string) => + Group.findOne({ + where: { + id + } + }); + +export const findAll = async () => + Group.findAll({ + raw: true + }); + +export const create = async (model: GroupModel) => { + const { name } = model; + const group = await Group.findOne({ + where: { + name + } + }); + + if (group) { + return null; + } + + return Group.create({ + ...model + }); +}; + +export const remove = async (id: string) => { + const transaction = await sequelize.transaction(); + + try { + const group = await find(id); + + if (!group) { + transaction.rollback(); + return null; + } + + await UserGroupsService.remove(transaction, { + groupId: id + }); + + await group.destroy({ transaction }); + await transaction.commit(); + } catch (error) { + console.error(error); + transaction.rollback(); + } +}; + +export const update = async ( + id: string, + model: GroupModel +): Promise | Errors> => { + const group = await find(id); + const { name, permissions } = model; + + if (!group) { + return { + type: "error", + message: "Group with this id not found" + }; + } + + if (isEqualsObjects(group.get(), model)) { + return { + type: "error", + message: "Input data is equal with exist data. Forbidden" + }; + } + + return group.update({ + name, + permissions + }); +}; diff --git a/src/module-3/services/types.ts b/src/module-3/services/types.ts new file mode 100644 index 0000000..3179ba9 --- /dev/null +++ b/src/module-3/services/types.ts @@ -0,0 +1,10 @@ +import { BaseUser, GroupModel, UserGroupModel } from "../types"; + +export type AddUsersToGroupModel = Omit & { + userIds: Array; +}; + +export interface RemoveUserGroupParams { + userId?: BaseUser["id"]; + groupId?: GroupModel["id"]; +} diff --git a/src/module-3/services/user-groups.ts b/src/module-3/services/user-groups.ts new file mode 100644 index 0000000..3174849 --- /dev/null +++ b/src/module-3/services/user-groups.ts @@ -0,0 +1,86 @@ +import { Transaction } from "sequelize/types"; +import sequelize from "../data-access"; +import UserGroup from "../models/user-group"; +import { UserGroupModel } from "../types"; +import { find as findGroupById } from "./groups"; +import { RemoveUserGroupParams } from "./types"; +import { find as findUserById } from "./users"; + +export const findAll = async () => UserGroup.findAll(); + +export const findUserGroup = async (userId: string) => + UserGroup.findOne({ + where: { + userId + } + }); + +export const addUsersToGroup = async ( + groupId: UserGroupModel["groupId"], + userIds: Array +) => { + const group = findGroupById(groupId); + const transaction = await sequelize.transaction(); + + try { + return sequelize + .transaction(async (t) => { + for (const userId of userIds) { + const user = await findUserById(userId); + const isUserGroupExisted = await UserGroup.findOne({ + where: { + groupId, + userId + } + }); + + if (!user || !group || isUserGroupExisted) { + return; + } + + await UserGroup.create( + { + userId, + groupId + }, + { transaction: t } + ); + } + }) + .then(() => { + return Promise.resolve(); + }); + } catch (error) { + console.error(error); + await transaction.rollback(); + } +}; + +export const remove = async ( + transaction: Transaction, + params: RemoveUserGroupParams +) => { + const { userId, groupId } = params ?? {}; + const id = userId ?? groupId; + + if (!id) { + return Promise.reject(); + } + + const userGroups = await UserGroup.findAll({ + where: { + ...(userId ? { userId: id } : undefined), + ...(groupId ? { groupId: id } : undefined) + } + }); + + if (!userGroups.length) { + return Promise.reject(); + } + + for (const userGroup of userGroups) { + await userGroup.destroy({ transaction }); + } + + return Promise.resolve(); +}; diff --git a/src/module-3/services/users.ts b/src/module-3/services/users.ts index 6637d8f..b89ff60 100644 --- a/src/module-3/services/users.ts +++ b/src/module-3/services/users.ts @@ -1,5 +1,7 @@ -import { UserModel } from "../types"; +import sequelize from "../data-access"; import User from "../models/user"; +import { UserModel } from "../types"; +import * as UserGroupsService from "./user-groups"; export const getUsersList = async () => User.findAll({ @@ -50,15 +52,32 @@ export const create = async (model: UserModel) => { }; export const remove = async (id: string) => { - const user = await find(id); + const transaction = await sequelize.transaction(); - if (!user) { - return null; - } + try { + const user = await find(id); + if (!user) { + transaction.rollback(); + return null; + } - return user.update({ - isDeleted: true - }); + await UserGroupsService.remove(transaction, { + userId: id + }); + + await user.update( + { + isDeleted: true + }, + { + transaction + } + ); + await transaction.commit(); + } catch (error) { + console.error(error); + transaction.rollback(); + } }; export const update = async (id: string, model: UserModel) => { diff --git a/src/module-3/services/utils.ts b/src/module-3/services/utils.ts new file mode 100644 index 0000000..028c64b --- /dev/null +++ b/src/module-3/services/utils.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const isEqualsObjects = (obj1: any, obj2: any) => { + // Delete useless ids here + /* eslint-disable @typescript-eslint/no-unused-vars */ + console.log(obj1, obj2); + const { id: id1, ...anotherDataObj1 } = obj1; + const { id: id2, ...anotherDataObj2 } = obj2; + /* eslint-enable */ + + return JSON.stringify(anotherDataObj1) === JSON.stringify(anotherDataObj2); +}; diff --git a/src/module-3/types.ts b/src/module-3/types.ts index 8364b99..0a22483 100644 --- a/src/module-3/types.ts +++ b/src/module-3/types.ts @@ -1,10 +1,35 @@ +export interface BaseUser { + id?: string; + isDeleted?: boolean; +} + +export interface BaseGroup { + id?: string; +} export interface UserModel extends BaseUser { login: string; password: string; age: number; } -export interface BaseUser { - id?: string; - isDeleted?: boolean; +export type Permissions = + | "READ" + | "WRITE" + | "DELETE" + | "SHARE" + | "UPLOAD_FILES"; + +export interface GroupModel extends BaseGroup { + name: string; + permissions: Array; +} + +export interface Errors { + type: string; + message: string; +} + +export interface UserGroupModel { + userId: string; + groupId: string; }