Compare commits
3
Commits
main
...
v0.0.1-fix01
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4906b1ddde | ||
|
|
495ac35937 | ||
|
|
7383ce6977 |
@@ -0,0 +1,49 @@
|
||||
name: release-tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
release-image:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
REPO_NAME: ${{ gitea.repository }}
|
||||
ENDPOINT: git.legonzaur.fr
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# - name: Login to DockerHub
|
||||
# uses: docker/login-action@v2
|
||||
# with:
|
||||
# registry: ${{ env.ENDPOINT }}
|
||||
# username: ${{ secrets.DOCKER_USERNAME }}
|
||||
# password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.ENDPOINT }}/${{ env.REPO_NAME }}
|
||||
tags: |
|
||||
type=schedule
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Containerfile
|
||||
build-args: |
|
||||
NUXT_PUBLIC_API_BASE=${{ vars.NUXT_PUBLIC_API_BASE }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/src/main.ts",
|
||||
"preLaunchTask": "npm: build",
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
FROM node:lts-alpine AS build-stage
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npx nest build
|
||||
|
||||
# étape de production
|
||||
FROM node:lts-alpine AS production-stage
|
||||
WORKDIR /app
|
||||
COPY --from=build-stage /app/dist/ /app
|
||||
COPY --from=build-stage /app/node_modules/ /app/node_modules
|
||||
EXPOSE 3000
|
||||
|
||||
ENV API_PREFIX=/api
|
||||
|
||||
ENV PG_HOST=127.0.0.1
|
||||
ENV PG_USER=polls
|
||||
ENV PG_PASSWORD=polls
|
||||
ENV PG_DATABASE=polls
|
||||
|
||||
CMD ["node", "main"]
|
||||
+4
-18
@@ -3,14 +3,8 @@ import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { PollsModule } from './polls/polls.module';
|
||||
import { Answer } from './polls/entities/answer.entity';
|
||||
import { Choice } from './polls/entities/choice.entity';
|
||||
import { InviteCode } from './polls/entities/inviteCode.entity';
|
||||
import { Poll } from './polls/entities/poll.entity';
|
||||
import { Question } from './polls/entities/question.entity';
|
||||
import { QuestionOption } from './polls/entities/questionOptions.entity';
|
||||
import { User } from './polls/entities/user.entity';
|
||||
import { Answer, InviteCode, Vote } from './vote/vote.entity';
|
||||
import { VoteModule } from './vote/vote.module';
|
||||
|
||||
void ConfigModule.forRoot({
|
||||
envFilePath: '.env',
|
||||
@@ -28,17 +22,9 @@ const configService = new ConfigService();
|
||||
username: configService.get('PG_USER'),
|
||||
database: configService.get('PG_DATABASE'),
|
||||
synchronize: configService.get('DEV_MODE') == 'true',
|
||||
entities: [
|
||||
Answer,
|
||||
Choice,
|
||||
InviteCode,
|
||||
Poll,
|
||||
Question,
|
||||
QuestionOption,
|
||||
User,
|
||||
],
|
||||
entities: [Vote, Answer, InviteCode],
|
||||
}),
|
||||
PollsModule,
|
||||
VoteModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
@@ -7,6 +7,9 @@ async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
app.enableCors({ origin: '*' });
|
||||
|
||||
app.setGlobalPrefix(process.env.API_PREFIX ?? '');
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Vote au jugement majoritaire')
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Expose } from 'class-transformer';
|
||||
import { IsNumber, IsString } from 'class-validator';
|
||||
|
||||
export class AnswerCreateDto {
|
||||
@Expose()
|
||||
@IsNumber()
|
||||
@ApiProperty()
|
||||
questionId: number;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
choice: string;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Expose } from 'class-transformer';
|
||||
|
||||
export class ChoiceReadDto {
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
value: string;
|
||||
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
order: string;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Expose } from 'class-transformer';
|
||||
import { QuestionUpdateDto } from './question.dto';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsUUID } from 'class-validator';
|
||||
|
||||
export class PollUpdateDTO {
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
description: string;
|
||||
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
questions: QuestionUpdateDto[];
|
||||
|
||||
@Expose()
|
||||
@IsUUID()
|
||||
@ApiProperty()
|
||||
publicCode: string;
|
||||
}
|
||||
|
||||
export class PollReadDTO extends PollUpdateDTO {
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
published: boolean;
|
||||
}
|
||||
|
||||
export class PollReadAdminDTO extends PollReadDTO {
|
||||
@Expose()
|
||||
@IsUUID()
|
||||
@ApiProperty()
|
||||
editCode: string;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Expose } from 'class-transformer';
|
||||
import { ChoiceReadDto } from './choice.dto';
|
||||
|
||||
export class QuestionUpdateDto {
|
||||
@Expose()
|
||||
id: number;
|
||||
|
||||
@Expose()
|
||||
value: string;
|
||||
|
||||
@Expose()
|
||||
mandatory: boolean;
|
||||
|
||||
@Expose()
|
||||
order: number;
|
||||
|
||||
@Expose()
|
||||
choices: ChoiceReadDto[];
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
ManyToOne,
|
||||
Unique,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Question } from './question.entity';
|
||||
import { User } from './user.entity';
|
||||
import { QuestionOption } from './questionOptions.entity';
|
||||
import { Choice } from './choice.entity';
|
||||
|
||||
@Entity()
|
||||
@Unique(['questionOption', 'user'])
|
||||
export class Answer {
|
||||
@PrimaryGeneratedColumn({})
|
||||
id: number;
|
||||
|
||||
@ManyToOne(() => Question, (question: Question) => question.answers)
|
||||
@JoinColumn([{ name: 'questionId', referencedColumnName: 'id' }])
|
||||
question: Question;
|
||||
|
||||
@ManyToOne(() => Choice)
|
||||
@JoinColumn([{ name: 'choiceId', referencedColumnName: 'id' }])
|
||||
choice: Choice;
|
||||
|
||||
@ManyToOne(() => QuestionOption)
|
||||
@JoinColumn([
|
||||
{ name: 'questionId', referencedColumnName: 'question' },
|
||||
{ name: 'choiceId', referencedColumnName: 'choice' },
|
||||
])
|
||||
questionOption: QuestionOption;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
user: User;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
@Unique(['value'])
|
||||
export class Choice {
|
||||
@PrimaryGeneratedColumn({})
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
value: string;
|
||||
|
||||
@Column()
|
||||
order: string;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
Generated,
|
||||
ManyToOne,
|
||||
} from 'typeorm';
|
||||
import { Poll } from './poll.entity';
|
||||
|
||||
@Entity()
|
||||
@Unique(['value'])
|
||||
export class InviteCode {
|
||||
@PrimaryGeneratedColumn({})
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
description: string;
|
||||
|
||||
@Column({ nullable: true, default: null })
|
||||
maxUsages: number;
|
||||
|
||||
@Column()
|
||||
@Generated('uuid')
|
||||
value: string;
|
||||
|
||||
@ManyToOne(() => Poll, (poll) => poll.codes)
|
||||
poll: Poll;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
OneToMany,
|
||||
Generated,
|
||||
Unique,
|
||||
} from 'typeorm';
|
||||
import { Question } from './question.entity';
|
||||
import { InviteCode } from './inviteCode.entity';
|
||||
|
||||
@Entity()
|
||||
@Unique(['editCode'])
|
||||
export class Poll {
|
||||
@PrimaryGeneratedColumn({})
|
||||
id: number;
|
||||
|
||||
@Column({ default: 'Sondage' })
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column()
|
||||
@Generated('uuid')
|
||||
publicCode: string;
|
||||
|
||||
@Column()
|
||||
@Generated('uuid')
|
||||
editCode: string;
|
||||
|
||||
@Column({ default: false })
|
||||
published: boolean;
|
||||
|
||||
@OneToMany(() => InviteCode, (code) => code.poll, { cascade: true })
|
||||
codes: InviteCode;
|
||||
|
||||
@OneToMany(() => Question, (question) => question.poll, { cascade: true })
|
||||
questions: Question[];
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
Unique,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
} from 'typeorm';
|
||||
import { Poll } from './poll.entity';
|
||||
import { Answer } from './answer.entity';
|
||||
import { Choice } from './choice.entity';
|
||||
|
||||
@Entity()
|
||||
@Unique(['poll', 'order'])
|
||||
export class Question {
|
||||
@PrimaryGeneratedColumn({})
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
value: string;
|
||||
|
||||
@Column()
|
||||
mandatory: boolean;
|
||||
|
||||
@Column()
|
||||
order: number;
|
||||
|
||||
@ManyToOne(() => Poll, (poll: Poll) => poll.questions)
|
||||
poll: Poll;
|
||||
|
||||
// @OneToMany(() => QuestionOption, (questionOption) => questionOption.question)
|
||||
// options: QuestionOption[];
|
||||
|
||||
@ManyToMany(() => Choice, { eager: true })
|
||||
@JoinTable({
|
||||
name: 'question_options',
|
||||
joinColumn: {
|
||||
name: 'question',
|
||||
referencedColumnName: 'id',
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: 'choice',
|
||||
referencedColumnName: 'id',
|
||||
},
|
||||
synchronize: false,
|
||||
})
|
||||
choices: Choice[];
|
||||
|
||||
@OneToMany(() => Answer, (answer) => answer.question)
|
||||
answers: Answer[];
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Entity, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
import { Choice } from './choice.entity';
|
||||
import { Question } from './question.entity';
|
||||
|
||||
@Entity()
|
||||
@Unique(['question', 'choice'])
|
||||
export class QuestionOption {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ManyToOne(() => Choice)
|
||||
choice: Choice;
|
||||
|
||||
@ManyToOne(() => Question)
|
||||
question: Question;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn({})
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
username: string;
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { PollsService } from './polls.service';
|
||||
import { PollReadAdminDTO, PollReadDTO, PollUpdateDTO } from './dto/poll.dto';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
|
||||
@ApiTags('polls')
|
||||
@Controller('polls')
|
||||
export class PollController {
|
||||
constructor(private readonly pollService: PollsService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create poll' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Poll successfully created.',
|
||||
type: PollReadAdminDTO,
|
||||
})
|
||||
async create(): Promise<PollReadAdminDTO> {
|
||||
const poll = await this.pollService.create();
|
||||
return plainToInstance(PollReadAdminDTO, poll);
|
||||
}
|
||||
|
||||
@Post(':publicCode/:editCode')
|
||||
@ApiOperation({ summary: 'Update a poll' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Poll successfully created.',
|
||||
type: PollReadAdminDTO,
|
||||
})
|
||||
async update(
|
||||
@Param('publicCode', new ParseUUIDPipe()) publicCode: string,
|
||||
@Param('editCode', new ParseUUIDPipe()) editCode: string,
|
||||
@Body() pollUpdate: PollUpdateDTO,
|
||||
): Promise<PollReadAdminDTO> {
|
||||
const poll = await this.pollService.readAdmin(publicCode, editCode);
|
||||
if (!poll) {
|
||||
throw new HttpException(
|
||||
'Poll not found or invalid editCode',
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
await this.pollService.update(poll, pollUpdate);
|
||||
return plainToInstance(PollReadAdminDTO, poll);
|
||||
}
|
||||
|
||||
@Get(':publicCode')
|
||||
@ApiOperation({ summary: 'Get public information about a poll' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Found',
|
||||
type: PollReadDTO,
|
||||
})
|
||||
async get(
|
||||
@Param('publicCode', new ParseUUIDPipe()) publicCode: string,
|
||||
): Promise<PollReadDTO> {
|
||||
const poll = await this.pollService.read(publicCode);
|
||||
if (!poll) {
|
||||
throw new HttpException('Poll not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (!poll?.published) {
|
||||
throw new HttpException('Poll not yet published', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
return plainToInstance(PollReadDTO, poll);
|
||||
}
|
||||
|
||||
@Get(':publicCode/:editCode')
|
||||
@ApiOperation({ summary: 'Get admin information about a poll' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Found',
|
||||
type: PollReadAdminDTO,
|
||||
})
|
||||
async getAdmin(
|
||||
@Param('publicCode', new ParseUUIDPipe()) publicCode: string,
|
||||
@Param('editCode', new ParseUUIDPipe()) editCode: string,
|
||||
): Promise<PollReadAdminDTO> {
|
||||
const poll = await this.pollService.readAdmin(publicCode, editCode);
|
||||
if (!poll) {
|
||||
throw new HttpException(
|
||||
'Poll not found or invalid editCode',
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
return plainToInstance(PollReadAdminDTO, poll);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Poll } from './entities/poll.entity';
|
||||
import { PollsService } from './polls.service';
|
||||
import { PollController } from './polls.controller';
|
||||
import { InviteCode } from './entities/inviteCode.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Poll, InviteCode])],
|
||||
controllers: [PollController],
|
||||
providers: [PollsService],
|
||||
})
|
||||
export class PollsModule {}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Poll } from './entities/poll.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PollUpdateDTO } from './dto/poll.dto';
|
||||
import { InviteCode } from './entities/inviteCode.entity';
|
||||
|
||||
@Injectable()
|
||||
export class PollsService {
|
||||
constructor(
|
||||
@InjectRepository(Poll) private pollRepository: Repository<Poll>,
|
||||
@InjectRepository(InviteCode)
|
||||
private inviteRepository: Repository<InviteCode>,
|
||||
) {}
|
||||
|
||||
async create() {
|
||||
const poll = this.pollRepository.create();
|
||||
await this.pollRepository.save(poll);
|
||||
return poll;
|
||||
}
|
||||
|
||||
async update(poll: Poll, pollUpdate: PollUpdateDTO) {
|
||||
if (poll.published) {
|
||||
throw Error('This poll is closed !');
|
||||
}
|
||||
await this.pollRepository.update(poll, pollUpdate);
|
||||
}
|
||||
|
||||
async read(pollId: string) {
|
||||
const poll = await this.pollRepository.findOneBy({ publicCode: pollId });
|
||||
return poll;
|
||||
}
|
||||
|
||||
async readAdmin(pollId: string, editCode: string) {
|
||||
const poll = await this.pollRepository.findOneBy({
|
||||
publicCode: pollId,
|
||||
editCode: editCode,
|
||||
});
|
||||
return poll;
|
||||
}
|
||||
|
||||
async hasAccess(poll: Poll, accessCode: string) {
|
||||
return await this.inviteRepository.exists({
|
||||
where: {
|
||||
poll: { id: poll.id },
|
||||
value: accessCode,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { PollsService } from './polls.service';
|
||||
import { PollReadAdminDTO } from './dto/poll.dto';
|
||||
import { QuestionsService } from './questions.service';
|
||||
import { AnswerCreateDto } from './dto/answer.dto';
|
||||
|
||||
@ApiTags('questions')
|
||||
@Controller('questions')
|
||||
export class QuestionsController {
|
||||
constructor(
|
||||
private readonly questionsService: QuestionsService,
|
||||
private readonly pollService: PollsService,
|
||||
) {}
|
||||
|
||||
@Post(':publicCode/:accessCode')
|
||||
@ApiOperation({ summary: 'Vote for a question' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Vote successfully saved.',
|
||||
type: PollReadAdminDTO,
|
||||
})
|
||||
async vote(
|
||||
@Param('publicCode', new ParseUUIDPipe()) publicCode: string,
|
||||
@Param('accessCode', new ParseUUIDPipe()) accessCode: string,
|
||||
@Body() answer: AnswerCreateDto,
|
||||
) {
|
||||
const poll = await this.pollService.read(publicCode);
|
||||
if (!poll) {
|
||||
throw new HttpException('Poll not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (!poll?.published) {
|
||||
throw new HttpException('Poll not yet published', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
if (!(await this.pollService.hasAccess(poll, accessCode))) {
|
||||
throw new HttpException('Invalid access code', HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
const question = this.questionsService.get(poll, answer.questionId);
|
||||
if (!question) {
|
||||
throw new HttpException('Question not found', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
await this.questionsService.vote(question, null, answer.choice);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { Question } from './entities/question.entity';
|
||||
import { Poll } from './entities/poll.entity';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionsService {
|
||||
constructor(
|
||||
@InjectRepository(Question)
|
||||
private questionRepository: Repository<Question>,
|
||||
) {}
|
||||
|
||||
get(poll: Poll, questionId: number) {
|
||||
const question = poll.questions.find((q) => q.id == questionId);
|
||||
return question;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { VoteController } from './vote.controller';
|
||||
|
||||
describe('VoteController', () => {
|
||||
let controller: VoteController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [VoteController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<VoteController>(VoteController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { VoteCreateDTO } from './vote.dto';
|
||||
import { VoteService } from './vote.service';
|
||||
import { ApiOperation } from '@nestjs/swagger';
|
||||
|
||||
@Controller('')
|
||||
export class VoteController {
|
||||
constructor(private readonly voteService: VoteService) {}
|
||||
|
||||
@Get('check/:accessCode')
|
||||
@ApiOperation({ summary: 'Checks if a code has been used' })
|
||||
async check(@Param('accessCode', new ParseUUIDPipe()) accessCode: string) {
|
||||
const value = await this.voteService.check(accessCode);
|
||||
if (value === null) {
|
||||
throw new HttpException('Invalid accessCode', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (value === false) {
|
||||
throw new HttpException('AccessCode expired', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@Post('vote/:accessCode')
|
||||
@ApiOperation({ summary: 'Vote for a poll' })
|
||||
async vote(
|
||||
@Param('accessCode', new ParseUUIDPipe()) accessCode: string,
|
||||
@Body() answer: VoteCreateDTO,
|
||||
) {
|
||||
const code = await this.voteService.check(accessCode);
|
||||
if (code === null) {
|
||||
throw new HttpException('Invalid accessCode', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (code === false) {
|
||||
throw new HttpException('AccessCode expired', HttpStatus.FORBIDDEN);
|
||||
}
|
||||
await this.voteService.create(answer, code);
|
||||
}
|
||||
|
||||
@Get('results/:accessCode')
|
||||
@ApiOperation({ summary: 'Get results' })
|
||||
async results(@Param('accessCode', new ParseUUIDPipe()) accessCode: string) {
|
||||
return await this.voteService.getResults();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class VoteCreateDTO {
|
||||
'1': string;
|
||||
'2.1': string;
|
||||
}
|
||||
|
||||
export class VoteResultsDTO {
|
||||
'1': number[];
|
||||
'2': number[];
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
Column,
|
||||
OneToOne,
|
||||
Generated,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Vote {
|
||||
@PrimaryGeneratedColumn({})
|
||||
id: number;
|
||||
|
||||
@OneToMany(() => Answer, (answer) => answer.vote, {
|
||||
cascade: true,
|
||||
eager: true,
|
||||
})
|
||||
answers: Answer[];
|
||||
|
||||
@OneToOne(() => InviteCode, (code) => code.vote, {
|
||||
eager: false,
|
||||
onDelete: 'CASCADE',
|
||||
nullable: false,
|
||||
})
|
||||
code: Promise<InviteCode>;
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Answer {
|
||||
@PrimaryGeneratedColumn({})
|
||||
id: number;
|
||||
|
||||
@ManyToOne(() => Vote, (vote) => vote.answers, {
|
||||
eager: false,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
vote: Promise<Vote>;
|
||||
|
||||
@Column()
|
||||
question_id: string;
|
||||
|
||||
@Column()
|
||||
answer: string;
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class InviteCode {
|
||||
@PrimaryGeneratedColumn({})
|
||||
id: number;
|
||||
|
||||
@OneToOne(() => Vote, (vote) => vote.code, {
|
||||
eager: false,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
vote: Promise<Vote>;
|
||||
|
||||
@Column({ default: 1 })
|
||||
usages_left: number;
|
||||
|
||||
@Column()
|
||||
@Generated('uuid')
|
||||
value: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InviteCode, Vote } from './vote.entity';
|
||||
import { VoteController } from './vote.controller';
|
||||
import { VoteService } from './vote.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Vote, InviteCode])],
|
||||
controllers: [VoteController],
|
||||
providers: [VoteService],
|
||||
})
|
||||
export class VoteModule {}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PollsService } from './polls.service';
|
||||
import { VoteService } from './vote.service';
|
||||
|
||||
describe('PollsService', () => {
|
||||
let service: PollsService;
|
||||
describe('VoteService', () => {
|
||||
let service: VoteService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PollsService],
|
||||
providers: [VoteService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PollsService>(PollsService);
|
||||
service = module.get<VoteService>(VoteService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { VoteCreateDTO } from './vote.dto';
|
||||
import { InviteCode, Vote } from './vote.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
interface IAnswer {
|
||||
question_id: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface IVote {
|
||||
answers: IAnswer[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class VoteService {
|
||||
constructor(
|
||||
@InjectRepository(Vote) private voteRepository: Repository<Vote>,
|
||||
@InjectRepository(InviteCode)
|
||||
private inviteCodeRepository: Repository<InviteCode>,
|
||||
) {}
|
||||
async create(value: VoteCreateDTO, accessCode: InviteCode) {
|
||||
accessCode.usages_left--;
|
||||
await this.inviteCodeRepository.save(accessCode);
|
||||
|
||||
const vote = Object.entries(value).reduce<IVote>(
|
||||
(acc, [question_id, a]) => {
|
||||
const answer = String(a);
|
||||
acc.answers.push({ question_id, answer });
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
answers: [],
|
||||
},
|
||||
);
|
||||
const voteEntity = this.voteRepository.create(vote);
|
||||
await this.voteRepository.save(voteEntity);
|
||||
}
|
||||
|
||||
async getResults() {
|
||||
const all_votes = await this.voteRepository.find();
|
||||
const compiled_results: { [question: string]: { [vote: string]: number } } =
|
||||
{};
|
||||
|
||||
for (const vote of all_votes) {
|
||||
for (const answer of vote.answers) {
|
||||
if (!(answer.question_id in compiled_results)) {
|
||||
compiled_results[answer.question_id] = {};
|
||||
}
|
||||
if (!(answer.answer in compiled_results[answer.question_id])) {
|
||||
compiled_results[answer.question_id][answer.answer] = 0;
|
||||
}
|
||||
compiled_results[answer.question_id][answer.answer]++;
|
||||
}
|
||||
}
|
||||
return compiled_results;
|
||||
}
|
||||
|
||||
async check(inviteCode: string) {
|
||||
const code = await this.inviteCodeRepository.findOneBy({
|
||||
value: inviteCode,
|
||||
});
|
||||
if (!code) {
|
||||
return null;
|
||||
}
|
||||
if (code.usages_left > 0) {
|
||||
return code;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user