Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f3a8fc6
Merge pull request #87 from GeneralMagicio/main
RamRamez Apr 30, 2025
7cbaec8
add PollStatus
RamRamez May 4, 2025
9426a19
change all poll fields to optional
RamRamez May 4, 2025
d2ca479
add patchDraftPoll and getDraftPoll to poll controller
RamRamez May 4, 2025
f3eeb14
add DraftPollDto
RamRamez May 4, 2025
7d82885
add draft poll to poll service
RamRamez May 4, 2025
95991f7
fix filters.AND error
RamRamez May 4, 2025
89a1e30
change userActivities to join only published polls
RamRamez May 4, 2025
e0a703b
check for existingDraft
RamRamez May 5, 2025
1dfe4dc
Merge pull request #88 from GeneralMagicio/add-draft-poll
RamRamez May 5, 2025
67f4612
prevent race conditions in creating draft poll
RamRamez May 6, 2025
26a147e
Merge pull request #97 from GeneralMagicio/prevent-race-conditions-in…
RamRamez May 7, 2025
70670e9
Merge pull request #98 from GeneralMagicio/main
Meriem-BM May 7, 2025
37e8d42
normalize voting distribution
RamRamez May 12, 2025
1d54b87
add env var to switch normalization on and off
RamRamez May 12, 2025
feb2ffe
update env example
RamRamez May 12, 2025
ec50b84
update comprehensive documentation for worldview BE
RamRamez May 12, 2025
6cc6db6
Merge pull request #99 from GeneralMagicio/normalize-voting-distribution
RamRamez May 12, 2025
dfed291
Merge pull request #100 from GeneralMagicio/update-comprehensive-docu…
RamRamez May 12, 2025
ce8610a
feat: add users count route
Meriem-BM May 14, 2025
07f1127
feat: add vote & poll count
Meriem-BM May 16, 2025
8e53b91
chore: remove console log
Meriem-BM May 16, 2025
83b0f13
chore: stats queries issue
Meriem-BM May 16, 2025
6c1f48d
fix: add common dto & add date validation
Meriem-BM May 16, 2025
309c015
resolve nerge conflict
Meriem-BM May 16, 2025
481e2e3
Delete all existing drafts on creating new poll
RamRamez May 17, 2025
f668124
add getPollVotes endpoint
RamRamez May 18, 2025
52a7d28
Merge pull request #104 from GeneralMagicio/feat/add_stats_endpoints
Meriem-BM May 19, 2025
db1d82c
Merge pull request #106 from GeneralMagicio/Delete-all-existing-drafts
RamRamez May 19, 2025
cb1bbcd
Merge pull request #107 from GeneralMagicio/add-getPollVotes-endpoint
RamRamez May 19, 2025
be8d82f
change getPollVotes to return normalized votes
RamRamez May 20, 2025
195ae7c
Merge pull request #108 from GeneralMagicio/change-getPollVotes-to-re…
RamRamez May 20, 2025
6ab007f
add pollId and title to getPollVotes
RamRamez May 20, 2025
12294cb
Merge pull request #109 from GeneralMagicio/add-pollId-and-title-to-g…
RamRamez May 20, 2025
b208a50
fix: explore all polls order
Meriem-BM May 21, 2025
951adbb
Merge pull request #111 from GeneralMagicio/fix/polls_list_all_order
Meriem-BM May 21, 2025
61ad706
add isAnonymous to getUserActivities
RamRamez May 21, 2025
5f51958
Merge pull request #112 from GeneralMagicio/add-isAnonymous-to-getUse…
RamRamez May 21, 2025
32bf1a7
add isAnonymoys to action
RamRamez May 21, 2025
2720b71
Merge pull request #113 from GeneralMagicio/add-isAnonymous-to-actions
RamRamez May 21, 2025
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
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ DATABASE_URL=
WHITELISTED_ORIGINS=
TUNNEL_DOMAINS=
JWT_SECRET=
VERIFICATION_LEVEL="device" # "orb" or "device", default is "device"
ENABLE_VOTE_NORMALIZATION=
VERIFICATION_LEVEL="device" # "orb" or "device", default is "device"
376 changes: 308 additions & 68 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "PollStatus" AS ENUM ('DRAFT', 'PUBLISHED');

-- AlterTable
ALTER TABLE "Poll" ADD COLUMN "status" "PollStatus" NOT NULL DEFAULT 'PUBLISHED';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "Poll" ALTER COLUMN "title" DROP NOT NULL,
ALTER COLUMN "startDate" DROP NOT NULL,
ALTER COLUMN "endDate" DROP NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
-- First drop the existing trigger that might be using these functions
DROP TRIGGER IF EXISTS normalize_weights_trigger ON "Vote";

-- Drop existing functions for clean recreation
DROP FUNCTION IF EXISTS update_normalized_weights();
DROP FUNCTION IF EXISTS json_normalize_weights(jsonb, integer);

-- Recreate the normalize weights function with IMMUTABLE
CREATE OR REPLACE FUNCTION json_normalize_weights(weight_distribution JSONB, voting_power INTEGER)
RETURNS JSONB AS $$
DECLARE
total NUMERIC := 0;
weight NUMERIC;
key TEXT;
normalized_weights JSONB := '{}'::JSONB;
normalized_total NUMERIC := 0;
diff INTEGER;
first_nonzero_key TEXT := NULL;
BEGIN
-- First check if input is a valid JSON object
IF weight_distribution IS NULL OR jsonb_typeof(weight_distribution) != 'object' THEN
-- Return empty object if input is not a valid object
RETURN '{}'::JSONB;
END IF;

-- Calculate total weights
FOR key, weight IN SELECT * FROM jsonb_each_text(weight_distribution)
LOOP
total := total + weight::NUMERIC;
END LOOP;

-- If total is 0 or already equals target, return original
IF total = 0 OR total = voting_power THEN
RETURN weight_distribution;
END IF;

-- Normalize each weight
FOR key, weight IN SELECT * FROM jsonb_each_text(weight_distribution)
LOOP
-- Formula: normalizedValue = (weightValue / sumOfAllWeights) * voting_power
-- Round to integer to avoid fractional weights
normalized_weights := normalized_weights ||
jsonb_build_object(key, ROUND((weight::NUMERIC / total) * voting_power));

-- Keep track of first non-zero value to adjust if necessary
IF first_nonzero_key IS NULL AND weight::NUMERIC > 0 THEN
first_nonzero_key := key;
END IF;
END LOOP;

-- Check if rounding caused total to drift from target
SELECT SUM(value::NUMERIC) INTO normalized_total FROM jsonb_each_text(normalized_weights);

IF normalized_total != voting_power AND first_nonzero_key IS NOT NULL THEN
diff := voting_power - normalized_total;
normalized_weights := jsonb_set(
normalized_weights,
ARRAY[first_nonzero_key],
to_jsonb((normalized_weights->>first_nonzero_key)::NUMERIC + diff)
);
END IF;

RETURN normalized_weights;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

-- Make sure calculate_normalized_quadratic_weights is immutable
DROP FUNCTION IF EXISTS calculate_normalized_quadratic_weights(jsonb);

CREATE OR REPLACE FUNCTION calculate_normalized_quadratic_weights(weights JSONB)
RETURNS JSONB AS $$
DECLARE
weight NUMERIC;
key TEXT;
quadratic_weights JSONB := '{}'::JSONB;
BEGIN
-- Calculate quadratic weights for each value
FOR key, weight IN SELECT * FROM jsonb_each_text(weights)
LOOP
-- Formula: quadraticWeight = sqrt(weight)
quadratic_weights := quadratic_weights ||
jsonb_build_object(key, ROUND(SQRT(weight::NUMERIC), 2));
END LOOP;

RETURN quadratic_weights;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

-- Only drop and recreate the columns we're interested in
ALTER TABLE "Vote" DROP COLUMN IF EXISTS "normalizedWeightDistribution";
ALTER TABLE "Vote" DROP COLUMN IF EXISTS "normalizedQuadraticWeights";

-- Add generated columns
ALTER TABLE "Vote" ADD COLUMN "normalizedWeightDistribution" JSONB
GENERATED ALWAYS AS (json_normalize_weights("weightDistribution", "votingPower")) STORED;

ALTER TABLE "Vote" ADD COLUMN "normalizedQuadraticWeights" JSONB
GENERATED ALWAYS AS (calculate_normalized_quadratic_weights(json_normalize_weights("weightDistribution", "votingPower"))) STORED;

-- Comment explaining usage
COMMENT ON FUNCTION json_normalize_weights IS
'Normalizes a JSON weight distribution so values sum to the specified voting power.
Usage: SELECT json_normalize_weights(''{"option1": 10, "option2": 15}'', 100)
Returns: {"option1": 40, "option2": 60}';

