mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-08 12:07:28 +02:00
✨ Add spaces concept (#1776)
This commit is contained in:
parent
92a72dde60
commit
04fcc0350f
21 changed files with 389 additions and 93 deletions
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[space_id]` on the table `subscriptions` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "polls" ADD COLUMN "space_id" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "subscriptions" ADD COLUMN "space_id" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "spaces" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"owner_id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "spaces_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "subscriptions_space_id_key" ON "subscriptions"("space_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "spaces"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "polls" ADD CONSTRAINT "polls_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "spaces"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "spaces" ADD CONSTRAINT "spaces_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -0,0 +1,17 @@
|
|||
INSERT INTO spaces (id, name, owner_id, created_at, updated_at)
|
||||
SELECT gen_random_uuid(), 'Personal', id, NOW(), NOW()
|
||||
FROM users
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM spaces WHERE spaces.owner_id = users.id
|
||||
) ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Set space_id for polls
|
||||
UPDATE polls
|
||||
SET space_id = spaces.id
|
||||
FROM spaces
|
||||
WHERE polls.user_id = spaces.owner_id AND polls.space_id IS NULL;
|
||||
|
||||
UPDATE subscriptions
|
||||
SET space_id = spaces.id
|
||||
FROM spaces
|
||||
WHERE subscriptions.user_id = spaces.owner_id AND subscriptions.space_id IS NULL;
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `space_id` to the `scheduled_events` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "scheduled_events" ADD COLUMN "space_id" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scheduled_events" ADD CONSTRAINT "scheduled_events_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "spaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
UPDATE "scheduled_events" SET "space_id" = (SELECT "id" FROM "spaces" WHERE "owner_id" = "scheduled_events"."user_id" LIMIT 1);
|
||||
|
||||
ALTER TABLE "scheduled_events" ALTER COLUMN "space_id" SET NOT NULL;
|
|
@ -11,8 +11,10 @@ model Subscription {
|
|||
periodEnd DateTime @map("period_end")
|
||||
cancelAtPeriodEnd Boolean @default(false) @map("cancel_at_period_end")
|
||||
userId String @unique @map("user_id")
|
||||
spaceId String? @unique @map("space_id")
|
||||
|
||||
user User @relation("UserToSubscription", fields: [userId], references: [id], onDelete: Cascade)
|
||||
user User @relation("UserToSubscription", fields: [userId], references: [id], onDelete: Cascade)
|
||||
space Space? @relation("SpaceToSubscription", fields: [spaceId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@map("subscriptions")
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ enum ScheduledEventInviteStatus {
|
|||
model ScheduledEvent {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
spaceId String @map("space_id")
|
||||
title String
|
||||
description String?
|
||||
location String?
|
||||
|
@ -31,6 +32,7 @@ model ScheduledEvent {
|
|||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
|
||||
rescheduledDates RescheduledEventDate[]
|
||||
invites ScheduledEventInvite[]
|
||||
polls Poll[]
|
||||
|
|
|
@ -46,6 +46,8 @@ model Poll {
|
|||
comments Comment[]
|
||||
votes Vote[]
|
||||
views PollView[]
|
||||
space Space? @relation(fields: [spaceId], references: [id], onDelete: SetNull)
|
||||
spaceId String? @map("space_id")
|
||||
|
||||
@@index([guestId])
|
||||
@@map("polls")
|
||||
|
|
|
@ -33,30 +33,33 @@ enum UserRole {
|
|||
}
|
||||
|
||||
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")
|
||||
role UserRole @default(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")
|
||||
role UserRole @default(user)
|
||||
|
||||
comments Comment[]
|
||||
polls Poll[]
|
||||
watcher Watcher[]
|
||||
accounts Account[]
|
||||
participants Participant[]
|
||||
paymentMethods PaymentMethod[]
|
||||
subscription Subscription? @relation("UserToSubscription")
|
||||
|
||||
spaces Space[] @relation("UserSpaces")
|
||||
|
||||
comments Comment[]
|
||||
polls Poll[]
|
||||
watcher Watcher[]
|
||||
accounts Account[]
|
||||
participants Participant[]
|
||||
paymentMethods PaymentMethod[]
|
||||
subscription Subscription? @relation("UserToSubscription")
|
||||
pollViews PollView[]
|
||||
scheduledEvents ScheduledEvent[]
|
||||
scheduledEventInvites ScheduledEventInvite[]
|
||||
|
@ -64,6 +67,21 @@ model User {
|
|||
@@map("users")
|
||||
}
|
||||
|
||||
model Space {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
ownerId String @map("owner_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
owner User @relation("UserSpaces", fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
polls Poll[]
|
||||
scheduledEvents ScheduledEvent[]
|
||||
subscription Subscription? @relation("SpaceToSubscription")
|
||||
|
||||
@@map("spaces")
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String @db.Citext
|
||||
token String @unique
|
||||
|
|
|
@ -41,7 +41,13 @@ function generateDescription() {
|
|||
return faker.helpers.arrayElement(descriptions);
|
||||
}
|
||||
|
||||
async function createPollForUser(userId: string) {
|
||||
async function createPollForUser({
|
||||
userId,
|
||||
spaceId,
|
||||
}: {
|
||||
userId: string;
|
||||
spaceId: 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
|
||||
|
@ -62,6 +68,11 @@ async function createPollForUser(userId: string) {
|
|||
id: userId,
|
||||
},
|
||||
},
|
||||
space: {
|
||||
connect: {
|
||||
id: spaceId,
|
||||
},
|
||||
},
|
||||
status: faker.helpers.arrayElement(["live", "paused", "finalized"]),
|
||||
timeZone: duration !== 0 ? "Europe/London" : undefined,
|
||||
options: {
|
||||
|
@ -108,8 +119,18 @@ async function createPollForUser(userId: string) {
|
|||
|
||||
export async function seedPolls(userId: string) {
|
||||
console.info("Seeding polls...");
|
||||
const space = await prisma.space.findFirst({
|
||||
where: {
|
||||
ownerId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!space) {
|
||||
throw new Error(`No space found for user ${userId}`);
|
||||
}
|
||||
|
||||
const pollPromises = Array.from({ length: 20 }).map(() =>
|
||||
createPollForUser(userId),
|
||||
createPollForUser({ userId, spaceId: space.id }),
|
||||
);
|
||||
|
||||
await Promise.all(pollPromises);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { faker } from "@faker-js/faker";
|
||||
import type { ScheduledEventInviteStatus } from "@prisma/client";
|
||||
import { type Prisma, ScheduledEventStatus } from "@prisma/client"; // Ensure Prisma is imported
|
||||
import { ScheduledEventStatus } from "@prisma/client"; // Ensure Prisma is imported
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { prisma } from "@rallly/database";
|
||||
|
@ -54,7 +54,13 @@ function generateEventDetails() {
|
|||
};
|
||||
}
|
||||
|
||||
async function createScheduledEventForUser(userId: string) {
|
||||
async function createScheduledEventForUser({
|
||||
userId,
|
||||
spaceId,
|
||||
}: {
|
||||
userId: string;
|
||||
spaceId: string;
|
||||
}) {
|
||||
const { title, description } = generateEventDetails();
|
||||
const isAllDay = Math.random() < 0.3; // ~30% chance of being all-day
|
||||
|
||||
|
@ -101,41 +107,56 @@ async function createScheduledEventForUser(userId: string) {
|
|||
]);
|
||||
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: {
|
||||
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
|
||||
space: {
|
||||
connect: { id: spaceId },
|
||||
},
|
||||
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 space = await prisma.space.findFirst({
|
||||
where: {
|
||||
ownerId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!space) {
|
||||
throw new Error(`No space found for user ${userId}`);
|
||||
}
|
||||
|
||||
const eventPromises = Array.from({ length: 15 }).map((_, i) =>
|
||||
createScheduledEventForUser(userId),
|
||||
createScheduledEventForUser({ userId, spaceId: space.id }),
|
||||
);
|
||||
|
||||
await Promise.all(eventPromises);
|
||||
|
|
|
@ -11,6 +11,12 @@ export async function seedUsers() {
|
|||
name: "Dev User",
|
||||
email: "dev@rallly.co",
|
||||
timeZone: "America/New_York",
|
||||
spaces: {
|
||||
create: {
|
||||
id: "space-1",
|
||||
name: "Personal",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -21,6 +27,12 @@ export async function seedUsers() {
|
|||
id: "pro-user",
|
||||
name: "Pro User",
|
||||
email: "dev+pro@rallly.co",
|
||||
spaces: {
|
||||
create: {
|
||||
id: "space-2",
|
||||
name: "Personal",
|
||||
},
|
||||
},
|
||||
subscription: {
|
||||
create: {
|
||||
id: "sub_123",
|
||||
|
@ -32,6 +44,7 @@ export async function seedUsers() {
|
|||
priceId: "price_123",
|
||||
periodStart: new Date(),
|
||||
periodEnd: dayjs().add(1, "month").toDate(),
|
||||
spaceId: "space-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue