🏗️ Update how we store date values

This also fixes a few bugs:
- some values had end times that were before the start times
- some values has end times a couple of days in the future

It’s not entirely clear how users were able to set these values but this update fixes these values and helps avoid similar issues in the future.
This commit is contained in:
Luke Vella 2023-03-30 14:10:23 +01:00
parent 8a9159c322
commit 51c5016656
12 changed files with 203 additions and 158 deletions

View file

@ -4,7 +4,6 @@ import React from "react";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
import { encodeDateOption } from "../utils/date-time-utils";
import { trpc } from "../utils/trpc"; import { trpc } from "../utils/trpc";
import { Button } from "./button"; import { Button } from "./button";
import { import {
@ -102,7 +101,6 @@ const Page: React.FunctionComponent = () => {
await createPoll.mutateAsync({ await createPoll.mutateAsync({
title: title, title: title,
type: "date",
location: formData?.eventDetails?.location, location: formData?.eventDetails?.location,
description: formData?.eventDetails?.description, description: formData?.eventDetails?.description,
user: session.user.isGuest user: session.user.isGuest
@ -112,7 +110,10 @@ const Page: React.FunctionComponent = () => {
} }
: undefined, : undefined,
timeZone: formData?.options?.timeZone, timeZone: formData?.options?.timeZone,
options: required(formData?.options?.options).map(encodeDateOption), options: required(formData?.options?.options).map((option) => ({
startDate: option.type === "date" ? option.date : option.start,
endDate: option.type === "timeSlot" ? option.end : undefined,
})),
}); });
} }
}; };

View file

@ -265,16 +265,11 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
<TimePicker <TimePicker
value={startDate} value={startDate}
onChange={(newStart) => { onChange={(newStart) => {
let newEnd = dayjs(newStart).add( const newEnd = dayjs(newStart).add(
duration, duration,
"minutes", "minutes",
); );
if (!newEnd.isSame(newStart, "day")) {
newEnd = newEnd
.set("hour", 23)
.set("minute", 45);
}
// replace enter with updated start time // replace enter with updated start time
onChange([ onChange([
...options.slice(0, index), ...options.slice(0, index),
@ -293,9 +288,7 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
/> />
<TimePicker <TimePicker
value={new Date(option.end)} value={new Date(option.end)}
startFrom={dayjs(startDate) after={startDate}
.add(15, "minutes")
.toDate()}
onChange={(newEnd) => { onChange={(newEnd) => {
onChange([ onChange([
...options.slice(0, index), ...options.slice(0, index),
@ -330,7 +323,15 @@ const MonthCalendar: React.FunctionComponent<DateTimePickerProps> = ({
const lastOption = expectTimeOption( const lastOption = expectTimeOption(
optionsForDay[optionsForDay.length - 1].option, optionsForDay[optionsForDay.length - 1].option,
); );
const startTime = lastOption.end;
const startTime = dayjs(lastOption.end).isSame(
lastOption.start,
"day",
)
? // if the end time of the previous option is on the same day as the start time, use the end time
lastOption.end
: // otherwise use the start time
lastOption.start;
onChange([ onChange([
...options, ...options,

View file

@ -9,6 +9,7 @@ import { Listbox } from "@headlessui/react";
import clsx from "clsx"; import clsx from "clsx";
import * as React from "react"; import * as React from "react";
import { getDuration } from "@/utils/date-time-utils";
import { stopPropagation } from "@/utils/stop-propagation"; import { stopPropagation } from "@/utils/stop-propagation";
import { useDayjs } from "../../../../utils/dayjs"; import { useDayjs } from "../../../../utils/dayjs";
@ -17,7 +18,7 @@ import { styleMenuItem } from "../../../menu-styles";
export interface TimePickerProps { export interface TimePickerProps {
value: Date; value: Date;
startFrom?: Date; after?: Date;
className?: string; className?: string;
onChange?: (value: Date) => void; onChange?: (value: Date) => void;
} }
@ -26,10 +27,11 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
value, value,
onChange, onChange,
className, className,
startFrom, after,
}) => { }) => {
const { dayjs } = useDayjs(); const { dayjs } = useDayjs();
const { reference, floating, x, y, strategy, refs } = useFloating({ const { reference, floating, x, y, strategy, refs } = useFloating({
placement: "bottom-start",
strategy: "fixed", strategy: "fixed",
middleware: [ middleware: [
offset(5), offset(5),
@ -38,7 +40,7 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
apply: ({ rects }) => { apply: ({ rects }) => {
if (refs.floating.current) { if (refs.floating.current) {
Object.assign(refs.floating.current.style, { Object.assign(refs.floating.current.style, {
width: `${rects.reference.width}px`, minWidth: `${rects.reference.width}px`,
}); });
} }
}, },
@ -47,15 +49,11 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
}); });
const renderOptions = () => { const renderOptions = () => {
const startFromDate = startFrom const startFromDate = after ? dayjs(after) : dayjs(value).startOf("day");
? dayjs(startFrom)
: dayjs(value).startOf("day");
const options: React.ReactNode[] = []; const options: React.ReactNode[] = [];
const startMinute =
startFromDate.get("hour") * 60 + startFromDate.get("minute"); for (let i = 1; i <= 96; i++) {
const intervals = Math.floor((1440 - startMinute) / 15);
for (let i = 0; i < intervals; i++) {
const optionValue = startFromDate.add(i * 15, "minutes"); const optionValue = startFromDate.add(i * 15, "minutes");
options.push( options.push(
<Listbox.Option <Listbox.Option
@ -63,7 +61,14 @@ const TimePicker: React.FunctionComponent<TimePickerProps> = ({
className={styleMenuItem} className={styleMenuItem}
value={optionValue.format("YYYY-MM-DDTHH:mm:ss")} value={optionValue.format("YYYY-MM-DDTHH:mm:ss")}
> >
{optionValue.format("LT")} <div className="flex items-center gap-2">
<span>{optionValue.format("LT")}</span>
{after ? (
<span className="text-sm text-slate-500">
{getDuration(dayjs(after), optionValue)}
</span>
) : null}
</div>
</Listbox.Option>, </Listbox.Option>,
); );
} }

View file

@ -1,4 +1,5 @@
import { Placement } from "@floating-ui/react-dom-interactions"; import { Placement } from "@floating-ui/react-dom-interactions";
import dayjs from "dayjs";
import { Trans, useTranslation } from "next-i18next"; import { Trans, useTranslation } from "next-i18next";
import * as React from "react"; import * as React from "react";
@ -23,6 +24,15 @@ import { useUpdatePollMutation } from "./mutations";
const PollOptionsForm = React.lazy(() => import("../forms/poll-options-form")); const PollOptionsForm = React.lazy(() => import("../forms/poll-options-form"));
const convertOptionToString = (option: { start: Date; duration: number }) => {
const start = dayjs(option.start);
return option.duration === 0
? start.format("YYYY-MM-DD")
: `${start.format("YYYY-MM-DDTHH:mm:ss")}/${start
.add(option.duration, "minute")
.format("YYYY-MM-DDTHH:mm:ss")}`;
};
const ManagePoll: React.FunctionComponent<{ const ManagePoll: React.FunctionComponent<{
placement?: Placement; placement?: Placement;
}> = ({ placement }) => { }> = ({ placement }) => {
@ -67,18 +77,20 @@ const ManagePoll: React.FunctionComponent<{
name="pollOptions" name="pollOptions"
title={poll.title} title={poll.title}
defaultValues={{ defaultValues={{
navigationDate: poll.options[0].value.split("/")[0], navigationDate: poll.options[0].start.toString(),
options: poll.options.map((option) => { options: poll.options.map((option) => {
const [start, end] = option.value.split("/"); const start = dayjs(option.start);
return end return option.duration > 0
? { ? {
type: "timeSlot", type: "timeSlot",
start, start: start.format("YYYY-MM-DDTHH:mm:ss"),
end, end: start
.add(option.duration, "minute")
.format("YYYY-MM-DDTHH:mm:ss"),
} }
: { : {
type: "date", type: "date",
date: start, date: start.format("YYYY-MM-DD"),
}; };
}), }),
timeZone: poll.timeZone ?? "", timeZone: poll.timeZone ?? "",
@ -86,12 +98,14 @@ const ManagePoll: React.FunctionComponent<{
onSubmit={(data) => { onSubmit={(data) => {
const encodedOptions = data.options.map(encodeDateOption); const encodedOptions = data.options.map(encodeDateOption);
const optionsToDelete = poll.options.filter((option) => { const optionsToDelete = poll.options.filter((option) => {
return !encodedOptions.includes(option.value); return !encodedOptions.includes(convertOptionToString(option));
}); });
const optionsToAdd = encodedOptions.filter( const optionsToAdd = encodedOptions.filter(
(encodedOption) => (encodedOption) =>
!poll.options.find((o) => o.value === encodedOption), !poll.options.find(
(o) => convertOptionToString(o) === encodedOption,
),
); );
const onOk = () => { const onOk = () => {

View file

@ -2,8 +2,6 @@ import { Prisma, prisma } from "@rallly/database";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { parseValue } from "../../utils/date-time-utils";
/** /**
* DANGER: This endpoint will permanently delete polls. * DANGER: This endpoint will permanently delete polls.
*/ */
@ -25,32 +23,26 @@ export default async function handler(
} }
// get polls that have not been accessed for over 30 days // get polls that have not been accessed for over 30 days
const inactivePolls = await prisma.$queryRaw< const thirtyDaysAgo = dayjs().subtract(30, "days").toDate();
Array<{ id: string; max: string }>
>`
SELECT polls.id, MAX(options.value) FROM polls
JOIN options ON options.poll_id = polls.id
WHERE touched_at <= ${dayjs().add(-30, "days").toDate()} AND deleted = false
GROUP BY polls.id;
`;
const pollsToSoftDelete: string[] = []; const oldPollsWithAllPastOptions = await prisma.poll.findMany({
select: {
// keep polls that have options that are in the future id: true,
inactivePolls.forEach(({ id, max: value }) => { },
const parsedValue = parseValue(value); where: {
const date = touchedAt: { lte: thirtyDaysAgo },
parsedValue.type === "date" ? parsedValue.date : parsedValue.end; options: {
every: {
if (dayjs(date).isBefore(dayjs())) { start: { lt: new Date() },
pollsToSoftDelete.push(id); },
} },
},
}); });
const softDeletedPolls = await prisma.poll.deleteMany({ const softDeletedPolls = await prisma.poll.deleteMany({
where: { where: {
id: { id: {
in: pollsToSoftDelete, in: oldPollsWithAllPastOptions.map(({ id }) => id),
}, },
}, },
}); });

View file

@ -1,6 +1,7 @@
import { prisma } from "@rallly/database"; import { prisma } from "@rallly/database";
import { sendEmail } from "@rallly/emails"; import { sendEmail } from "@rallly/emails";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import dayjs from "dayjs";
import { z } from "zod"; import { z } from "zod";
import { absoluteUrl } from "../../utils/absolute-url"; import { absoluteUrl } from "../../utils/absolute-url";
@ -10,46 +11,6 @@ import { comments } from "./polls/comments";
import { demo } from "./polls/demo"; import { demo } from "./polls/demo";
import { participants } from "./polls/participants"; import { participants } from "./polls/participants";
const defaultSelectFields: {
id: true;
timeZone: true;
title: true;
location: true;
description: true;
createdAt: true;
adminUrlId: true;
participantUrlId: true;
closed: true;
legacy: true;
demo: true;
options: {
orderBy: {
value: "asc";
};
};
user: true;
deleted: true;
} = {
id: true,
timeZone: true,
title: true,
location: true,
description: true,
createdAt: true,
adminUrlId: true,
participantUrlId: true,
closed: true,
legacy: true,
demo: true,
options: {
orderBy: {
value: "asc",
},
},
user: true,
deleted: true,
};
const getPollIdFromAdminUrlId = async (urlId: string) => { const getPollIdFromAdminUrlId = async (urlId: string) => {
const res = await prisma.poll.findUnique({ const res = await prisma.poll.findUnique({
select: { select: {
@ -72,7 +33,6 @@ export const polls = router({
.input( .input(
z.object({ z.object({
title: z.string(), title: z.string(),
type: z.literal("date"),
timeZone: z.string().optional(), timeZone: z.string().optional(),
location: z.string().optional(), location: z.string().optional(),
description: z.string().optional(), description: z.string().optional(),
@ -82,7 +42,12 @@ export const polls = router({
email: z.string(), email: z.string(),
}) })
.optional(), .optional(),
options: z.string().array(), options: z
.object({
startDate: z.string(),
endDate: z.string().optional(),
})
.array(),
demo: z.boolean().optional(), demo: z.boolean().optional(),
}), }),
) )
@ -120,7 +85,6 @@ export const polls = router({
data: { data: {
id: await nanoid(), id: await nanoid(),
title: input.title, title: input.title,
type: input.type,
timeZone: input.timeZone, timeZone: input.timeZone,
location: input.location, location: input.location,
description: input.description, description: input.description,
@ -137,8 +101,14 @@ export const polls = router({
: undefined, : undefined,
options: { options: {
createMany: { createMany: {
data: input.options.map((value) => ({ data: input.options.map((option) => ({
value, start: new Date(option.startDate),
duration: option.endDate
? dayjs(option.endDate).diff(
dayjs(option.startDate),
"minute",
)
: 0,
})), })),
}, },
}, },
@ -193,15 +163,26 @@ export const polls = router({
if (input.optionsToAdd && input.optionsToAdd.length > 0) { if (input.optionsToAdd && input.optionsToAdd.length > 0) {
await prisma.option.createMany({ await prisma.option.createMany({
data: input.optionsToAdd.map((optionValue) => ({ data: input.optionsToAdd.map((optionValue) => {
value: optionValue, const [start, end] = optionValue.split("/");
pollId, if (end) {
})), return {
start: new Date(start),
duration: dayjs(end).diff(dayjs(start), "minute"),
pollId,
};
} else {
return {
start: new Date(start.substring(0, 10) + "T00:00:00"),
pollId,
};
}
}),
}); });
} }
await prisma.poll.update({ await prisma.poll.update({
select: defaultSelectFields, select: { id: true },
where: { where: {
id: pollId, id: pollId,
}, },
@ -303,7 +284,24 @@ export const polls = router({
.query(async ({ input }) => { .query(async ({ input }) => {
const res = await prisma.poll.findUnique({ const res = await prisma.poll.findUnique({
select: { select: {
...defaultSelectFields, id: true,
timeZone: true,
title: true,
location: true,
description: true,
createdAt: true,
adminUrlId: true,
participantUrlId: true,
closed: true,
legacy: true,
demo: true,
options: {
orderBy: {
start: "asc",
},
},
user: true,
deleted: true,
watchers: { watchers: {
select: { select: {
userId: true, userId: true,
@ -333,7 +331,32 @@ export const polls = router({
) )
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const res = await prisma.poll.findUnique({ const res = await prisma.poll.findUnique({
select: { userId: true, ...defaultSelectFields }, select: {
id: true,
timeZone: true,
title: true,
location: true,
description: true,
createdAt: true,
adminUrlId: true,
participantUrlId: true,
closed: true,
legacy: true,
demo: true,
options: {
orderBy: {
start: "asc",
},
},
user: true,
userId: true,
deleted: true,
watchers: {
select: {
userId: true,
},
},
},
where: { where: {
participantUrlId: input.urlId, participantUrlId: input.urlId,
}, },

View file

@ -30,10 +30,10 @@ export const demo = router({
const adminUrlId = await nanoid(); const adminUrlId = await nanoid();
const demoUser = { name: "John Example", email: "noreply@rallly.co" }; const demoUser = { name: "John Example", email: "noreply@rallly.co" };
const options: Array<{ value: string; id: string }> = []; const options: Array<{ start: Date; id: string }> = [];
for (let i = 0; i < optionValues.length; i++) { for (let i = 0; i < optionValues.length; i++) {
options.push({ id: await nanoid(), value: optionValues[i] }); options.push({ id: await nanoid(), start: new Date(optionValues[i]) });
} }
const participants: Array<{ const participants: Array<{
@ -74,7 +74,6 @@ export const demo = router({
data: { data: {
id: await nanoid(), id: await nanoid(),
title: "Lunch Meeting", title: "Lunch Meeting",
type: "date",
location: "Starbucks, 901 New York Avenue", location: "Starbucks, 901 New York Avenue",
description: `Hey everyone, please choose the dates when you are available to meet for our monthly get together. Looking forward to see you all!`, description: `Hey everyone, please choose the dates when you are available to meet for our monthly get together. Looking forward to see you all!`,
demo: true, demo: true,

View file

@ -65,7 +65,9 @@ export const decodeOptions = (
): ):
| { pollType: "date"; options: ParsedDateOption[] } | { pollType: "date"; options: ParsedDateOption[] }
| { pollType: "timeSlot"; options: ParsedTimeSlotOption[] } => { | { pollType: "timeSlot"; options: ParsedTimeSlotOption[] } => {
const pollType = isTimeSlot(options[0].value) ? "timeSlot" : "date"; const pollType = options.some(({ duration }) => duration > 0)
? "timeSlot"
: "date";
if (pollType === "timeSlot") { if (pollType === "timeSlot") {
return { return {
@ -83,12 +85,7 @@ export const decodeOptions = (
}; };
const parseDateOption = (option: Option): ParsedDateOption => { const parseDateOption = (option: Option): ParsedDateOption => {
const dateString = const date = dayjs(option.start);
option.value.indexOf("T") === -1
? // we add the time because otherwise Date will assume UTC time which might change the day for some time zones
option.value + "T00:00:00"
: option.value;
const date = dayjs(dateString);
return { return {
type: "date", type: "date",
optionId: option.id, optionId: option.id,
@ -104,16 +101,12 @@ const parseTimeSlotOption = (
timeZone: string | null, timeZone: string | null,
targetTimeZone: string, targetTimeZone: string,
): ParsedTimeSlotOption => { ): ParsedTimeSlotOption => {
const [start, end] = option.value.split("/");
const startDate = const startDate =
timeZone && targetTimeZone timeZone && targetTimeZone
? dayjs(start).tz(timeZone, true).tz(targetTimeZone) ? dayjs(option.start).tz(timeZone, true).tz(targetTimeZone)
: dayjs(start); : dayjs(option.start);
const endDate =
timeZone && targetTimeZone const endDate = startDate.add(option.duration, "minute");
? dayjs(end).tz(timeZone, true).tz(targetTimeZone)
: dayjs(end);
return { return {
type: "timeSlot", type: "timeSlot",

View file

@ -176,7 +176,6 @@ const seedData = async () => {
{ {
title: "Active Poll", title: "Active Poll",
id: "active-poll", id: "active-poll",
type: "date",
userId: "user1", userId: "user1",
participantUrlId: "p1", participantUrlId: "p1",
adminUrlId: "a1", adminUrlId: "a1",
@ -185,7 +184,6 @@ const seedData = async () => {
{ {
title: "Deleted poll", title: "Deleted poll",
id: "deleted-poll-6d", id: "deleted-poll-6d",
type: "date",
userId: "user1", userId: "user1",
deleted: true, deleted: true,
deletedAt: dayjs().add(-6, "days").toDate(), deletedAt: dayjs().add(-6, "days").toDate(),
@ -196,7 +194,6 @@ const seedData = async () => {
{ {
title: "Deleted poll 7d", title: "Deleted poll 7d",
id: "deleted-poll-7d", id: "deleted-poll-7d",
type: "date",
userId: "user1", userId: "user1",
deleted: true, deleted: true,
deletedAt: dayjs().add(-7, "days").toDate(), deletedAt: dayjs().add(-7, "days").toDate(),
@ -207,7 +204,6 @@ const seedData = async () => {
{ {
title: "Still active", title: "Still active",
id: "still-active-poll", id: "still-active-poll",
type: "date",
userId: "user1", userId: "user1",
touchedAt: dayjs().add(-29, "days").toDate(), touchedAt: dayjs().add(-29, "days").toDate(),
participantUrlId: "p4", participantUrlId: "p4",
@ -217,7 +213,6 @@ const seedData = async () => {
{ {
title: "Inactive poll", title: "Inactive poll",
id: "inactive-poll", id: "inactive-poll",
type: "date",
userId: "user1", userId: "user1",
touchedAt: dayjs().add(-30, "days").toDate(), touchedAt: dayjs().add(-30, "days").toDate(),
participantUrlId: "p5", participantUrlId: "p5",
@ -228,7 +223,6 @@ const seedData = async () => {
demo: true, demo: true,
title: "Demo poll", title: "Demo poll",
id: "demo-poll-new", id: "demo-poll-new",
type: "date",
userId: "user1", userId: "user1",
createdAt: new Date(), createdAt: new Date(),
participantUrlId: "p6", participantUrlId: "p6",
@ -238,7 +232,6 @@ const seedData = async () => {
demo: true, demo: true,
title: "Old demo poll", title: "Old demo poll",
id: "demo-poll-old", id: "demo-poll-old",
type: "date",
userId: "user1", userId: "user1",
createdAt: dayjs().add(-2, "days").toDate(), createdAt: dayjs().add(-2, "days").toDate(),
participantUrlId: "p7", participantUrlId: "p7",
@ -247,7 +240,6 @@ const seedData = async () => {
{ {
title: "Inactive poll with future option", title: "Inactive poll with future option",
id: "inactive-poll-future-option", id: "inactive-poll-future-option",
type: "date",
userId: "user1", userId: "user1",
touchedAt: dayjs().add(-30, "days").toDate(), touchedAt: dayjs().add(-30, "days").toDate(),
participantUrlId: "p8", participantUrlId: "p8",
@ -260,32 +252,27 @@ const seedData = async () => {
data: [ data: [
{ {
id: "option-1", id: "option-1",
value: "2022-02-22", start: new Date("2022-02-22T00:00:00"),
pollId: "deleted-poll-7d", pollId: "deleted-poll-7d",
}, },
{ {
id: "option-2", id: "option-2",
value: "2022-02-23", start: new Date("2022-02-23T00:00:00"),
pollId: "deleted-poll-7d", pollId: "deleted-poll-7d",
}, },
{ {
id: "option-3", id: "option-3",
value: "2022-02-24", start: new Date("2022-02-24T00:00:00"),
pollId: "deleted-poll-7d", pollId: "deleted-poll-7d",
}, },
{ {
id: "option-4", id: "option-4",
value: `${dayjs() start: dayjs().add(10, "days").toDate(),
.add(10, "days")
.format("YYYY-MM-DDTHH:mm:ss")}/${dayjs()
.add(10, "days")
.add(1, "hour")
.format("YYYY-MM-DDTHH:mm:ss")}`,
pollId: "inactive-poll-future-option", pollId: "inactive-poll-future-option",
}, },
{ {
id: "option-5", id: "option-5",
value: dayjs().add(-1, "days").format("YYYY-MM-DD"), start: dayjs().add(-1, "days").toDate(),
pollId: "inactive-poll", pollId: "inactive-poll",
}, },
], ],

View file

@ -0,0 +1,35 @@
-- AlterTable
ALTER TABLE "options"
ADD COLUMN "duration_minutes" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "start" TIMESTAMP(0);
-- AlterTable
ALTER TABLE "polls" DROP COLUMN "type";
-- DropEnum
DROP TYPE "poll_type";
-- Reformat option value into new columns
UPDATE "options"
SET "start" = CASE
WHEN POSITION('/' IN "value") = 0 THEN (value || 'T00:00:00')::TIMESTAMP WITHOUT TIME ZONE
ELSE SPLIT_PART("value", '/', 1)::TIMESTAMP WITHOUT TIME ZONE
END,
"duration_minutes" = CASE
WHEN POSITION('/' IN "value") = 0 THEN 0
ELSE
LEAST(EXTRACT(EPOCH FROM (split_part("value", '/', 2)::timestamp - split_part("value", '/', 1)::timestamp)) / 60, 1440)
END;
-- Fix cases where we have a negative duration due to the end time being in the past
-- eg. Some polls have value 2023-03-29T23:00:00/2023-03-29T01:00:00
UPDATE "options"
SET "duration_minutes" = "duration_minutes" + 1440
WHERE "duration_minutes" < 0;
-- Set start date to be not null now that we have all the data and drop the old value column
ALTER TABLE "options"
DROP COLUMN "value",
DROP COLUMN "updated_at",
ALTER COLUMN "start" SET NOT NULL;

View file

@ -22,19 +22,12 @@ model User {
@@map("users") @@map("users")
} }
enum PollType {
date
@@map("poll_type")
}
model Poll { model Poll {
id String @id @unique @map("id") id String @id @unique @map("id")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
deadline DateTime? deadline DateTime?
title String title String
type PollType
description String? description String?
location String? location String?
user User? @relation(fields: [userId], references: [id]) user User? @relation(fields: [userId], references: [id])
@ -88,11 +81,11 @@ model Participant {
model Option { model Option {
id String @id @default(cuid()) id String @id @default(cuid())
value String start DateTime @db.Timestamp(0)
duration Int @default(0) @map("duration_minutes")
pollId String @map("poll_id") pollId String @map("poll_id")
poll Poll @relation(fields: [pollId], references: [id]) poll Poll @relation(fields: [pollId], references: [id])
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt @map("updated_at")
@@index([pollId], type: Hash) @@index([pollId], type: Hash)
@@map("options") @@map("options")

View file

@ -1,5 +1,5 @@
import { faker } from "@faker-js/faker"; import { faker } from "@faker-js/faker";
import { PrismaClient, VoteType } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@ -11,14 +11,16 @@ async function main() {
// Create some users // Create some users
const user = await prisma.user.create({ const user = await prisma.user.create({
data: { data: {
name: faker.name.fullName(), name: "Dev User",
email: faker.internet.email(), email: "dev@rallly.co",
}, },
}); });
// Create some polls // Create some polls
const polls = await Promise.all( const polls = await Promise.all(
Array.from({ length: 20 }).map(async () => { Array.from({ length: 20 }).map(async (_, i) => {
// create some polls with no duration (all day) and some with a random duration.
const duration = i % 5 === 0 ? 15 * randInt(8) : 0;
const poll = await prisma.poll.create({ const poll = await prisma.poll.create({
include: { include: {
participants: true, participants: true,
@ -30,7 +32,6 @@ async function main() {
description: faker.lorem.paragraph(), description: faker.lorem.paragraph(),
location: faker.address.streetAddress(), location: faker.address.streetAddress(),
deadline: faker.date.future(), deadline: faker.date.future(),
type: "date",
user: { user: {
connect: { connect: {
id: user.id, id: user.id,
@ -46,7 +47,8 @@ async function main() {
) // ) //
.map((date) => { .map((date) => {
return { return {
value: date.toISOString().substring(0, 10), start: date,
duration,
}; };
}), }),
}, },
@ -73,7 +75,7 @@ async function main() {
data: poll.options.map((option) => { data: poll.options.map((option) => {
const randomNumber = randInt(100); const randomNumber = randInt(100);
const vote = const vote =
randomNumber > 90 ? "ifNeedBe" : randomNumber > 50 ? "yes" : "no"; randomNumber > 95 ? "ifNeedBe" : randomNumber > 50 ? "yes" : "no";
return { return {
participantId: participant.id, participantId: participant.id,
pollId: poll.id, pollId: poll.id,