Skip to content

Commit

Permalink
feat: vote service & mutations
Browse files Browse the repository at this point in the history
  • Loading branch information
chikof committed Apr 13, 2024
1 parent 57e67a9 commit ced2c41
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 6 deletions.
3 changes: 3 additions & 0 deletions src/database/tables/vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ export const votes = pgTable('votes', {
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
expires: bigint('expires', { mode: 'number' }).notNull()
});

export type TvotesInsert = typeof votes.$inferInsert;
export type TvotesSelect = typeof votes.$inferSelect;
5 changes: 4 additions & 1 deletion src/lib/constants/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ export enum ErrorMessages {

// Vanity
VANITY_NOT_FOUND = 'Vanity not found',
VANITY_ALREADY_EXISTS = 'Vanity already exists'
VANITY_ALREADY_EXISTS = 'Vanity already exists',

// Votes
VOTE_USER_ALREADY_VOTED = 'You have already voted for this bot'

// TODO: (Chiko/Simxnet) Implement custom errors for Mutations
}
8 changes: 6 additions & 2 deletions src/modules/bot/bot.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { BotFields } from './resolvers/fields/bot.fields';
import { BotOwnerFields } from './resolvers/fields/owner.fields';
import { BotTagFields } from './resolvers/fields/tag.fields';
import { BotOwnerResolver } from './resolvers/owner.resolver';
import { TagResolver } from './resolvers/tag.resolver';
import { BotTagResolver } from './resolvers/tag.resolver';
import { BotVoteResolver } from './resolvers/vote.resolver';
import { BotService } from './services/bot.service';
import { BotOwnerService } from './services/owner.service';
import { BotTagService } from './services/tag.service';
import { BotVoteService } from './services/vote.service';
import { BotWebhookService } from './services/webhook.service';

@Module({
Expand All @@ -24,7 +26,9 @@ import { BotWebhookService } from './services/webhook.service';
BotTagFields,
BotWebhookService,
PaginatorService,
TagResolver
BotTagResolver,
BotVoteResolver,
BotVoteService
],
imports: [HttpModule],
exports: [
Expand Down
20 changes: 20 additions & 0 deletions src/modules/bot/inputs/vote/create.input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { TvotesInsert } from '@database/schema';
import type { OmitType } from '@lib/types/utils';
import { Field, InputType } from '@nestjs/graphql';
import { IsSnowflake } from '@utils/graphql/validators/isSnowflake';

@InputType({
description: 'The input type for creating a vote'
})
export class BotVoteCreateInput
implements OmitType<TvotesInsert, 'expires' | 'userId'>
{
/**
* The bot ID of the vote.
*/
@Field(() => String, {
description: 'The bot ID of the vote'
})
@IsSnowflake()
public botId!: string;
}
51 changes: 51 additions & 0 deletions src/modules/bot/objects/vote/vote.object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { TvotesSelect } from '@database/schema';
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Paginated } from '@utils/graphql/pagination';

/**
* Represents a vote object for a bot.
*/
@ObjectType({
description: 'A vote object for a bot'
})
export class BotVoteObject implements TvotesSelect {
/**
* The ID of the vote.
*/
@Field(() => ID, {
description: 'The ID of the vote'
})
public id!: number;

/**
* The user ID of the vote.
*/
@Field(() => String, {
description: 'The user ID of the vote'
})
public userId!: string;

/**
* The bot ID of the vote.
*/
@Field(() => String, {
description: 'The bot ID of the vote'
})
public botId!: string;

/**
* The expiration date of the vote.
*/
@Field(() => Number, {
description: 'The expiration date of the vote'
})
public expires!: number;
}

/**
* Represents a paginated list of vote objects.
*/
@ObjectType({
description: 'A paginated list of vote objects'
})
export class BotVoteObjectConnection extends Paginated(BotVoteObject) {}
25 changes: 23 additions & 2 deletions src/modules/bot/resolvers/fields/bot.fields.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { BotObject } from '@modules/bot/objects/bot/bot.object';
import { BotOwnerObject } from '@modules/bot/objects/owner/owner.object';
import { BotTagObject } from '@modules/bot/objects/tag/tag.object';
import { BotVoteObjectConnection } from '@modules/bot/objects/vote/vote.object';
import { BotOwnerService } from '@modules/bot/services/owner.service';
import { BotTagService } from '@modules/bot/services/tag.service';
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { BotVoteService } from '@modules/bot/services/vote.service';
import { Args, Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PaginationInput } from '@utils/graphql/pagination';

/**
* Represents the fields resolver for the Bot object.
Expand All @@ -14,10 +17,12 @@ export class BotFields {
* Constructor for BotFields class.
* @param _botOwnerService The BotOwnerService instance.
* @param _botTagService The BotTagService instance.
* @param _botVoteService The BotVoteService instance.
*/
public constructor(
private _botOwnerService: BotOwnerService,
private _botTagService: BotTagService
private _botTagService: BotTagService,
private _botVoteService: BotVoteService
) {}

/**
Expand All @@ -43,4 +48,20 @@ export class BotFields {
public tags(@Parent() bot: BotObject) {
return this._botTagService.getBotTags(bot.id);
}

/**
* Retrieves the total number of votes for a bot.
* @param bot - The bot object.
* @param pagination - The pagination options.
* @returns The total number of votes for the bot.
*/
@ResolveField(() => BotVoteObjectConnection, {
description: 'The votes for the bot.'
})
public votes(
@Parent() bot: BotObject,
@Args('pagination') pagination: PaginationInput
) {
return this._botVoteService.paginateVotes(bot.id, pagination);
}
}
2 changes: 1 addition & 1 deletion src/modules/bot/resolvers/tag.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { BotTagService } from '../services/tag.service';
*/
@Resolver(() => BotTagObject)
@UsePipes(ValidationTypes, ValidationPipe)
export class TagResolver {
export class BotTagResolver {
/**
* Constructs a new instance of the TagResolver class.
* @param _tagService The BotTagService instance to be injected.
Expand Down
38 changes: 38 additions & 0 deletions src/modules/bot/resolvers/vote.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { User } from '@modules/auth/decorators/user.decorator';
import { JwtAuthGuard } from '@modules/auth/guards/jwt.guard';
import type { JwtPayload } from '@modules/auth/interfaces/payload.interface';
import { UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { ValidationTypes } from 'class-validator';
import { BotVoteCreateInput } from '../inputs/vote/create.input';
import { BotVoteObject } from '../objects/vote/vote.object';
import { BotVoteService } from '../services/vote.service';

/**
* The resolver that contains mutations for bot votes.
*/
@Resolver(() => BotVoteObject)
@UsePipes(ValidationTypes, ValidationPipe)
export class BotVoteResolver {
/**
* Creates an instance of `BotVoteResolver`.
* @param voteService - The vote service.
*/
public constructor(private readonly voteService: BotVoteService) {}

/**
* Creates a vote for a bot.
*
* @param input - The input data for creating the vote.
* @param user - The authenticated user making the vote.
* @returns A Promise that resolves to the created vote.
*/
@Mutation(() => BotVoteObject)
@UseGuards(JwtAuthGuard)
public async createVote(
@Args('input') input: BotVoteCreateInput,
@User() user: JwtPayload
) {
return this.voteService.createVote(input.botId, user.id);
}
}
117 changes: 117 additions & 0 deletions src/modules/bot/services/vote.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { PaginatorService } from '@/services/paginator.service';
import { ErrorMessages } from '@constants/errors';
import { DATABASE } from '@constants/tokens';
import { votes } from '@database/schema';
import type { DrizzleService } from '@lib/types';
import {
ForbiddenException,
Inject,
Injectable,
type OnModuleInit
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { hours } from '@nestjs/throttler';
import type { PaginationInput } from '@utils/graphql/pagination';
import { and, eq, gt } from 'drizzle-orm';
import { BotService } from './bot.service';

/**
* Service class for managing votes for bots.
*/
@Injectable()
export class BotVoteService implements OnModuleInit {
/**
* The injected BotService instance.
*/
private _botService!: BotService;

/**
* Constructs a new instance of the VoteService class.
* @param _drizzleService - The injected DrizzleService instance.
* @param _paginatorService - The injected PaginatorService instance.
*/
public constructor(
@Inject(DATABASE) private _drizzleService: DrizzleService,
private _paginatorService: PaginatorService,
private _moduleRef: ModuleRef
) {}

/**
* Lifecycle hook that runs after the module has been initialized.
*/
public onModuleInit() {
this._botService = this._moduleRef.get(BotService, { strict: false });
}

/**
* Creates a new vote for a bot by a user.
* @param botId - The ID of the bot.
* @param userId - The ID of the user.
* @returns The created vote.
* @throws ForbiddenException if the user has already voted for the bot.
*/
public async createVote(botId: string, userId: string) {
// Check if the bot exists.
await this._botService.getBot(botId);

// Check if the user has already voted.
if (await this.canVote(botId, userId)) {
throw new ForbiddenException(ErrorMessages.VOTE_USER_ALREADY_VOTED);
}

// Create the vote.
const [vote] = await this._drizzleService
.insert(votes)
.values({
botId,
userId,
expires: Date.now() + hours(12)
})
.returning();

return vote;
}

/**
* Retrieves the votes for a bot.
* @param botId - The ID of the bot.
* @param pagination - The pagination options.
* @returns The paginated list of votes.
*/
public async paginateVotes(
botId: string,
pagination: PaginationInput = {}
) {
return this._paginatorService.paginate<
typeof votes._.config,
typeof votes
>({
schema: votes,
where: eq(votes.botId, botId),
pagination
});
}

/**
* Checks if a user can vote for a bot.
* @param botId - The ID of the bot.
* @param userId - The ID of the user.
* @returns A Promise that resolves to a boolean indicating if the user can vote.
*/
public async canVote(botId: string, userId: string): Promise<boolean> {
const userVotes = await this._drizzleService
.select()
.from(votes)
.where(
and(
eq(votes.botId, botId),
eq(votes.userId, userId),
gt(votes.expires, Date.now())
)
)
.limit(1)
.execute();

return !!userVotes.length;
}
}

0 comments on commit ced2c41

Please sign in to comment.