COMMENT ON FUNCTION calculate_normalized_quadratic_weights IS
'Calculates quadratic weights from linear weights.
Usage: SELECT calculate_normalized_quadratic_weights(''{"option1": 40, "option2": 60}'')
Returns: {"option1": 6.32, "option2": 7.75}';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
33 changes: 21 additions & 12 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ model User {
profilePicture String?
pollsCreatedCount Int @default(0)
pollsParticipatedCount Int @default(0)
createdAt DateTime @default(now())
createdPolls Poll[] @relation("PollAuthor")
actions UserAction[]
votes Vote[]
Expand All @@ -33,35 +34,43 @@ model UserAction {
model Poll {
pollId Int @id @default(autoincrement())
authorUserId Int
title String
title String?
description String?
options String[]
creationDate DateTime @default(now())
startDate DateTime
endDate DateTime
startDate DateTime?
endDate DateTime?
tags String[]
isAnonymous Boolean @default(false)
participantCount Int @default(0)
voteResults Json
searchVector Unsupported("tsvector")?
status PollStatus @default(PUBLISHED)
author User @relation("PollAuthor", fields: [authorUserId], references: [id])
userAction UserAction[]
votes Vote[]
}

model Vote {
voteID String @id @default(uuid())
userId Int
pollId Int
votingPower Int
weightDistribution Json
proof String
quadraticWeights Json? @default(dbgenerated("calculate_quadratic_weights(\"weightDistribution\")"))
poll Poll @relation(fields: [pollId], references: [pollId], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
voteID String @id @default(uuid())
userId Int
pollId Int
votingPower Int
weightDistribution Json
proof String
quadraticWeights Json? @default(dbgenerated("calculate_quadratic_weights(\"weightDistribution\")"))
normalizedWeightDistribution Json? @default(dbgenerated("json_normalize_weights(\"weightDistribution\", \"votingPower\")"))
normalizedQuadraticWeights Json? @default(dbgenerated("calculate_normalized_quadratic_weights(json_normalize_weights(\"weightDistribution\", \"votingPower\"))"))
poll Poll @relation(fields: [pollId], references: [pollId], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
}

enum ActionType {
CREATED
VOTED
}

enum PollStatus {
DRAFT
PUBLISHED
}
3 changes: 2 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { AppService } from './app.service';
import { DatabaseService } from './database/database.service';
import { DatabaseModule } from './database/database.module';
import { PollModule } from './poll/poll.module';
import { VoteModule } from './vote/vote.module';
import { AuthModule } from './auth/auth.module';
import { AuthService } from './auth/auth.service';
import { UserModule } from './user/user.module';

@Module({
imports: [DatabaseModule, PollModule, UserModule, AuthModule],
imports: [DatabaseModule, PollModule, VoteModule, UserModule, AuthModule],
controllers: [AppController],
providers: [AppService, DatabaseService, AuthService],
})
Expand Down
15 changes: 15 additions & 0 deletions src/common/common.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsDateString, IsOptional, IsString, Matches } from 'class-validator';

export class GetCountDto {
@IsOptional()
@IsString()
@Matches(/^\d{4}-\d{2}-\d{2}$/)
@IsDateString({ strict: true })
from?: string;

@IsOptional()
@IsString()
@Matches(/^\d{4}-\d{2}-\d{2}$/)
@IsDateString({ strict: true })
to?: string;
}
46 changes: 46 additions & 0 deletions src/poll/Poll.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
IsOptional,
IsString,
Min,
Validate,
} from 'class-validator';
import { IsPositiveInteger } from '../common/validators';

export class CreatePollDto {
@IsString()
Expand Down Expand Up @@ -43,6 +45,43 @@ export class CreatePollDto {
isAnonymous?: boolean;
}

export class DraftPollDto {
@IsOptional()
@IsString()
title?: string;

@IsOptional()
@IsString()
description?: string;

@IsOptional()
@IsArray()
@IsString({ each: true })
options?: string[];

@IsOptional()
@IsDateString()
startDate?: string;

@IsOptional()
@IsDateString()
endDate?: string;

@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];

@IsOptional()
@IsBoolean()
isAnonymous?: boolean;

@IsOptional()
@Validate(IsPositiveInteger)
@Transform(({ value }) => (value ? parseInt(value, 10) : undefined))
pollId?: number;
}

export class GetPollsDto {
@IsString()
@IsOptional()
Expand Down Expand Up @@ -83,3 +122,10 @@ export class GetPollsDto {
@IsEnum(['asc', 'desc'])
sortOrder?: 'asc' | 'desc';
}

export class GetPollVotesDto {
@IsInt()
@Type(() => Number)
@IsNotEmpty()
pollId: number;
}
31 changes: 29 additions & 2 deletions src/poll/poll.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import {
Delete,
Get,
Param,
Patch,
Post,
Query,
} from '@nestjs/common';
import { CreatePollDto, GetPollsDto } from './Poll.dto';
import { PollService } from './poll.service';
import { User } from 'src/auth/user.decorator';
import { CreatePollDto, DraftPollDto, GetPollsDto } from './Poll.dto';
import { PollService } from './poll.service';
import { Public } from 'src/auth/jwt-auth.guard';
import { GetCountDto } from '../common/common.dto';

@Controller('poll')
export class PollController {
Expand All @@ -23,6 +26,19 @@ export class PollController {
return await this.pollService.createPoll(dto, worldID);
}

@Patch('draft')
async patchDraftPoll(
@Body() dto: DraftPollDto,
@User('worldID') worldID: string,
) {
return await this.pollService.patchDraftPoll(dto, worldID);
}

@Get('draft')
async getDraftPoll(@User('worldID') worldID: string) {
return await this.pollService.getUserDraftPoll(worldID);
}

@Get()
async getPolls(
@Query() query: GetPollsDto,
Expand All @@ -31,11 +47,22 @@ export class PollController {
return await this.pollService.getPolls(query, worldID);
}

@Get('count')
@Public()
async getPollsCount(@Query() query: GetCountDto): Promise<number> {
return await this.pollService.getPollsCount(query);
}

@Get(':id')
async getPollDetails(@Param('id') id: number) {
return await this.pollService.getPollDetails(Number(id));
}

@Get(':id/votes')
async getPollVotes(@Param('id') id: number) {
return await this.pollService.getPollVotes(Number(id));
}

@Delete(':id')
async deletePoll(@Param('id') id: number, @User('worldID') worldID: string) {
const poll = await this.pollService.deletePoll(Number(id), worldID);
Expand Down
Loading