mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-18 16:57:22 +02:00
✨ Add scheduled events schema (#1679)
This commit is contained in:
parent
22f32f9314
commit
56bd684c55
35 changed files with 1412 additions and 659 deletions
|
@ -0,0 +1,87 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "scheduled_event_status" AS ENUM ('confirmed', 'canceled', 'unconfirmed');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "scheduled_event_invite_status" AS ENUM ('pending', 'accepted', 'declined', 'tentative');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "scheduled_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"location" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"status" "scheduled_event_status" NOT NULL DEFAULT 'confirmed',
|
||||
"time_zone" TEXT,
|
||||
"start" TIMESTAMP(3) NOT NULL,
|
||||
"end" TIMESTAMP(3) NOT NULL,
|
||||
"all_day" BOOLEAN NOT NULL DEFAULT false,
|
||||
"deleted_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "scheduled_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "rescheduled_event_dates" (
|
||||
"id" TEXT NOT NULL,
|
||||
"scheduled_event_id" TEXT NOT NULL,
|
||||
"start" TIMESTAMP(3) NOT NULL,
|
||||
"end" TIMESTAMP(3) NOT NULL,
|
||||
"all_day" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "rescheduled_event_dates_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "scheduled_event_invites" (
|
||||
"id" TEXT NOT NULL,
|
||||
"scheduled_event_id" TEXT NOT NULL,
|
||||
"invitee_name" TEXT NOT NULL,
|
||||
"invitee_email" TEXT NOT NULL,
|
||||
"invitee_id" TEXT,
|
||||
"invitee_time_zone" TEXT,
|
||||
"status" "scheduled_event_invite_status" NOT NULL DEFAULT 'pending',
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "scheduled_event_invites_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "rescheduled_event_dates_scheduled_event_id_idx" ON "rescheduled_event_dates"("scheduled_event_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "scheduled_event_invites_scheduled_event_id_idx" ON "scheduled_event_invites"("scheduled_event_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "scheduled_event_invites_invitee_id_idx" ON "scheduled_event_invites"("invitee_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "scheduled_event_invites_invitee_email_idx" ON "scheduled_event_invites"("invitee_email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "scheduled_event_invites_scheduled_event_id_invitee_email_key" ON "scheduled_event_invites"("scheduled_event_id", "invitee_email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "scheduled_event_invites_scheduled_event_id_invitee_id_key" ON "scheduled_event_invites"("scheduled_event_id", "invitee_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scheduled_events" ADD CONSTRAINT "scheduled_events_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rescheduled_event_dates" ADD CONSTRAINT "rescheduled_event_dates_scheduled_event_id_fkey" FOREIGN KEY ("scheduled_event_id") REFERENCES "scheduled_events"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scheduled_event_invites" ADD CONSTRAINT "scheduled_event_invites_scheduled_event_id_fkey" FOREIGN KEY ("scheduled_event_id") REFERENCES "scheduled_events"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scheduled_event_invites" ADD CONSTRAINT "scheduled_event_invites_invitee_id_fkey" FOREIGN KEY ("invitee_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "polls" ADD COLUMN "scheduled_event_id" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "polls" ADD CONSTRAINT "polls_scheduled_event_id_fkey" FOREIGN KEY ("scheduled_event_id") REFERENCES "scheduled_events"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@ -0,0 +1,52 @@
|
|||
-- Step 1: Insert data from Event into ScheduledEvent
|
||||
-- Reuse Event ID for ScheduledEvent ID
|
||||
-- Calculate 'end': For all-day (duration 0), end is start + 1 day. Otherwise, calculate based on duration.
|
||||
-- Set 'all_day' based on 'duration_minutes'
|
||||
-- Fetch 'location' and 'time_zone' from the related Poll using event_id
|
||||
-- Set defaults for other fields
|
||||
INSERT INTO "scheduled_events" (
|
||||
"id",
|
||||
"user_id",
|
||||
"title",
|
||||
"description",
|
||||
"location",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"status",
|
||||
"time_zone",
|
||||
"start",
|
||||
"end",
|
||||
"all_day",
|
||||
"deleted_at"
|
||||
)
|
||||
SELECT
|
||||
e."id", -- Reuse Event ID
|
||||
e."user_id",
|
||||
e."title",
|
||||
NULL, -- Default description
|
||||
p."location", -- Get location from the related Poll
|
||||
e."created_at",
|
||||
NOW(), -- Set updated_at to current time
|
||||
'confirmed'::"scheduled_event_status", -- Default status 'confirmed'
|
||||
p."time_zone", -- Get timeZone from the related Poll
|
||||
e."start",
|
||||
-- Calculate 'end': If duration is 0 (all-day), set end to start + 1 day. Otherwise, calculate based on duration.
|
||||
CASE
|
||||
WHEN e."duration_minutes" = 0 THEN e."start" + interval '1 day'
|
||||
ELSE e."start" + (e."duration_minutes" * interval '1 minute')
|
||||
END,
|
||||
-- Set 'all_day': TRUE if duration is 0, FALSE otherwise
|
||||
CASE
|
||||
WHEN e."duration_minutes" = 0 THEN TRUE
|
||||
ELSE FALSE
|
||||
END,
|
||||
NULL -- Default deletedAt to NULL
|
||||
FROM
|
||||
"events" e
|
||||
LEFT JOIN "polls" p ON e."id" = p."event_id";
|
||||
-- Step 2: Update the polls table to link to the new scheduled_event_id
|
||||
-- Set scheduled_event_id = event_id where event_id was previously set
|
||||
-- Only update if the corresponding ScheduledEvent was successfully created in Step 1
|
||||
UPDATE "polls" p
|
||||
SET "scheduled_event_id" = p."event_id"
|
||||
WHERE p."event_id" IS NOT NULL;
|
|
@ -0,0 +1,88 @@
|
|||
-- migrate_event_votes_to_invites.sql V7
|
||||
-- Migrate participants with emails from polls linked to events with a selected winning option (event.optionId)
|
||||
-- into scheduled_event_invites for the corresponding scheduled_event (poll.scheduled_event_id).
|
||||
-- Map the participant's vote on the winning option to the invite status.
|
||||
-- Uses CTE with ROW_NUMBER() to handle potential duplicates based on email *and* user_id per scheduled event, preferring the most recent participant.
|
||||
-- Uses NOT EXISTS in WHERE clause to avoid inserting invites if they already exist from other sources.
|
||||
-- Reuses the participant's unique ID (pt.id) as the invite ID for this migration.
|
||||
-- Excludes participants with NULL or empty string emails.
|
||||
|
||||
WITH PotentialInvites AS (
|
||||
SELECT
|
||||
pt.id as participant_id, -- Keep original participant ID for reuse
|
||||
p.scheduled_event_id,
|
||||
pt.name as invitee_name,
|
||||
pt.email as invitee_email,
|
||||
pt.user_id as invitee_id,
|
||||
u.time_zone as invitee_time_zone,
|
||||
v.type as vote_type,
|
||||
pt.created_at as participant_created_at,
|
||||
-- Assign row number partitioned by event and email, preferring most recent participant
|
||||
ROW_NUMBER() OVER(PARTITION BY p.scheduled_event_id, pt.email ORDER BY pt.created_at DESC) as rn_email,
|
||||
-- Assign row number partitioned by event and user_id (if not null), preferring most recent participant
|
||||
CASE
|
||||
WHEN pt.user_id IS NOT NULL THEN ROW_NUMBER() OVER(PARTITION BY p.scheduled_event_id, pt.user_id ORDER BY pt.created_at DESC)
|
||||
ELSE NULL
|
||||
END as rn_user
|
||||
FROM
|
||||
events e
|
||||
JOIN
|
||||
polls p ON e.id = p.event_id
|
||||
JOIN
|
||||
participants pt ON p.id = pt.poll_id
|
||||
LEFT JOIN
|
||||
votes v ON pt.id = v.participant_id AND e.option_id = v.option_id
|
||||
LEFT JOIN
|
||||
users u ON pt.user_id = u.id
|
||||
WHERE
|
||||
e.option_id IS NOT NULL
|
||||
AND p.scheduled_event_id IS NOT NULL
|
||||
AND pt.email IS NOT NULL
|
||||
AND pt.email != ''
|
||||
AND pt.deleted = false
|
||||
)
|
||||
INSERT INTO scheduled_event_invites (
|
||||
id,
|
||||
scheduled_event_id,
|
||||
invitee_name,
|
||||
invitee_email,
|
||||
invitee_id,
|
||||
invitee_time_zone,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
pi.participant_id as id, -- Reuse participant's unique CUID as invite ID
|
||||
pi.scheduled_event_id,
|
||||
pi.invitee_name,
|
||||
pi.invitee_email,
|
||||
pi.invitee_id,
|
||||
pi.invitee_time_zone,
|
||||
CASE pi.vote_type
|
||||
WHEN 'yes' THEN 'accepted'::scheduled_event_invite_status
|
||||
WHEN 'ifNeedBe' THEN 'tentative'::scheduled_event_invite_status
|
||||
WHEN 'no' THEN 'declined'::scheduled_event_invite_status
|
||||
ELSE 'pending'::scheduled_event_invite_status
|
||||
END as status,
|
||||
NOW() as created_at,
|
||||
NOW() as updated_at
|
||||
FROM
|
||||
PotentialInvites pi
|
||||
WHERE
|
||||
pi.rn_email = 1 -- Only take the first row for each email/event combo
|
||||
AND (pi.invitee_id IS NULL OR pi.rn_user = 1) -- Only take the first row for each user_id/event combo (if user_id exists)
|
||||
-- Check for existing invite by email for the same scheduled event
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM scheduled_event_invites sei
|
||||
WHERE sei.scheduled_event_id = pi.scheduled_event_id
|
||||
AND sei.invitee_email = pi.invitee_email
|
||||
)
|
||||
-- Check for existing invite by user ID for the same scheduled event (only if participant has a user ID)
|
||||
AND (pi.invitee_id IS NULL OR NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM scheduled_event_invites sei
|
||||
WHERE sei.scheduled_event_id = pi.scheduled_event_id
|
||||
AND sei.invitee_id = pi.invitee_id
|
||||
));
|
|
@ -5,8 +5,8 @@ datasource db {
|
|||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native"]
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native"]
|
||||
previewFeatures = ["relationJoins"]
|
||||
}
|
||||
|
||||
|
@ -38,31 +38,33 @@ model Account {
|
|||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
email String @unique() @db.Citext
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
image String?
|
||||
timeZone String? @map("time_zone")
|
||||
weekStart Int? @map("week_start")
|
||||
timeFormat TimeFormat? @map("time_format")
|
||||
locale String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime? @updatedAt @map("updated_at")
|
||||
customerId String? @map("customer_id")
|
||||
banned Boolean @default(false)
|
||||
bannedAt DateTime? @map("banned_at")
|
||||
banReason String? @map("ban_reason")
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
email String @unique() @db.Citext
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
image String?
|
||||
timeZone String? @map("time_zone")
|
||||
weekStart Int? @map("week_start")
|
||||
timeFormat TimeFormat? @map("time_format")
|
||||
locale String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime? @updatedAt @map("updated_at")
|
||||
customerId String? @map("customer_id")
|
||||
banned Boolean @default(false)
|
||||
bannedAt DateTime? @map("banned_at")
|
||||
banReason String? @map("ban_reason")
|
||||
|
||||
comments Comment[]
|
||||
polls Poll[]
|
||||
watcher Watcher[]
|
||||
events Event[]
|
||||
accounts Account[]
|
||||
participants Participant[]
|
||||
paymentMethods PaymentMethod[]
|
||||
subscription Subscription? @relation("UserToSubscription")
|
||||
pollViews PollView[]
|
||||
comments Comment[]
|
||||
polls Poll[]
|
||||
watcher Watcher[]
|
||||
events Event[]
|
||||
accounts Account[]
|
||||
participants Participant[]
|
||||
paymentMethods PaymentMethod[]
|
||||
subscription Subscription? @relation("UserToSubscription")
|
||||
pollViews PollView[]
|
||||
scheduledEvents ScheduledEvent[]
|
||||
scheduledEventInvites ScheduledEventInvite[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
@ -155,19 +157,21 @@ model Poll {
|
|||
participantUrlId String @unique @map("participant_url_id")
|
||||
adminUrlId String @unique @map("admin_url_id")
|
||||
eventId String? @unique @map("event_id")
|
||||
scheduledEventId String? @map("scheduled_event_id")
|
||||
hideParticipants Boolean @default(false) @map("hide_participants")
|
||||
hideScores Boolean @default(false) @map("hide_scores")
|
||||
disableComments Boolean @default(false) @map("disable_comments")
|
||||
requireParticipantEmail Boolean @default(false) @map("require_participant_email")
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
event Event? @relation(fields: [eventId], references: [id], onDelete: SetNull)
|
||||
options Option[]
|
||||
participants Participant[]
|
||||
watchers Watcher[]
|
||||
comments Comment[]
|
||||
votes Vote[]
|
||||
views PollView[]
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
event Event? @relation(fields: [eventId], references: [id], onDelete: SetNull)
|
||||
scheduledEvent ScheduledEvent? @relation(fields: [scheduledEventId], references: [id], onDelete: SetNull)
|
||||
options Option[]
|
||||
participants Participant[]
|
||||
watchers Watcher[]
|
||||
comments Comment[]
|
||||
votes Vote[]
|
||||
views PollView[]
|
||||
|
||||
@@index([guestId])
|
||||
@@map("polls")
|
||||
|
@ -293,7 +297,7 @@ model PollView {
|
|||
userAgent String? @map("user_agent")
|
||||
viewedAt DateTime @default(now()) @map("viewed_at")
|
||||
|
||||
poll Poll @relation(fields: [pollId], references: [id], onDelete: Cascade)
|
||||
poll Poll @relation(fields: [pollId], references: [id], onDelete: Cascade)
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([pollId], type: Hash)
|
||||
|
@ -309,4 +313,80 @@ model VerificationToken {
|
|||
|
||||
@@unique([identifier, token])
|
||||
@@map("verification_tokens")
|
||||
}
|
||||
}
|
||||
|
||||
enum ScheduledEventStatus {
|
||||
confirmed
|
||||
canceled
|
||||
unconfirmed
|
||||
|
||||
@@map("scheduled_event_status")
|
||||
}
|
||||
|
||||
enum ScheduledEventInviteStatus {
|
||||
pending
|
||||
accepted
|
||||
declined
|
||||
tentative
|
||||
|
||||
@@map("scheduled_event_invite_status")
|
||||
}
|
||||
|
||||
model ScheduledEvent {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
title String
|
||||
description String?
|
||||
location String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
status ScheduledEventStatus @default(confirmed)
|
||||
timeZone String? @map("time_zone")
|
||||
start DateTime
|
||||
end DateTime
|
||||
allDay Boolean @default(false) @map("all_day")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
rescheduledDates RescheduledEventDate[]
|
||||
invites ScheduledEventInvite[]
|
||||
polls Poll[]
|
||||
|
||||
@@map("scheduled_events")
|
||||
}
|
||||
|
||||
model RescheduledEventDate {
|
||||
id String @id @default(cuid())
|
||||
scheduledEventId String @map("scheduled_event_id")
|
||||
start DateTime @map("start")
|
||||
end DateTime @map("end")
|
||||
allDay Boolean @default(false) @map("all_day")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
scheduledEvent ScheduledEvent @relation(fields: [scheduledEventId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([scheduledEventId])
|
||||
@@map("rescheduled_event_dates")
|
||||
}
|
||||
|
||||
model ScheduledEventInvite {
|
||||
id String @id @default(cuid())
|
||||
scheduledEventId String @map("scheduled_event_id")
|
||||
inviteeName String @map("invitee_name")
|
||||
inviteeEmail String @map("invitee_email")
|
||||
inviteeId String? @map("invitee_id")
|
||||
inviteeTimeZone String? @map("invitee_time_zone")
|
||||
status ScheduledEventInviteStatus @default(pending)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
scheduledEvent ScheduledEvent @relation(fields: [scheduledEventId], references: [id], onDelete: Cascade)
|
||||
user User? @relation(fields: [inviteeId], references: [id], onDelete: SetNull) // Optional relation to User model
|
||||
|
||||
@@unique([scheduledEventId, inviteeEmail])
|
||||
@@unique([scheduledEventId, inviteeId])
|
||||
@@index([scheduledEventId])
|
||||
@@index([inviteeId])
|
||||
@@index([inviteeEmail])
|
||||
@@map("scheduled_event_invites")
|
||||
}
|
||||
|
|
|
@ -1,156 +1,17 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
import { PrismaClient, VoteType } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { seedPolls } from "./seed/polls";
|
||||
import { seedScheduledEvents } from "./seed/scheduled-events";
|
||||
import { seedUsers } from "./seed/users";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const randInt = (max = 1, floor = 0) => {
|
||||
return Math.round(Math.random() * max) + floor;
|
||||
};
|
||||
|
||||
function generateTitle() {
|
||||
const titleTemplates = [
|
||||
() => `${faker.company.catchPhrase()} Meeting`,
|
||||
() => `${faker.commerce.department()} Team Sync`,
|
||||
() => `Q${faker.datatype.number({ min: 1, max: 4 })} Planning`,
|
||||
() => `${faker.name.jobArea()} Workshop`,
|
||||
() => `Project ${faker.word.adjective()} Update`,
|
||||
() => `${faker.company.bsBuzz()} Strategy Session`,
|
||||
() => faker.company.catchPhrase(),
|
||||
() => `${faker.name.jobType()} Interview`,
|
||||
() => `${faker.commerce.productAdjective()} Product Review`,
|
||||
() => `Team ${faker.word.verb()} Day`,
|
||||
];
|
||||
|
||||
return faker.helpers.arrayElement(titleTemplates)();
|
||||
}
|
||||
|
||||
async function createPollForUser(userId: string) {
|
||||
const duration = 60 * randInt(8);
|
||||
let cursor = dayjs().add(randInt(30), "day").second(0).minute(0);
|
||||
const numberOfOptions = randInt(5, 2); // Reduced for realism
|
||||
|
||||
const poll = await prisma.poll.create({
|
||||
include: {
|
||||
participants: true,
|
||||
options: true,
|
||||
},
|
||||
data: {
|
||||
id: faker.random.alpha(10),
|
||||
title: generateTitle(),
|
||||
description: generateDescription(),
|
||||
location: faker.address.streetAddress(),
|
||||
deadline: faker.date.future(),
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
status: faker.helpers.arrayElement(["live", "paused", "finalized"]),
|
||||
timeZone: duration !== 0 ? "Europe/London" : undefined,
|
||||
options: {
|
||||
create: Array.from({ length: numberOfOptions }).map(() => {
|
||||
const startTime = cursor.toDate();
|
||||
cursor = cursor.add(randInt(72, 1), "hour");
|
||||
return {
|
||||
startTime,
|
||||
duration,
|
||||
};
|
||||
}),
|
||||
},
|
||||
participants: {
|
||||
create: Array.from({ length: Math.round(Math.random() * 10) }).map(
|
||||
() => ({
|
||||
name: faker.name.fullName(),
|
||||
email: faker.internet.email(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
adminUrlId: faker.random.alpha(10),
|
||||
participantUrlId: faker.random.alpha(10),
|
||||
},
|
||||
});
|
||||
|
||||
// Generate vote data for all participants and options
|
||||
const voteData = poll.participants.flatMap((participant) =>
|
||||
poll.options.map((option) => ({
|
||||
id: faker.random.alpha(10),
|
||||
optionId: option.id,
|
||||
participantId: participant.id,
|
||||
pollId: poll.id,
|
||||
type: faker.helpers.arrayElement(["yes", "no", "ifNeedBe"]) as VoteType,
|
||||
})),
|
||||
);
|
||||
|
||||
// Create all votes in a single query
|
||||
await prisma.vote.createMany({
|
||||
data: voteData,
|
||||
});
|
||||
|
||||
return poll;
|
||||
}
|
||||
|
||||
// Function to generate realistic descriptions
|
||||
function generateDescription() {
|
||||
const descriptions = [
|
||||
"Discuss the quarterly results and strategize for the upcoming quarter. Please come prepared with your reports.",
|
||||
"Team meeting to align on project goals and timelines. Bring your ideas and feedback.",
|
||||
"An informal catch-up to discuss ongoing projects and any roadblocks. Open to all team members.",
|
||||
"Monthly review of our marketing strategies and performance metrics. Let's brainstorm new ideas.",
|
||||
"A brief meeting to go over the new software updates and how they will impact our workflow.",
|
||||
"Discussion on the upcoming product launch and marketing strategies. Your input is valuable!",
|
||||
"Weekly sync to check in on project progress and address any concerns. Please be on time.",
|
||||
"A brainstorming session for the new campaign. All creative minds are welcome!",
|
||||
"Review of the last sprint and planning for the next one. Let's ensure we're on track.",
|
||||
"An open forum for team members to share updates and challenges. Everyone is encouraged to speak up.",
|
||||
];
|
||||
|
||||
// Randomly select a description
|
||||
return faker.helpers.arrayElement(descriptions);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Create some users
|
||||
// Create some users and polls
|
||||
const freeUser = await prisma.user.create({
|
||||
data: {
|
||||
id: "free-user",
|
||||
name: "Dev User",
|
||||
email: "dev@rallly.co",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
});
|
||||
const users = await seedUsers();
|
||||
|
||||
const proUser = await prisma.user.create({
|
||||
data: {
|
||||
id: "pro-user",
|
||||
name: "Pro User",
|
||||
email: "dev+pro@rallly.co",
|
||||
subscription: {
|
||||
create: {
|
||||
id: "sub_123",
|
||||
currency: "usd",
|
||||
amount: 700,
|
||||
interval: "month",
|
||||
status: "active",
|
||||
active: true,
|
||||
priceId: "price_123",
|
||||
periodStart: new Date(),
|
||||
periodEnd: dayjs().add(1, "month").toDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
[freeUser, proUser].map(async (user) => {
|
||||
Array.from({ length: 20 }).forEach(async () => {
|
||||
await createPollForUser(user.id);
|
||||
});
|
||||
console.info(`✓ Added ${user.email}`);
|
||||
}),
|
||||
);
|
||||
console.info(`✓ Added polls for ${freeUser.email}`);
|
||||
for (const user of users) {
|
||||
await seedPolls(user.id);
|
||||
await seedScheduledEvents(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
|
119
packages/database/prisma/seed/polls.ts
Normal file
119
packages/database/prisma/seed/polls.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
import type { User } from "@prisma/client";
|
||||
import { VoteType } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import { prisma } from "@rallly/database";
|
||||
|
||||
import { randInt } from "./utils";
|
||||
|
||||
function generateTitle() {
|
||||
const titleTemplates = [
|
||||
() => `${faker.company.catchPhrase()} Meeting`,
|
||||
() => `${faker.commerce.department()} Team Sync`,
|
||||
() => `Q${faker.datatype.number({ min: 1, max: 4 })} Planning`,
|
||||
() => `${faker.name.jobArea()} Workshop`,
|
||||
() => `Project ${faker.word.adjective()} Update`,
|
||||
() => `${faker.company.bsBuzz()} Strategy Session`,
|
||||
() => faker.company.catchPhrase(),
|
||||
() => `${faker.name.jobType()} Interview`,
|
||||
() => `${faker.commerce.productAdjective()} Product Review`,
|
||||
() => `Team ${faker.word.verb()} Day`,
|
||||
];
|
||||
|
||||
return faker.helpers.arrayElement(titleTemplates)();
|
||||
}
|
||||
|
||||
// Function to generate realistic descriptions
|
||||
function generateDescription() {
|
||||
const descriptions = [
|
||||
"Discuss the quarterly results and strategize for the upcoming quarter. Please come prepared with your reports.",
|
||||
"Team meeting to align on project goals and timelines. Bring your ideas and feedback.",
|
||||
"An informal catch-up to discuss ongoing projects and any roadblocks. Open to all team members.",
|
||||
"Monthly review of our marketing strategies and performance metrics. Let's brainstorm new ideas.",
|
||||
"A brief meeting to go over the new software updates and how they will impact our workflow.",
|
||||
"Discussion on the upcoming product launch and marketing strategies. Your input is valuable!",
|
||||
"Weekly sync to check in on project progress and address any concerns. Please be on time.",
|
||||
"A brainstorming session for the new campaign. All creative minds are welcome!",
|
||||
"Review of the last sprint and planning for the next one. Let's ensure we're on track.",
|
||||
"An open forum for team members to share updates and challenges. Everyone is encouraged to speak up.",
|
||||
];
|
||||
|
||||
// Randomly select a description
|
||||
return faker.helpers.arrayElement(descriptions);
|
||||
}
|
||||
|
||||
async function createPollForUser(userId: string) {
|
||||
const duration = 60 * randInt(8);
|
||||
let cursor = dayjs().add(randInt(30), "day").second(0).minute(0);
|
||||
const numberOfOptions = randInt(5, 2); // Reduced for realism
|
||||
|
||||
const poll = await prisma.poll.create({
|
||||
include: {
|
||||
participants: true,
|
||||
options: true,
|
||||
},
|
||||
data: {
|
||||
id: faker.random.alpha(10),
|
||||
title: generateTitle(),
|
||||
description: generateDescription(),
|
||||
location: faker.address.streetAddress(),
|
||||
deadline: faker.date.future(),
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
status: faker.helpers.arrayElement(["live", "paused", "finalized"]),
|
||||
timeZone: duration !== 0 ? "Europe/London" : undefined,
|
||||
options: {
|
||||
create: Array.from({ length: numberOfOptions }).map(() => {
|
||||
const startTime = cursor.toDate();
|
||||
cursor = cursor.add(randInt(72, 1), "hour");
|
||||
return {
|
||||
startTime,
|
||||
duration,
|
||||
};
|
||||
}),
|
||||
},
|
||||
participants: {
|
||||
create: Array.from({ length: randInt(10) }).map(() => ({
|
||||
name: faker.name.fullName(),
|
||||
email: faker.internet.email(),
|
||||
})),
|
||||
},
|
||||
adminUrlId: faker.random.alpha(10),
|
||||
participantUrlId: faker.random.alpha(10),
|
||||
},
|
||||
});
|
||||
|
||||
// Generate vote data for all participants and options
|
||||
const voteData = poll.participants.flatMap((participant) =>
|
||||
poll.options.map((option) => ({
|
||||
id: faker.random.alpha(10),
|
||||
optionId: option.id,
|
||||
participantId: participant.id,
|
||||
pollId: poll.id,
|
||||
type: faker.helpers.arrayElement(["yes", "no", "ifNeedBe"]) as VoteType,
|
||||
})),
|
||||
);
|
||||
|
||||
// Create all votes in a single query
|
||||
if (voteData.length > 0) {
|
||||
await prisma.vote.createMany({
|
||||
data: voteData,
|
||||
});
|
||||
}
|
||||
|
||||
return poll;
|
||||
}
|
||||
|
||||
export async function seedPolls(userId: string) {
|
||||
console.info("Seeding polls...");
|
||||
const pollPromises = Array.from({ length: 20 }).map(() =>
|
||||
createPollForUser(userId),
|
||||
);
|
||||
|
||||
await Promise.all(pollPromises);
|
||||
|
||||
console.info(`✓ Seeded polls for ${userId}`);
|
||||
}
|
144
packages/database/prisma/seed/scheduled-events.ts
Normal file
144
packages/database/prisma/seed/scheduled-events.ts
Normal file
|
@ -0,0 +1,144 @@
|
|||
import { ScheduledEventInviteStatus } from "@prisma/client";
|
||||
import { Prisma, ScheduledEventStatus } from "@prisma/client"; // Ensure Prisma is imported
|
||||
import dayjs from "dayjs";
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
import { prisma } from "@rallly/database";
|
||||
import { randInt } from "./utils";
|
||||
|
||||
// Realistic event titles and descriptions
|
||||
function generateEventDetails() {
|
||||
const titles = [
|
||||
"Team Sync Meeting",
|
||||
"Product Strategy Session",
|
||||
"Design Review",
|
||||
"Engineering Standup",
|
||||
"Client Check-in Call",
|
||||
"Marketing Campaign Kickoff",
|
||||
"Sales Pipeline Review",
|
||||
"HR Training Workshop",
|
||||
"Finance Budget Planning",
|
||||
"All Hands Company Update",
|
||||
"Sprint Retrospective",
|
||||
"User Research Debrief",
|
||||
"Technical Deep Dive",
|
||||
"Content Calendar Planning",
|
||||
"Partnership Discussion",
|
||||
];
|
||||
const descriptions = [
|
||||
"Discussing project updates and blockers.",
|
||||
"Aligning on the product roadmap for the next quarter.",
|
||||
"Gathering feedback on the latest UI mockups.",
|
||||
"Quick daily updates from the engineering team.",
|
||||
"Reviewing progress and addressing client concerns.",
|
||||
"Launching the new social media campaign.",
|
||||
"Analyzing the current sales funnel and opportunities.",
|
||||
"Mandatory compliance training session.",
|
||||
"Meeting to finalize budget decisions across different departments.",
|
||||
"Sharing company performance and upcoming goals.",
|
||||
"Reflecting on the past sprint, celebrating successes, and identifying areas for improvement.",
|
||||
"Sharing key insights gathered from recent user research sessions.",
|
||||
"Exploring the architecture of the new microservice.",
|
||||
"Planning blog posts and social media content for the month.",
|
||||
"Exploring potential collaboration opportunities with external partners.",
|
||||
"Team building activity to foster collaboration.",
|
||||
"Workshop on improving presentation skills.",
|
||||
"Onboarding session for new hires.",
|
||||
"Reviewing customer feedback and planning improvements.",
|
||||
"Brainstorming session for new feature ideas.",
|
||||
];
|
||||
|
||||
return {
|
||||
title: faker.helpers.arrayElement(titles),
|
||||
description: faker.helpers.arrayElement(descriptions),
|
||||
};
|
||||
}
|
||||
|
||||
async function createScheduledEventForUser(userId: string) {
|
||||
const { title, description } = generateEventDetails();
|
||||
const isAllDay = Math.random() < 0.3; // ~30% chance of being all-day
|
||||
|
||||
let startTime: Date;
|
||||
let endTime: Date | null;
|
||||
|
||||
if (isAllDay) {
|
||||
const startDate = dayjs(
|
||||
faker.datatype.boolean() ? faker.date.past(1) : faker.date.soon(30),
|
||||
)
|
||||
.startOf("day")
|
||||
.toDate();
|
||||
startTime = startDate;
|
||||
|
||||
// Decide if it's a multi-day event
|
||||
const isMultiDay = Math.random() < 0.2; // ~20% chance of multi-day
|
||||
if (isMultiDay) {
|
||||
const durationDays = faker.datatype.number({ min: 1, max: 3 });
|
||||
// End date is the start of the day *after* the last full day
|
||||
endTime = dayjs(startDate)
|
||||
.add(durationDays + 1, "day")
|
||||
.toDate();
|
||||
} else {
|
||||
// Single all-day event ends at the start of the next day
|
||||
endTime = dayjs(startDate).add(1, "day").toDate();
|
||||
}
|
||||
} else {
|
||||
// Generate times for non-all-day events
|
||||
startTime = dayjs(
|
||||
faker.datatype.boolean() ? faker.date.past(1) : faker.date.soon(30),
|
||||
)
|
||||
.second(faker.helpers.arrayElement([0, 15, 30, 45])) // Add some variance
|
||||
.minute(faker.helpers.arrayElement([0, 15, 30, 45]))
|
||||
.hour(faker.datatype.number({ min: 8, max: 20 })) // Wider range for hours
|
||||
.toDate();
|
||||
const durationMinutes = faker.helpers.arrayElement([30, 60, 90, 120, 180]); // Longer durations possible
|
||||
endTime = dayjs(startTime).add(durationMinutes, "minute").toDate();
|
||||
}
|
||||
|
||||
// Use only valid statuses from the schema
|
||||
const status = faker.helpers.arrayElement<ScheduledEventStatus>([
|
||||
ScheduledEventStatus.confirmed,
|
||||
ScheduledEventStatus.canceled,
|
||||
]);
|
||||
const timeZone = faker.address.timeZone();
|
||||
|
||||
const data: Prisma.ScheduledEventCreateInput = {
|
||||
title,
|
||||
description,
|
||||
start: startTime, // Use correct model field name 'start'
|
||||
end: endTime, // Use correct model field name 'end'
|
||||
timeZone,
|
||||
status, // Assign the randomly selected valid status
|
||||
user: { connect: { id: userId } }, // Connect to existing user
|
||||
allDay: isAllDay,
|
||||
location: faker.datatype.boolean()
|
||||
? faker.address.streetAddress()
|
||||
: undefined,
|
||||
// Add invites (optional, example below)
|
||||
invites: {
|
||||
create: Array.from({ length: randInt(5, 0) }).map(() => ({
|
||||
inviteeEmail: faker.internet.email(),
|
||||
inviteeName: faker.name.fullName(),
|
||||
inviteeTimeZone: faker.address.timeZone(),
|
||||
status: faker.helpers.arrayElement<ScheduledEventInviteStatus>([
|
||||
"accepted",
|
||||
"declined",
|
||||
"tentative",
|
||||
"pending",
|
||||
]),
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
await prisma.scheduledEvent.create({ data });
|
||||
}
|
||||
|
||||
export async function seedScheduledEvents(userId: string) {
|
||||
console.info("Seeding scheduled events...");
|
||||
const eventPromises = Array.from({ length: 15 }).map((_, i) =>
|
||||
createScheduledEventForUser(userId),
|
||||
);
|
||||
|
||||
await Promise.all(eventPromises);
|
||||
|
||||
console.info(`✓ Seeded scheduled events for ${userId}`);
|
||||
}
|
43
packages/database/prisma/seed/users.ts
Normal file
43
packages/database/prisma/seed/users.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import dayjs from "dayjs";
|
||||
import { prisma } from "@rallly/database";
|
||||
|
||||
export async function seedUsers() {
|
||||
console.info("Seeding users...");
|
||||
const freeUser = await prisma.user.upsert({
|
||||
where: { email: "dev@rallly.co" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "free-user",
|
||||
name: "Dev User",
|
||||
email: "dev@rallly.co",
|
||||
timeZone: "America/New_York",
|
||||
},
|
||||
});
|
||||
|
||||
const proUser = await prisma.user.upsert({
|
||||
where: { email: "dev+pro@rallly.co" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "pro-user",
|
||||
name: "Pro User",
|
||||
email: "dev+pro@rallly.co",
|
||||
subscription: {
|
||||
create: {
|
||||
id: "sub_123",
|
||||
currency: "usd",
|
||||
amount: 700,
|
||||
interval: "month",
|
||||
status: "active",
|
||||
active: true,
|
||||
priceId: "price_123",
|
||||
periodStart: new Date(),
|
||||
periodEnd: dayjs().add(1, "month").toDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
console.info(`✓ Seeded user ${freeUser.email}`);
|
||||
console.info(`✓ Seeded user ${proUser.email}`);
|
||||
|
||||
return [freeUser, proUser];
|
||||
}
|
9
packages/database/prisma/seed/utils.ts
Normal file
9
packages/database/prisma/seed/utils.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Generates a random integer between floor and max (inclusive).
|
||||
* @param max The maximum value.
|
||||
* @param floor The minimum value (default: 0).
|
||||
* @returns A random integer.
|
||||
*/
|
||||
export const randInt = (max = 1, floor = 0): number => {
|
||||
return Math.round(Math.random() * max) + floor;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue