Skip to content

Commit

Permalink
feat: board users and roles (#121)
Browse files Browse the repository at this point in the history
  • Loading branch information
nunocaseiro authored Apr 7, 2022
1 parent 4315497 commit 5b930b2
Show file tree
Hide file tree
Showing 17 changed files with 824 additions and 1,062 deletions.
21 changes: 11 additions & 10 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@
"pre-commit": "lint-staged"
},
"dependencies": {
"@nestjs/common": "^8.4.3",
"@faker-js/faker": "^6.1.2",
"@nestjs/common": "^8.4.4",
"@nestjs/config": "^2.0.0",
"@nestjs/core": "^8.4.3",
"@nestjs/core": "^8.4.4",
"@nestjs/jwt": "^8.0.0",
"@nestjs/mongoose": "^9.0.3",
"@nestjs/passport": "^8.2.1",
"@nestjs/platform-express": "^8.4.3",
"@nestjs/platform-socket.io": "^8.4.3",
"@nestjs/platform-express": "^8.4.4",
"@nestjs/platform-socket.io": "^8.4.4",
"@nestjs/schedule": "^1.1.0",
"@nestjs/websockets": "^8.4.3",
"@nestjs/websockets": "^8.4.4",
"@types/bcrypt": "^5.0.0",
"@types/passport-jwt": "^3.0.6",
"@types/passport-local": "^1.0.34",
Expand All @@ -50,19 +51,19 @@
"lint-staged": "^12.3.7",
"mongodb": "^4.5.0",
"mongoose": "^6.2.10",
"mongoose-lean-virtuals": "^0.9.0",
"passport": "^0.5.2",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.5.5",
"socket.io": "^4.4.1",
"@faker-js/faker": "^6.1.2"
"socket.io": "^4.4.1"
},
"devDependencies": {
"@nestjs/cli": "^8.2.4",
"@nestjs/schematics": "^8.0.9",
"@nestjs/testing": "^8.4.3",
"@nestjs/cli": "^8.2.5",
"@nestjs/schematics": "^8.0.10",
"@nestjs/testing": "^8.4.4",
"@types/express": "^4.17.13",
"@types/jest": "^27.4.1",
"@types/mongodb": "^4.0.7",
Expand Down
7 changes: 7 additions & 0 deletions backend/src/infrastructure/database/mongoose.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { MongooseModule } from '@nestjs/mongoose';
import User, { UserSchema } from '../../modules/users/schemas/user.schema';
import Board, { BoardSchema } from '../../modules/boards/schemas/board.schema';
import BoardUser, {
BoardUserSchema,
} from '../../modules/boards/schemas/board.user.schema';

export const mongooseBoardModule = MongooseModule.forFeature([
{ name: Board.name, schema: BoardSchema },
]);

export const mongooseBoardUserModule = MongooseModule.forFeature([
{ name: BoardUser.name, schema: BoardUserSchema },
]);

export const mongooseUserModule = MongooseModule.forFeature([
{ name: User.name, schema: UserSchema },
]);
5 changes: 5 additions & 0 deletions backend/src/libs/enum/board.roles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum BoardRoles {
OWNER = 'owner',
MEMBER = 'member',
RESPONSIBLE = 'responsible',
}
21 changes: 21 additions & 0 deletions backend/src/libs/validators/check-unique-users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import BoardUserDto from '../../modules/boards/dto/board.user.dto';

@ValidatorConstraint({ name: 'checkUniqueUsers', async: false })
export class CheckUniqueUsers implements ValidatorConstraintInterface {
validate(users: BoardUserDto[]) {
const usersIds = users.map((user) => user.user);
if (usersIds.length === new Set(usersIds).size) {
return true;
}

return false;
}

defaultMessage() {
return 'Duplicate users are not allowed';
}
}
7 changes: 5 additions & 2 deletions backend/src/modules/boards/boards.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ import {
deleteBoardApplication,
getBoardApplication,
} from './boards.providers';
import { mongooseBoardModule } from '../../infrastructure/database/mongoose.module';
import {
mongooseBoardModule,
mongooseBoardUserModule,
} from '../../infrastructure/database/mongoose.module';

@Module({
imports: [UsersModule, mongooseBoardModule],
imports: [UsersModule, mongooseBoardModule, mongooseBoardUserModule],
providers: [
createBoardService,
updateBoardService,
Expand Down
23 changes: 22 additions & 1 deletion backend/src/modules/boards/dto/board.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import {
IsOptional,
IsBoolean,
IsNumber,
ValidateIf,
IsMongoId,
Validate,
} from 'class-validator';
import { Transform, TransformFnParams, Type } from 'class-transformer';
import ColumnDto from './column/column.dto';
import BoardUserDto from './board.user.dto';
import { CheckUniqueUsers } from '../../../libs/validators/check-unique-users';

export default class BoardDto {
@IsOptional()
Expand All @@ -33,11 +37,28 @@ export default class BoardDto {
@IsBoolean()
isPublic!: boolean;

@IsOptional()
@ValidateIf((o) => o.isPublic === false)
@IsNotEmpty()
@Transform(({ value }: TransformFnParams) => value.trim())
password?: string;

@IsNotEmpty()
@IsNumber()
maxVotes!: number;

@IsOptional()
@ValidateNested({ each: true })
dividedBoards?: BoardDto[];

// @IsOptional()
// @IsMongoId()
// @IsString()
// team?: string;

@IsOptional()
socketId?: string;

@IsOptional()
@Validate(CheckUniqueUsers)
users!: BoardUserDto[];
}
24 changes: 24 additions & 0 deletions backend/src/modules/boards/dto/board.user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
IsString,
IsNotEmpty,
IsOptional,
IsMongoId,
IsEnum,
} from 'class-validator';
import { BoardRoles } from '../../../libs/enum/board.roles';

export default class BoardUserDto {
@IsOptional()
@IsMongoId()
_id?: string;

@IsString()
@IsNotEmpty()
@IsEnum(BoardRoles, { each: true })
role!: string;

@IsMongoId()
@IsString()
@IsNotEmpty()
user!: string;
}
48 changes: 40 additions & 8 deletions backend/src/modules/boards/schemas/board.schema.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import * as mongoose from 'mongoose';
import * as leanVirtualsPlugin from 'mongoose-lean-virtuals';
import { ObjectId, SchemaTypes, Document } from 'mongoose';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import User from '../../users/schemas/user.schema';
import { ColumnDocument, ColumnSchema } from './column.schema';
import User from '../../users/schemas/user.schema';

export type BoardDocument = Board & mongoose.Document;
export type BoardDocument = Board & Document;

@Schema()
@Schema({
timestamps: true,
toJSON: {
virtuals: true,
},
})
export default class Board {
@Prop({ nullable: false })
title!: string;
Expand All @@ -16,17 +22,43 @@ export default class Board {
@Prop({ nullable: true })
password?: string;

@Prop({ type: Date, default: Date.now })
creationDate!: Date;
@Prop({ type: SchemaTypes.ObjectId, ref: 'User' })
submitedByUser!: ObjectId;

@Prop({ nullable: false })
maxVotes!: number;

@Prop({ nullable: false, type: [ColumnSchema] })
columns!: ColumnDocument[];

@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User', nullable: false })
createdBy!: User | mongoose.Schema.Types.ObjectId;
@Prop({ type: [{ type: SchemaTypes.ObjectId, ref: 'Board' }] })
dividedBoards!: Board[] | ObjectId[];

// @Prop({
// type: SchemaTypes.ObjectId,
// ref: 'Team',
// nullable: true,
// default: null,
// })
// team!: Team | ObjectId;

@Prop({ type: SchemaTypes.ObjectId, ref: 'User' })
createdBy!: User | ObjectId;

@Prop({ default: false })
recurrent!: boolean;

@Prop({ default: false })
isSubBoard!: boolean;
}

export const BoardSchema = SchemaFactory.createForClass(Board);

BoardSchema.plugin(leanVirtualsPlugin);

BoardSchema.virtual('users', {
ref: 'BoardUser',
localField: '_id',
foreignField: 'board',
justOne: false,
});
31 changes: 31 additions & 0 deletions backend/src/modules/boards/schemas/board.user.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ObjectId, SchemaTypes, Document } from 'mongoose';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { BoardRoles } from '../../../libs/enum/board.roles';
import User from '../../users/schemas/user.schema';

export type BoardUserDocument = BoardUser & Document;

@Schema({
toJSON: {
virtuals: true,
},
})
export default class BoardUser {
@Prop({
nullable: false,
type: String,
enum: [BoardRoles.RESPONSIBLE, BoardRoles.MEMBER, BoardRoles.OWNER],
})
role!: string;

@Prop({ type: SchemaTypes.ObjectId, ref: 'User', nullable: false })
user!: User | ObjectId;

@Prop({ type: SchemaTypes.ObjectId, ref: 'Board', nullable: false })
board!: ObjectId;

@Prop({ nullable: false })
votesCount!: number;
}

export const BoardUserSchema = SchemaFactory.createForClass(BoardUser);
67 changes: 63 additions & 4 deletions backend/src/modules/boards/services/create.board.service.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,83 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { BoardRoles } from '../../../libs/enum/board.roles';
import { encrypt } from '../../../libs/utils/bcrypt';
import isEmpty from '../../../libs/utils/isEmpty';
import BoardDto from '../dto/board.dto';
import BoardUserDto from '../dto/board.user.dto';
import { CreateBoardService } from '../interfaces/services/create.board.service.interface';
import Board, { BoardDocument } from '../schemas/board.schema';
import BoardUser, { BoardUserDocument } from '../schemas/board.user.schema';

@Injectable()
export default class CreateBoardServiceImpl implements CreateBoardService {
constructor(
@InjectModel(Board.name) private boardModel: Model<BoardDocument>,
@InjectModel(BoardUser.name)
private boardUserModel: Model<BoardUserDocument>,
) {}

async create(boardData: BoardDto, userId: string) {
if (boardData.password) {
boardData.password = await encrypt(boardData.password);
}
saveBoardUsers(newUsers: BoardUserDto[], newBoardId: string) {
Promise.all(
newUsers.map((user) =>
this.boardUserModel.create({ ...user, board: newBoardId }),
),
);
}

async createDividedBoards(boards: BoardDto[], userId: string) {
const newBoardsIds = await Promise.allSettled(
boards.map(async (board) => {
const { users } = board;
const { _id } = await this.create(board, userId);
if (!isEmpty(users)) {
this.saveBoardUsers(users, _id);
}
return _id;
}),
);
return newBoardsIds.flatMap((result) =>
result.status === 'fulfilled' ? [result.value] : [],
);
}

async createBoard(boardData: BoardDto, userId: string) {
const { dividedBoards } = boardData;
return this.boardModel.create({
...boardData,
createdBy: userId,
dividedBoards: await this.createDividedBoards(
dividedBoards ?? [],
userId,
),
});
}

addOwner(users: BoardUserDto[], userId: string) {
return [
...users,
{
user: userId.toString(),
role: BoardRoles.OWNER,
},
];
}

async create(boardData: BoardDto, userId: string) {
const { password, users } = boardData;
if (password) {
boardData.password = await encrypt(password);
}

const newBoard = await this.createBoard(boardData, userId);

const newUsers = this.addOwner(users, userId);

if (!isEmpty(newUsers)) {
this.saveBoardUsers(newUsers, newBoard._id);
}

return newBoard;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ export class GetUserApplicationImpl implements GetUserApplication {
getByEmail(email: string) {
return this.getUserService.getByEmail(email);
}

countUsers() {
return this.getUserService.countUsers();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { UserDocument } from '../../schemas/user.schema';

export interface GetUserApplication {
getByEmail(email: string): Promise<LeanDocument<UserDocument> | null>;
countUsers(): Promise<number>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface GetUserService {
refreshToken: string,
userId: string,
): Promise<LeanDocument<UserDocument> | false>;
countUsers(): Promise<number>;
}
Loading

0 comments on commit 5b930b2

Please sign in to comment.