Skip to content

Commit

Permalink
feat: 🔥 [EXL-68] support group creation page (#317)
Browse files Browse the repository at this point in the history
  • Loading branch information
tal-rofe committed Sep 18, 2022
2 parents de6c101 + c83eb94 commit 9bdf639
Show file tree
Hide file tree
Showing 77 changed files with 1,743 additions and 140 deletions.
27 changes: 21 additions & 6 deletions apps/backend/src/modules/database/group.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { PrismaService } from './prisma.service';
export class DBGroupService {
constructor(private prisma: PrismaService) {}

public async createGroup(userId: string) {
public async createGroup(userId: string, label: string, description: string | null) {
const createdGroup = await this.prisma.group.create({
data: { userId },
data: { userId, label, description },
select: { id: true },
});

Expand All @@ -29,18 +29,33 @@ export class DBGroupService {
await this.prisma.group.delete({ where: { id: groupId } });
}

public async getUserGroups(userId: string) {
const userGroups = await this.prisma.group.findMany({
public getUserGroups(userId: string) {
return this.prisma.group.findMany({
where: { userId },
select: {
id: true,
label: true,
inlinePolicies: {
select: { id: true, label: true, library: true, configuration: true },
select: { library: true },
},
},
});
}

public async isLabelAvailable(userId: string, label: string) {
const record = await this.prisma.group.findFirst({
where: { userId, label },
});

return record === null;
}

return userGroups;
public getUserGroup(userId: string, groupId: string) {
return this.prisma.group.findFirst({
where: { userId, id: groupId },
select: {
label: true,
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Controller, Get, HttpCode, HttpStatus, Logger, Param } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import {
ApiBearerAuth,
ApiInternalServerErrorResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';

import { CurrentUserId } from '@/decorators/current-user-id.decorator';

import Routes from './groups.routes';
import { AvailableLabelResponse } from './classes/responses';
import { AvailableLabelContract } from './queries/contracts/available-label.contract';

@ApiTags('Groups')
@Controller(Routes.CONTROLLER)
export class AvailableLabelController {
private readonly logger = new Logger(AvailableLabelController.name);

constructor(private readonly queryBus: QueryBus) {}

@ApiOperation({ description: 'Check whether a provided label is availble' })
@ApiBearerAuth('access-token')
@ApiOkResponse({
description: 'Returns whether the provided label is available',
type: AvailableLabelResponse,
})
@ApiUnauthorizedResponse({
description: 'If access token is invalid or missing',
})
@ApiInternalServerErrorResponse({ description: 'If get availability status of the label' })
@Get(Routes.AVAILABLE_LABEL)
@HttpCode(HttpStatus.OK)
public async availableLabel(
@CurrentUserId() userId: string,
@Param('label') label: string,
): Promise<AvailableLabelResponse> {
this.logger.log(`Will try to get availability status of label: "${label}" with an Id: "${userId}"`);

const isAvailable = await this.queryBus.execute<AvailableLabelContract, boolean>(
new AvailableLabelContract(userId, label),
);

this.logger.log(`Successfully got availability status of label: "${label}" with an Id: "${userId}"`);

return {
isAvailable,
};
}
}
21 changes: 21 additions & 0 deletions apps/backend/src/modules/user/modules/groups/classes/create.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, MaxLength, MinLength } from 'class-validator';

import { IsNullable } from '@/decorators/is-nullable.decorator';

export class CreateDto {
@ApiProperty({ type: String, description: 'The label for a group', example: 'Yazif Group' })
@IsString()
@MinLength(1)
@MaxLength(30)
readonly label!: string;

@ApiProperty({
type: String,
description: 'The description for a group',
example: 'Yazif Group is brilliant group',
})
@IsString()
@IsNullable()
readonly description!: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class UserGroupGetAll implements IUserGroupGetAll {
@ApiResponseProperty({
type: [UserGroupInlinePolicyGetAll],
})
public inlinePolicies!: IUserGroupInlinePolicy[];
public librariesNames!: PolicyLibrary[];
}

export class CreateGroupResponse {
Expand All @@ -61,3 +61,19 @@ export class GetAllGroupsResponse {
})
public groups!: IUserGroupGetAll[];
}

export class AvailableLabelResponse {
@ApiResponseProperty({
type: Boolean,
example: true,
})
public isAvailable!: boolean;
}

export class GetResponse {
@ApiResponseProperty({
type: String,
example: 'Yazif Group',
})
public label!: string;
}
17 changes: 12 additions & 5 deletions apps/backend/src/modules/user/modules/groups/create.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, HttpCode, HttpStatus, Logger, Post } from '@nestjs/common';
import { Body, Controller, HttpCode, HttpStatus, Logger, Post } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import { RealIP } from 'nestjs-real-ip';
import {
Expand All @@ -15,6 +15,7 @@ import { CurrentUserId } from '@/decorators/current-user-id.decorator';
import Routes from './groups.routes';
import { CreateGroupResponse } from './classes/responses';
import { CreateGroupContract } from './queries/contracts/create-group.contact';
import { CreateDto } from './classes/create.dto';

@ApiTags('Groups')
@Controller(Routes.CONTROLLER)
Expand All @@ -30,15 +31,21 @@ export class CreateController {
@ApiInternalServerErrorResponse({ description: 'If failed to create the group' })
@Post(Routes.CREATE)
@HttpCode(HttpStatus.CREATED)
public async create(@CurrentUserId() userId: string, @RealIP() ip: string): Promise<CreateGroupResponse> {
this.logger.log(`Will try to create a group for a user with an Id: ${userId}`);
public async create(
@CurrentUserId() userId: string,
@RealIP() ip: string,
@Body() createGroupDto: CreateDto,
): Promise<CreateGroupResponse> {
this.logger.log(
`Will try to create a group with label: "${createGroupDto.label}" for a user with an Id: ${userId}`,
);

const createdGroupId = await this.queryBus.execute<CreateGroupContract, string>(
new CreateGroupContract(userId, ip),
new CreateGroupContract(userId, ip, createGroupDto.label, createGroupDto.description),
);

this.logger.log(
`Successfully created a group with an Id: ${createdGroupId} for a user with an Id: ${userId}`,
`Successfully created a group with label "${createGroupDto.label}" with an Id: ${createdGroupId} for a user with an Id: ${userId}`,
);

return {
Expand Down
52 changes: 52 additions & 0 deletions apps/backend/src/modules/user/modules/groups/get.contoller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Controller, Get, HttpCode, HttpStatus, Logger, NotFoundException, Param } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import {
ApiBearerAuth,
ApiInternalServerErrorResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';

import { CurrentUserId } from '@/decorators/current-user-id.decorator';

import Routes from './groups.routes';
import { GetResponse } from './classes/responses';
import { GetGroupContract } from './queries/contracts/get-group.contract';

@ApiTags('Groups')
@Controller(Routes.CONTROLLER)
export class GetController {
private readonly logger = new Logger(GetController.name);

constructor(private readonly queryBus: QueryBus) {}

@ApiBearerAuth('access-token')
@ApiOperation({ description: 'Get a group of a user' })
@ApiOkResponse({ description: "If successfully got a user's group", type: GetResponse })
@ApiUnauthorizedResponse({
description: 'If access token is either missing or invalid',
})
@ApiInternalServerErrorResponse({ description: "If failed to fetch a user's group" })
@Get(Routes.GET)
@HttpCode(HttpStatus.OK)
public async getAll(
@CurrentUserId() userId: string,
@Param('group_id') groupId: string,
): Promise<GetResponse> {
this.logger.log(`Will try to fetch all groups belong to use with an Id: "${userId}"`);

const userGroup = await this.queryBus.execute<GetGroupContract, GetResponse | null>(
new GetGroupContract(userId, groupId),
);

if (!userGroup) {
throw new NotFoundException();
}

this.logger.log(`Successfully got all groups belong to user with an Id: "${userId}"`);

return userGroup;
}
}
11 changes: 10 additions & 1 deletion apps/backend/src/modules/user/modules/groups/groups.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@ import { EditLabelController } from './edit-label.controller';
import { QueryHandlers } from './queries/handlers';
import { EventHandlers } from './events/handlers';
import { GetAllController } from './get-all.controller';
import { AvailableLabelController } from './available-label.controller';
import { GetController } from './get.contoller';

@Module({
imports: [CqrsModule],
controllers: [CreateController, EditLabelController, DeleteController, GetAllController],
controllers: [
CreateController,
EditLabelController,
DeleteController,
GetAllController,
AvailableLabelController,
GetController,
],
providers: [...QueryHandlers, ...CommandHandlers, ...EventHandlers, BelongingGroupGuard],
})
export class GroupsModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ const Routes = {
CREATE: '',
EDIT_LABEL: ':group_id',
DELETE: ':group_id',
GET_ALL: 'all',
GET_ALL: '',
AVAILABLE_LABEL: 'available/:label',
GET: ':group_id',
};

export default Routes;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Group, InlinePolicy } from '@prisma/client';
import type { Group, InlinePolicy, PolicyLibrary } from '@prisma/client';

export type IUserGroupInlinePolicy = Pick<InlinePolicy, 'id' | 'label' | 'library'> & { rulesCount: number };

export interface IUserGroupGetAll extends Pick<Group, 'id' | 'label'> {
inlinePolicies: IUserGroupInlinePolicy[];
librariesNames: PolicyLibrary[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class AvailableLabelContract {
constructor(public readonly userId: string, public readonly label: string) {}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export class CreateGroupContract {
constructor(public readonly userId: string, public readonly ip: string) {}
constructor(
public readonly userId: string,
public readonly ip: string,
public readonly label: string,
public readonly description: string | null,
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class GetGroupContract {
constructor(public readonly userId: string, public readonly groupId: string) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';

import { DBGroupService } from '@/modules/database/group.service';

import { AvailableLabelContract } from '../contracts/available-label.contract';

@QueryHandler(AvailableLabelContract)
export class AvailableLabelHandler implements IQueryHandler<AvailableLabelContract> {
constructor(private readonly dbGroupService: DBGroupService) {}

execute(contract: AvailableLabelContract) {
return this.dbGroupService.isLabelAvailable(contract.userId, contract.label);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ export class CreateGroupHandler implements IQueryHandler<CreateGroupContract> {
constructor(private readonly dbGroupService: DBGroupService, private readonly eventBus: EventBus) {}

async execute(contract: CreateGroupContract) {
const createdGroupId = await this.dbGroupService.createGroup(contract.userId);
const createdGroupId = await this.dbGroupService.createGroup(
contract.userId,
contract.label,
contract.description,
);

this.eventBus.publish(new CreateGroupMixpanelContract(contract.userId, contract.ip));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import type { Prisma } from '@prisma/client';

import { DBGroupService } from '@/modules/database/group.service';

Expand All @@ -14,12 +13,7 @@ export class GetAllGroupsHandler implements IQueryHandler<GetAllGroupsContract>

return userGroups.map((userGroup) => ({
...userGroup,
inlinePolicies: userGroup.inlinePolicies.map((inlinePolicy) => ({
...inlinePolicy,
rulesCount: Object.keys((inlinePolicy.configuration as Prisma.JsonObject)?.['rules'] ?? {})
.length,
configuration: undefined,
})),
librariesNames: userGroup.inlinePolicies.map((inlinePolicy) => inlinePolicy.library),
}));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';

import { DBGroupService } from '@/modules/database/group.service';

import { GetGroupContract } from '../contracts/get-group.contract';

@QueryHandler(GetGroupContract)
export class GetGroupHandler implements IQueryHandler<GetGroupContract> {
constructor(private readonly dbGroupService: DBGroupService) {}

execute(contract: GetGroupContract) {
return this.dbGroupService.getUserGroup(contract.userId, contract.groupId);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { AvailableLabelHandler } from './available-label.handler';
import { CreateGroupHandler } from './create-group.handler';
import { GetAllGroupsHandler } from './get-all-groups.handler';
import { GetGroupHandler } from './get-group.handler';

export const QueryHandlers = [CreateGroupHandler, GetAllGroupsHandler];
export const QueryHandlers = [
CreateGroupHandler,
GetAllGroupsHandler,
AvailableLabelHandler,
GetGroupHandler,
];
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface IProps {}
const <%= h.changeCase.pascalCase(name) %>View: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
const { t } = useTranslation();

return <React.Fragment></React.Fragment>;
return <></>;
};

<%= h.changeCase.pascalCase(name) %>View.displayName = '<%= h.changeCase.pascalCase(name) %>View';
Expand Down
Loading

0 comments on commit 9bdf639

Please sign in to comment.