mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
Add poll database schema and basic implementation
Co-authored-by: CPlusPatch <42910258+CPlusPatch@users.noreply.github.com>
This commit is contained in:
parent
8645093a3f
commit
31171b5fc7
|
|
@ -6,7 +6,7 @@ import {
|
||||||
StatusSource as StatusSourceSchema,
|
StatusSource as StatusSourceSchema,
|
||||||
zBoolean,
|
zBoolean,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { Emoji, Media, Note } from "@versia/kit/db";
|
import { Emoji, Media, Note, Poll } from "@versia/kit/db";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute } from "hono-openapi";
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
|
|
@ -164,6 +164,10 @@ export default apiRoute((app) =>
|
||||||
visibility,
|
visibility,
|
||||||
content_type,
|
content_type,
|
||||||
local_only,
|
local_only,
|
||||||
|
"poll[options]": pollOptions,
|
||||||
|
"poll[expires_in]": pollExpiresIn,
|
||||||
|
"poll[multiple]": pollMultiple,
|
||||||
|
"poll[hide_totals]": pollHideTotals,
|
||||||
} = context.req.valid("json");
|
} = context.req.valid("json");
|
||||||
|
|
||||||
// Check if media attachments are all valid
|
// Check if media attachments are all valid
|
||||||
|
|
@ -177,6 +181,27 @@ export default apiRoute((app) =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate poll parameters
|
||||||
|
if (pollOptions && pollOptions.length > 0) {
|
||||||
|
if (media_ids.length > 0) {
|
||||||
|
throw new ApiError(422, "Cannot attach poll to media");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pollExpiresIn) {
|
||||||
|
throw new ApiError(
|
||||||
|
422,
|
||||||
|
"poll[expires_in] must be provided when creating a poll"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pollOptions.length < 2) {
|
||||||
|
throw new ApiError(
|
||||||
|
422,
|
||||||
|
"Poll must have at least 2 options"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const reply = in_reply_to_id
|
const reply = in_reply_to_id
|
||||||
? await Note.fromId(in_reply_to_id)
|
? await Note.fromId(in_reply_to_id)
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -248,6 +273,21 @@ export default apiRoute((app) =>
|
||||||
await newNote.updateMentions(parsedMentions);
|
await newNote.updateMentions(parsedMentions);
|
||||||
await newNote.updateAttachments(foundAttachments);
|
await newNote.updateAttachments(foundAttachments);
|
||||||
|
|
||||||
|
// Create poll if poll options are provided
|
||||||
|
if (pollOptions && pollOptions.length > 0 && pollExpiresIn) {
|
||||||
|
const expiresAt = new Date(Date.now() + pollExpiresIn * 1000).toISOString();
|
||||||
|
|
||||||
|
await Poll.insert({
|
||||||
|
id: randomUUIDv7(),
|
||||||
|
noteId: newNote.data.id,
|
||||||
|
expiresAt,
|
||||||
|
multiple: pollMultiple ?? false,
|
||||||
|
hideTotals: pollHideTotals ?? false,
|
||||||
|
votesCount: 0,
|
||||||
|
votersCount: 0,
|
||||||
|
}, pollOptions);
|
||||||
|
}
|
||||||
|
|
||||||
await newNote.reload();
|
await newNote.reload();
|
||||||
|
|
||||||
if (!local_only) {
|
if (!local_only) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { mergeAndDeduplicate } from "@/lib.ts";
|
||||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||||
import { contentToHtml, findManyNotes } from "~/classes/functions/status";
|
import { contentToHtml, findManyNotes } from "~/classes/functions/status";
|
||||||
import { config } from "~/config.ts";
|
import { config } from "~/config.ts";
|
||||||
|
import { Poll } from "./poll.ts";
|
||||||
import * as VersiaEntities from "~/packages/sdk/entities/index.ts";
|
import * as VersiaEntities from "~/packages/sdk/entities/index.ts";
|
||||||
import type { NonTextContentFormatSchema } from "~/packages/sdk/schemas/contentformat.ts";
|
import type { NonTextContentFormatSchema } from "~/packages/sdk/schemas/contentformat.ts";
|
||||||
import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
|
import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
|
||||||
|
|
@ -55,6 +56,7 @@ type NoteTypeWithRelations = NoteType & {
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
liked: boolean;
|
liked: boolean;
|
||||||
reactions: Omit<typeof Reaction.$type, "note" | "author">[];
|
reactions: Omit<typeof Reaction.$type, "note" | "author">[];
|
||||||
|
poll: typeof Poll.$type | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NoteTypeWithoutRecursiveRelations = Omit<
|
export type NoteTypeWithoutRecursiveRelations = Omit<
|
||||||
|
|
@ -691,8 +693,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
language: null,
|
language: null,
|
||||||
muted: data.muted,
|
muted: data.muted,
|
||||||
pinned: data.pinned,
|
pinned: data.pinned,
|
||||||
// TODO: Add polls
|
poll: data.poll ? data.poll.toApi(userFetching) : null,
|
||||||
poll: null,
|
|
||||||
reblog: data.reblog
|
reblog: data.reblog
|
||||||
? await new Note(data.reblog as NoteTypeWithRelations).toApi(
|
? await new Note(data.reblog as NoteTypeWithRelations).toApi(
|
||||||
userFetching,
|
userFetching,
|
||||||
|
|
|
||||||
284
classes/database/poll.ts
Normal file
284
classes/database/poll.ts
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
import { db } from "@versia/kit/db";
|
||||||
|
import {
|
||||||
|
Notes,
|
||||||
|
PollOptions,
|
||||||
|
Polls,
|
||||||
|
PollVotes,
|
||||||
|
type Users,
|
||||||
|
} from "@versia/kit/tables";
|
||||||
|
import {
|
||||||
|
and,
|
||||||
|
eq,
|
||||||
|
type InferInsertModel,
|
||||||
|
type InferSelectModel,
|
||||||
|
inArray,
|
||||||
|
} from "drizzle-orm";
|
||||||
|
import type { z } from "zod";
|
||||||
|
import type { Poll as PollSchema } from "@versia/client/schemas";
|
||||||
|
import { BaseInterface } from "./base.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type definition for Poll with all relations
|
||||||
|
*/
|
||||||
|
type PollTypeWithRelations = InferSelectModel<typeof Polls> & {
|
||||||
|
options: (InferSelectModel<typeof PollOptions> & {
|
||||||
|
votes: InferSelectModel<typeof PollVotes>[];
|
||||||
|
})[];
|
||||||
|
votes: InferSelectModel<typeof PollVotes>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database class for managing polls
|
||||||
|
*/
|
||||||
|
export class Poll extends BaseInterface<typeof Polls, PollTypeWithRelations> {
|
||||||
|
public static $type: PollTypeWithRelations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the poll data from the database
|
||||||
|
*/
|
||||||
|
public async reload(): Promise<void> {
|
||||||
|
const reloaded = await Poll.fromId(this.data.id);
|
||||||
|
|
||||||
|
if (!reloaded) {
|
||||||
|
throw new Error("Failed to reload poll");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data = reloaded.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a poll by ID
|
||||||
|
* @param id - The poll ID
|
||||||
|
* @returns The poll instance or null if not found
|
||||||
|
*/
|
||||||
|
public static async fromId(id: string | null): Promise<Poll | null> {
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Poll.fromSql(eq(Polls.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a poll by note ID
|
||||||
|
* @param noteId - The note ID
|
||||||
|
* @returns The poll instance or null if not found
|
||||||
|
*/
|
||||||
|
public static async fromNoteId(noteId: string): Promise<Poll | null> {
|
||||||
|
return await Poll.fromSql(eq(Polls.noteId, noteId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get multiple polls by IDs
|
||||||
|
* @param ids - Array of poll IDs
|
||||||
|
* @returns Array of poll instances
|
||||||
|
*/
|
||||||
|
public static async fromIds(ids: string[]): Promise<Poll[]> {
|
||||||
|
return await Poll.manyFromSql(inArray(Polls.id, ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute SQL query to get a single poll with relations
|
||||||
|
* @param sql - SQL condition
|
||||||
|
* @returns Poll instance or null
|
||||||
|
*/
|
||||||
|
protected static async fromSql(sql: any): Promise<Poll | null> {
|
||||||
|
const result = await db
|
||||||
|
.select()
|
||||||
|
.from(Polls)
|
||||||
|
.leftJoin(PollOptions, eq(Polls.id, PollOptions.pollId))
|
||||||
|
.leftJoin(PollVotes, eq(PollOptions.id, PollVotes.optionId))
|
||||||
|
.where(sql);
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group the results to build the poll object with options
|
||||||
|
const pollData = result[0].Polls;
|
||||||
|
const optionsMap = new Map<string, any>();
|
||||||
|
const votesData: InferSelectModel<typeof PollVotes>[] = [];
|
||||||
|
|
||||||
|
for (const row of result) {
|
||||||
|
if (row.PollOptions) {
|
||||||
|
if (!optionsMap.has(row.PollOptions.id)) {
|
||||||
|
optionsMap.set(row.PollOptions.id, {
|
||||||
|
...row.PollOptions,
|
||||||
|
votes: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.PollVotes) {
|
||||||
|
optionsMap.get(row.PollOptions.id)!.votes.push(row.PollVotes);
|
||||||
|
votesData.push(row.PollVotes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = Array.from(optionsMap.values()).sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
|
const pollWithRelations: PollTypeWithRelations = {
|
||||||
|
...pollData,
|
||||||
|
options,
|
||||||
|
votes: votesData,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Poll(pollWithRelations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute SQL query to get multiple polls with relations
|
||||||
|
* @param sql - SQL condition
|
||||||
|
* @returns Array of poll instances
|
||||||
|
*/
|
||||||
|
protected static async manyFromSql(sql: any): Promise<Poll[]> {
|
||||||
|
const result = await db
|
||||||
|
.select()
|
||||||
|
.from(Polls)
|
||||||
|
.leftJoin(PollOptions, eq(Polls.id, PollOptions.pollId))
|
||||||
|
.leftJoin(PollVotes, eq(PollOptions.id, PollVotes.optionId))
|
||||||
|
.where(sql);
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by poll ID
|
||||||
|
const pollsMap = new Map<string, any>();
|
||||||
|
|
||||||
|
for (const row of result) {
|
||||||
|
const pollId = row.Polls.id;
|
||||||
|
|
||||||
|
if (!pollsMap.has(pollId)) {
|
||||||
|
pollsMap.set(pollId, {
|
||||||
|
...row.Polls,
|
||||||
|
options: new Map(),
|
||||||
|
votes: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const poll = pollsMap.get(pollId);
|
||||||
|
|
||||||
|
if (row.PollOptions) {
|
||||||
|
if (!poll.options.has(row.PollOptions.id)) {
|
||||||
|
poll.options.set(row.PollOptions.id, {
|
||||||
|
...row.PollOptions,
|
||||||
|
votes: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.PollVotes) {
|
||||||
|
poll.options.get(row.PollOptions.id)!.votes.push(row.PollVotes);
|
||||||
|
poll.votes.push(row.PollVotes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(pollsMap.values()).map((pollData) => {
|
||||||
|
const options = Array.from(pollData.options.values()).sort(
|
||||||
|
(a, b) => a.index - b.index,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Poll({
|
||||||
|
...pollData,
|
||||||
|
options,
|
||||||
|
votes: pollData.votes,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new poll into the database
|
||||||
|
* @param pollData - Poll data to insert
|
||||||
|
* @param options - Poll options to insert
|
||||||
|
* @returns The inserted poll instance
|
||||||
|
*/
|
||||||
|
public static async insert(
|
||||||
|
pollData: InferInsertModel<typeof Polls>,
|
||||||
|
options: string[],
|
||||||
|
): Promise<Poll> {
|
||||||
|
return await db.transaction(async (tx) => {
|
||||||
|
// Insert the poll
|
||||||
|
const insertedPoll = (await tx.insert(Polls).values(pollData).returning())[0];
|
||||||
|
|
||||||
|
// Insert poll options
|
||||||
|
const optionInserts = options.map((title, index) => ({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
pollId: insertedPoll.id,
|
||||||
|
title,
|
||||||
|
index,
|
||||||
|
votesCount: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await tx.insert(PollOptions).values(optionInserts);
|
||||||
|
|
||||||
|
// Return the poll with relations
|
||||||
|
const poll = await Poll.fromId(insertedPoll.id);
|
||||||
|
if (!poll) {
|
||||||
|
throw new Error("Failed to retrieve inserted poll");
|
||||||
|
}
|
||||||
|
|
||||||
|
return poll;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the poll has expired
|
||||||
|
* @returns True if the poll has expired
|
||||||
|
*/
|
||||||
|
public isExpired(): boolean {
|
||||||
|
if (!this.data.expiresAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(this.data.expiresAt) < new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user has voted in this poll
|
||||||
|
* @param userId - The user ID to check
|
||||||
|
* @returns True if the user has voted
|
||||||
|
*/
|
||||||
|
public hasUserVoted(userId: string): boolean {
|
||||||
|
return this.data.votes.some((vote) => vote.userId === userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the vote options for a specific user
|
||||||
|
* @param userId - The user ID
|
||||||
|
* @returns Array of option indices the user voted for
|
||||||
|
*/
|
||||||
|
public getUserVotes(userId: string): number[] {
|
||||||
|
const userVotes = this.data.votes.filter((vote) => vote.userId === userId);
|
||||||
|
return userVotes.map((vote) => {
|
||||||
|
const option = this.data.options.find((opt) => opt.id === vote.optionId);
|
||||||
|
return option?.index ?? -1;
|
||||||
|
}).filter((index) => index !== -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert poll to Mastodon API format
|
||||||
|
* @param userFetching - The user fetching the poll (to check if they voted)
|
||||||
|
* @returns Poll in Mastodon API format
|
||||||
|
*/
|
||||||
|
public toApi(userFetching?: { id: string } | null): z.infer<typeof PollSchema> {
|
||||||
|
const voted = userFetching ? this.hasUserVoted(userFetching.id) : undefined;
|
||||||
|
const ownVotes = userFetching ? this.getUserVotes(userFetching.id) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: this.data.id,
|
||||||
|
expires_at: this.data.expiresAt,
|
||||||
|
expired: this.isExpired(),
|
||||||
|
multiple: this.data.multiple,
|
||||||
|
votes_count: this.data.votesCount,
|
||||||
|
voters_count: this.data.votersCount,
|
||||||
|
options: this.data.options.map((option) => ({
|
||||||
|
title: option.title,
|
||||||
|
votes_count: this.data.hideTotals && !this.isExpired() ? null : option.votesCount,
|
||||||
|
})),
|
||||||
|
emojis: [], // TODO: Parse emojis from poll options
|
||||||
|
voted,
|
||||||
|
own_votes: ownVotes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -115,6 +115,16 @@ export const findManyNotes = async (
|
||||||
...userRelations,
|
...userRelations,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
poll: {
|
||||||
|
with: {
|
||||||
|
options: {
|
||||||
|
with: {
|
||||||
|
votes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
votes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extras: {
|
extras: {
|
||||||
pinned: userId
|
pinned: userId
|
||||||
|
|
@ -141,6 +151,16 @@ export const findManyNotes = async (
|
||||||
},
|
},
|
||||||
reply: true,
|
reply: true,
|
||||||
quote: true,
|
quote: true,
|
||||||
|
poll: {
|
||||||
|
with: {
|
||||||
|
options: {
|
||||||
|
with: {
|
||||||
|
votes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
votes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extras: {
|
extras: {
|
||||||
pinned: userId
|
pinned: userId
|
||||||
|
|
@ -176,6 +196,10 @@ export const findManyNotes = async (
|
||||||
})),
|
})),
|
||||||
attachments: post.attachments.map((attachment) => attachment.media),
|
attachments: post.attachments.map((attachment) => attachment.media),
|
||||||
emojis: (post.emojis ?? []).map((emoji) => emoji.emoji),
|
emojis: (post.emojis ?? []).map((emoji) => emoji.emoji),
|
||||||
|
poll: post.poll ? {
|
||||||
|
...post.poll,
|
||||||
|
options: post.poll.options.sort((a, b) => a.index - b.index),
|
||||||
|
} : null,
|
||||||
reblog: post.reblog && {
|
reblog: post.reblog && {
|
||||||
...post.reblog,
|
...post.reblog,
|
||||||
author: transformOutputToUserWithRelations(post.reblog.author),
|
author: transformOutputToUserWithRelations(post.reblog.author),
|
||||||
|
|
@ -187,6 +211,10 @@ export const findManyNotes = async (
|
||||||
(attachment) => attachment.media,
|
(attachment) => attachment.media,
|
||||||
),
|
),
|
||||||
emojis: (post.reblog.emojis ?? []).map((emoji) => emoji.emoji),
|
emojis: (post.reblog.emojis ?? []).map((emoji) => emoji.emoji),
|
||||||
|
poll: post.reblog.poll ? {
|
||||||
|
...post.reblog.poll,
|
||||||
|
options: post.reblog.poll.options.sort((a, b) => a.index - b.index),
|
||||||
|
} : null,
|
||||||
pinned: Boolean(post.reblog.pinned),
|
pinned: Boolean(post.reblog.pinned),
|
||||||
reblogged: Boolean(post.reblog.reblogged),
|
reblogged: Boolean(post.reblog.reblogged),
|
||||||
muted: Boolean(post.reblog.muted),
|
muted: Boolean(post.reblog.muted),
|
||||||
|
|
|
||||||
|
|
@ -512,6 +512,10 @@ export const NotesRelations = relations(Notes, ({ many, one }) => ({
|
||||||
reactions: many(Reactions, {
|
reactions: many(Reactions, {
|
||||||
relationName: "NoteToReactions",
|
relationName: "NoteToReactions",
|
||||||
}),
|
}),
|
||||||
|
poll: one(Polls, {
|
||||||
|
fields: [Notes.id],
|
||||||
|
references: [Polls.noteId],
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const Instances = pgTable("Instances", {
|
export const Instances = pgTable("Instances", {
|
||||||
|
|
@ -947,3 +951,97 @@ export const MediasToNotesRelations = relations(MediasToNotes, ({ one }) => ({
|
||||||
relationName: "AttachmentToNote",
|
relationName: "AttachmentToNote",
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const Polls = pgTable("Polls", {
|
||||||
|
id: id(),
|
||||||
|
noteId: uuid("noteId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => Notes.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
})
|
||||||
|
.unique(),
|
||||||
|
expiresAt: timestamp("expires_at", { precision: 3, mode: "string" }),
|
||||||
|
multiple: boolean("multiple").notNull().default(false),
|
||||||
|
hideTotals: boolean("hide_totals").notNull().default(false),
|
||||||
|
votesCount: integer("votes_count").notNull().default(0),
|
||||||
|
votersCount: integer("voters_count").notNull().default(0),
|
||||||
|
createdAt: createdAt(),
|
||||||
|
updatedAt: updatedAt(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PollOptions = pgTable("PollOptions", {
|
||||||
|
id: id(),
|
||||||
|
pollId: uuid("pollId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => Polls.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
}),
|
||||||
|
title: text("title").notNull(),
|
||||||
|
index: integer("index").notNull(),
|
||||||
|
votesCount: integer("votes_count").notNull().default(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PollVotes = pgTable(
|
||||||
|
"PollVotes",
|
||||||
|
{
|
||||||
|
id: id(),
|
||||||
|
pollId: uuid("pollId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => Polls.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
}),
|
||||||
|
optionId: uuid("optionId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => PollOptions.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
}),
|
||||||
|
userId: uuid("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => Users.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
}),
|
||||||
|
createdAt: createdAt(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex().on(table.pollId, table.userId, table.optionId),
|
||||||
|
index().on(table.pollId),
|
||||||
|
index().on(table.userId),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PollsRelations = relations(Polls, ({ one, many }) => ({
|
||||||
|
note: one(Notes, {
|
||||||
|
fields: [Polls.noteId],
|
||||||
|
references: [Notes.id],
|
||||||
|
}),
|
||||||
|
options: many(PollOptions),
|
||||||
|
votes: many(PollVotes),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const PollOptionsRelations = relations(PollOptions, ({ one, many }) => ({
|
||||||
|
poll: one(Polls, {
|
||||||
|
fields: [PollOptions.pollId],
|
||||||
|
references: [Polls.id],
|
||||||
|
}),
|
||||||
|
votes: many(PollVotes),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const PollVotesRelations = relations(PollVotes, ({ one }) => ({
|
||||||
|
poll: one(Polls, {
|
||||||
|
fields: [PollVotes.pollId],
|
||||||
|
references: [Polls.id],
|
||||||
|
}),
|
||||||
|
option: one(PollOptions, {
|
||||||
|
fields: [PollVotes.optionId],
|
||||||
|
references: [PollOptions.id],
|
||||||
|
}),
|
||||||
|
user: one(Users, {
|
||||||
|
fields: [PollVotes.userId],
|
||||||
|
references: [Users.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export { Like } from "~/classes/database/like.ts";
|
||||||
export { Media } from "~/classes/database/media";
|
export { Media } from "~/classes/database/media";
|
||||||
export { Note } from "~/classes/database/note.ts";
|
export { Note } from "~/classes/database/note.ts";
|
||||||
export { Notification } from "~/classes/database/notification.ts";
|
export { Notification } from "~/classes/database/notification.ts";
|
||||||
|
export { Poll } from "~/classes/database/poll.ts";
|
||||||
export { PushSubscription } from "~/classes/database/pushsubscription.ts";
|
export { PushSubscription } from "~/classes/database/pushsubscription.ts";
|
||||||
export { Reaction } from "~/classes/database/reaction.ts";
|
export { Reaction } from "~/classes/database/reaction.ts";
|
||||||
export { Relationship } from "~/classes/database/relationship.ts";
|
export { Relationship } from "~/classes/database/relationship.ts";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue