Skip to content

Commit e1115c2

Browse files
committed
feat: add service and controller pseudocode
1 parent 7776a35 commit e1115c2

File tree

3 files changed

+288
-4
lines changed

3 files changed

+288
-4
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { Request, Response } from "express";
2+
import type { PollService } from "../services/poll.service";
3+
4+
export class PollController {
5+
constructor(private readonly pollService: PollService) {}
6+
7+
async createPoll(req: Request, res: Response) {
8+
try {
9+
const poll = await this.pollService.createPoll(
10+
req.body,
11+
req.user?.id,
12+
);
13+
res.status(201).json(poll);
14+
} catch (err: any) {
15+
res.status(400).json({ error: err.message });
16+
}
17+
}
18+
19+
async getPoll(req: Request, res: Response) {
20+
try {
21+
const poll = await this.pollService.getPollById(req.params.id);
22+
if (!poll) return res.status(404).json({ error: "Poll not found" });
23+
res.json(poll);
24+
} catch (err: any) {
25+
res.status(500).json({ error: err.message });
26+
}
27+
}
28+
29+
async castVote(req: Request, res: Response) {
30+
try {
31+
const vote = await this.pollService.castVote(
32+
req.params.id,
33+
req.user?.id,
34+
req.body,
35+
);
36+
res.status(201).json(vote);
37+
} catch (err: any) {
38+
res.status(400).json({ error: err.message });
39+
}
40+
}
41+
42+
async getResults(req: Request, res: Response) {
43+
try {
44+
const results = await this.pollService.getResults(req.params.id);
45+
res.json(results);
46+
} catch (err: any) {
47+
res.status(400).json({ error: err.message });
48+
}
49+
}
50+
51+
async getPollsByUser(req: Request, res: Response) {
52+
try {
53+
const polls = await this.pollService.getPollsByUser(req.user?.id);
54+
res.json(polls);
55+
} catch (err: any) {
56+
res.status(500).json({ error: err.message });
57+
}
58+
}
59+
60+
async deletePoll(req: Request, res: Response) {
61+
try {
62+
await this.pollService.deletePoll(req.params.id, req.user?.id);
63+
res.status(204).send();
64+
} catch (err: any) {
65+
res.status(400).json({ error: err.message });
66+
}
67+
}
68+
}

platforms/evoting-api/src/database/entities/Vote.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@ import {
99
} from "typeorm";
1010
import { Poll } from "./Poll";
1111

12+
export type NormalVoteData = string[];
13+
14+
export type PointVoteData = {
15+
option: string;
16+
points: number;
17+
}[];
18+
19+
export type RankVoteData = {
20+
option: string;
21+
points: number; // 1 = top pick, 2 = next, etc.
22+
}[];
23+
24+
export type VoteData = NormalVoteData | PointVoteData | RankVoteData;
25+
26+
export type VoteDataByMode =
27+
| { mode: "normal"; data: NormalVoteData }
28+
| { mode: "point"; data: PointVoteData }
29+
| { mode: "rank"; data: RankVoteData };
30+
1231
@Entity("vote")
1332
export class Vote {
1433
@PrimaryGeneratedColumn("uuid")
@@ -37,10 +56,7 @@ export class Vote {
3756
* Stored as JSON for flexibility
3857
*/
3958
@Column("jsonb")
40-
data:
41-
| string[] // normal
42-
| { option: string; points: number }[] // point
43-
| string[]; // rank
59+
data: VoteDataByMode;
4460

4561
@CreateDateColumn()
4662
createdAt: Date;
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { AppDataSource } from "../database/data-source";
2+
import { Poll } from "../database/entities/Poll";
3+
import { User } from "../database/entities/User";
4+
import {
5+
type PointVoteData,
6+
type RankVoteData,
7+
Vote,
8+
type VoteDataByMode,
9+
} from "../database/entities/Vote";
10+
11+
export function isValidRankVote(
12+
data: unknown,
13+
pollOptions: string[],
14+
): data is RankVoteData {
15+
if (!Array.isArray(data) || data.length !== pollOptions.length)
16+
return false;
17+
18+
const optionSet = new Set(data.map((d) => d.option));
19+
const pointSet = new Set(data.map((d) => d.points));
20+
21+
return (
22+
optionSet.size === pollOptions.length &&
23+
pointSet.size === pollOptions.length &&
24+
data.every(
25+
(d) =>
26+
typeof d.option === "string" &&
27+
typeof d.points === "number" &&
28+
pollOptions.includes(d.option) &&
29+
d.points >= 1 &&
30+
d.points <= pollOptions.length,
31+
)
32+
);
33+
}
34+
35+
function isValidPointVote(
36+
data: unknown,
37+
pollOptions: string[],
38+
): data is PointVoteData {
39+
return (
40+
Array.isArray(data) &&
41+
data.length > 0 &&
42+
new Set(data.map((d) => d.option)).size === data.length &&
43+
data.every(
44+
(d) =>
45+
typeof d.option === "string" &&
46+
typeof d.points === "number" &&
47+
pollOptions.includes(d.option),
48+
)
49+
);
50+
}
51+
52+
export class PollService {
53+
private pollRepository = AppDataSource.getRepository(Poll);
54+
private voteRepository = AppDataSource.getRepository(Vote);
55+
private userRepository = AppDataSource.getRepository(User);
56+
57+
// Create a new poll
58+
async createPoll(
59+
title: string,
60+
mode: "normal" | "point" | "rank",
61+
visibility: "public" | "private",
62+
options: string[],
63+
deadline?: Date,
64+
): Promise<Poll> {
65+
if (options.length < 2) {
66+
throw new Error("At least two options are required");
67+
}
68+
69+
const poll = this.pollRepository.create({
70+
title,
71+
mode,
72+
visibility,
73+
options,
74+
deadline: deadline ?? null,
75+
});
76+
77+
return await this.pollRepository.save(poll);
78+
}
79+
80+
// Get a poll by ID
81+
async getPollById(pollId: string): Promise<Poll | null> {
82+
return await this.pollRepository.findOne({
83+
where: { id: pollId },
84+
relations: ["votes"],
85+
});
86+
}
87+
88+
// Get all polls (optionally by visibility)
89+
async getPolls(visibility?: "public" | "private"): Promise<Poll[]> {
90+
return this.pollRepository.find({
91+
where: visibility ? { visibility } : {},
92+
relations: ["votes"],
93+
});
94+
}
95+
96+
// Cast a vote
97+
async castVote(
98+
pollId: string,
99+
userId: string,
100+
data: VoteDataByMode, // should be validated before calling
101+
): Promise<Vote> {
102+
const poll = await this.getPollById(pollId);
103+
if (!poll) throw new Error("Poll not found");
104+
105+
const now = new Date();
106+
if (poll.deadline && poll.deadline < now) {
107+
throw new Error("Poll has expired");
108+
}
109+
110+
const user = await this.userRepository.findOneBy({ id: userId });
111+
if (!user) throw new Error("User not found");
112+
113+
// Check if user already voted
114+
const existing = await this.voteRepository.findOneBy({
115+
pollId,
116+
voterId: userId,
117+
});
118+
if (existing) throw new Error("User has already voted");
119+
120+
// Validate data based on poll mode
121+
switch (poll.mode) {
122+
case "normal":
123+
if (
124+
!Array.isArray(data) ||
125+
data.length === 0 ||
126+
data.some(
127+
(d) =>
128+
typeof d !== "string" || !poll.options.includes(d),
129+
)
130+
) {
131+
throw new Error("Invalid vote for normal mode");
132+
}
133+
break;
134+
case "point":
135+
if (
136+
!Array.isArray(data) ||
137+
data.length === 0 ||
138+
data.some(
139+
(d) =>
140+
typeof d.option !== "string" ||
141+
typeof d.points !== "number" ||
142+
!poll.options.includes(d.option),
143+
)
144+
) {
145+
throw new Error("Invalid vote for point mode");
146+
}
147+
break;
148+
case "rank":
149+
if (
150+
!Array.isArray(data) ||
151+
data.length !== poll.options.length ||
152+
data.some((d) => typeof d !== "string") ||
153+
new Set(data).size !== data.length ||
154+
data.some((option) => !poll.options.includes(option))
155+
) {
156+
throw new Error("Invalid vote for rank mode");
157+
}
158+
break;
159+
}
160+
161+
const vote = this.voteRepository.create({
162+
poll,
163+
voterId: user.id,
164+
data,
165+
});
166+
167+
return await this.voteRepository.save(vote);
168+
}
169+
170+
// Get votes for a poll
171+
async getVotes(pollId: string): Promise<Vote[]> {
172+
return await this.voteRepository.find({
173+
where: { pollId },
174+
});
175+
}
176+
177+
// Get vote count
178+
async getVoteCount(pollId: string): Promise<number> {
179+
return await this.voteRepository.count({
180+
where: { pollId },
181+
});
182+
}
183+
184+
// Optional: remove vote (e.g., allow user to change vote)
185+
async removeVote(pollId: string, userId: string): Promise<void> {
186+
await this.voteRepository.delete({
187+
pollId,
188+
voterId: userId,
189+
});
190+
}
191+
192+
// Optional: close poll early
193+
async closePoll(pollId: string): Promise<Poll> {
194+
const poll = await this.getPollById(pollId);
195+
if (!poll) throw new Error("Poll not found");
196+
197+
poll.deadline = new Date();
198+
return await this.pollRepository.save(poll);
199+
}
200+
}

0 commit comments

Comments
 (0)