Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 🔥 [EXL-68] support group creation page #317

Merged
merged 3 commits into from
Sep 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
};
}
}
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;
}
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