From 04fcc0350f56b36386da1691e0f1a914712b7ddd Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Sun, 15 Jun 2025 11:48:51 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20spaces=20concept=20(#1776)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/api/stripe/checkout/route.ts | 29 ++++++- .../handlers/customer-subscription/created.ts | 72 +++++++++++----- .../handlers/customer-subscription/updated.ts | 2 +- .../app/api/stripe/webhook/handlers/utils.ts | 5 -- apps/web/src/auth/adapters/prisma.ts | 22 +++++ apps/web/src/auth/helpers/merge-user.ts | 10 +-- apps/web/src/auth/queries.ts | 19 +++++ apps/web/src/features/spaces/queries.ts | 38 +++++++++ apps/web/src/features/subscription/schema.ts | 8 ++ apps/web/src/trpc/routers/auth.ts | 5 ++ apps/web/src/trpc/routers/polls.ts | 13 +++ .../migration.sql | 34 ++++++++ .../migration.sql | 17 ++++ .../migration.sql | 15 ++++ .../database/prisma/models/billing.prisma | 4 +- packages/database/prisma/models/event.prisma | 2 + packages/database/prisma/models/poll.prisma | 2 + packages/database/prisma/models/user.prisma | 64 +++++++++----- packages/database/prisma/seed/polls.ts | 25 +++++- .../database/prisma/seed/scheduled-events.ts | 83 ++++++++++++------- packages/database/prisma/seed/users.ts | 13 +++ 21 files changed, 389 insertions(+), 93 deletions(-) create mode 100644 apps/web/src/features/spaces/queries.ts create mode 100644 packages/database/prisma/migrations/20250614062855_create_spaces_model/migration.sql create mode 100644 packages/database/prisma/migrations/20250614110551_create_spaces/migration.sql create mode 100644 packages/database/prisma/migrations/20250614115818_migrate_events_to_spaces/migration.sql diff --git a/apps/web/src/app/api/stripe/checkout/route.ts b/apps/web/src/app/api/stripe/checkout/route.ts index 95d00d1a8..00a746317 100644 --- a/apps/web/src/app/api/stripe/checkout/route.ts +++ b/apps/web/src/app/api/stripe/checkout/route.ts @@ -5,6 +5,10 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { z } from "zod"; +import type { + SubscriptionCheckoutMetadata, + SubscriptionMetadata, +} from "@/features/subscription/schema"; import { auth } from "@/next-auth"; const inputSchema = z.object({ @@ -46,11 +50,26 @@ export async function POST(request: NextRequest) { active: true, }, }, + spaces: { + select: { + id: true, + }, + }, }, }); if (!user) { - return new NextResponse(null, { status: 404 }); + return NextResponse.json( + { error: `User with ID ${userSession.user.id} not found` }, + { status: 404 }, + ); + } + + if (user.spaces.length === 0) { + return NextResponse.json( + { error: `Space with owner ID ${userSession.user.id} not found` }, + { status: 404 }, + ); } let customerId = user.customerId; @@ -83,6 +102,8 @@ export async function POST(request: NextRequest) { const proPricingData = await getProPricing(); + const spaceId = user.spaces[0].id; + const session = await stripe.checkout.sessions.create({ success_url: absoluteUrl( return_path ?? "/api/stripe/portal?session_id={CHECKOUT_SESSION_ID}", @@ -101,11 +122,13 @@ export async function POST(request: NextRequest) { }, metadata: { userId: userSession.user.id, - }, + spaceId, + } satisfies SubscriptionCheckoutMetadata, subscription_data: { metadata: { userId: userSession.user.id, - }, + spaceId, + } satisfies SubscriptionMetadata, }, line_items: [ { diff --git a/apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts b/apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts index 0f1b42d7a..9efa9458e 100644 --- a/apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts +++ b/apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/created.ts @@ -1,11 +1,12 @@ import type { Stripe } from "@rallly/billing"; import { prisma } from "@rallly/database"; +import { getDefaultSpace, getSpace } from "@/features/spaces/queries"; +import { subscriptionMetadataSchema } from "@/features/subscription/schema"; import { getExpandedSubscription, getSubscriptionDetails, isSubscriptionActive, - subscriptionMetadataSchema, toDate, } from "../utils"; @@ -29,13 +30,46 @@ export async function onCustomerSubscriptionCreated(event: Stripe.Event) { // Check if user already has a subscription const existingUser = await prisma.user.findUnique({ where: { id: userId }, - include: { subscription: true }, + select: { + subscription: true, + }, }); if (!existingUser) { throw new Error(`User with ID ${userId} not found`); } + let spaceId: string; + + // The space should be in the metadata, + // but if it's not, we fallback to the default space. + // This is temporary while we haven't run a data migration + // to add the spaceId to the metadata for all existing subscriptions + if (res.data.spaceId) { + const space = await getSpace({ id: res.data.spaceId }); + if (!space) { + throw new Error(`Space with ID ${res.data.spaceId} not found`); + } + + if (space.ownerId !== userId) { + throw new Error( + `Space with ID ${res.data.spaceId} does not belong to user ${userId}`, + ); + } + + spaceId = space.id; + } else { + // TODO: Remove this fallback once all subscriptions have + // a spaceId in their metadata + const space = await getDefaultSpace({ ownerId: userId }); + + if (!space) { + throw new Error(`Default space with owner ID ${userId} not found`); + } + + spaceId = space.id; + } + // If user already has a subscription, update it or replace it if (existingUser.subscription) { // Update the existing subscription with new data @@ -53,30 +87,26 @@ export async function onCustomerSubscriptionCreated(event: Stripe.Event) { periodStart: toDate(subscription.current_period_start), periodEnd: toDate(subscription.current_period_end), cancelAtPeriodEnd: subscription.cancel_at_period_end, + spaceId, }, }); } else { // Create a new subscription for the user - await prisma.user.update({ - where: { - id: userId, - }, + await prisma.subscription.create({ data: { - subscription: { - create: { - id: subscription.id, - active: isActive, - priceId, - currency, - interval, - amount, - status: subscription.status, - createdAt: toDate(subscription.created), - periodStart: toDate(subscription.current_period_start), - periodEnd: toDate(subscription.current_period_end), - cancelAtPeriodEnd: subscription.cancel_at_period_end, - }, - }, + id: subscription.id, + userId, + active: isActive, + priceId, + currency, + interval, + amount, + status: subscription.status, + createdAt: toDate(subscription.created), + periodStart: toDate(subscription.current_period_start), + periodEnd: toDate(subscription.current_period_end), + cancelAtPeriodEnd: subscription.cancel_at_period_end, + spaceId, }, }); } diff --git a/apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/updated.ts b/apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/updated.ts index 87b550c72..a752be38a 100644 --- a/apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/updated.ts +++ b/apps/web/src/app/api/stripe/webhook/handlers/customer-subscription/updated.ts @@ -2,11 +2,11 @@ import type { Stripe } from "@rallly/billing"; import { prisma } from "@rallly/database"; import { posthog } from "@rallly/posthog/server"; +import { subscriptionMetadataSchema } from "@/features/subscription/schema"; import { getExpandedSubscription, getSubscriptionDetails, isSubscriptionActive, - subscriptionMetadataSchema, toDate, } from "../utils"; diff --git a/apps/web/src/app/api/stripe/webhook/handlers/utils.ts b/apps/web/src/app/api/stripe/webhook/handlers/utils.ts index 8b3e63596..581d03653 100644 --- a/apps/web/src/app/api/stripe/webhook/handlers/utils.ts +++ b/apps/web/src/app/api/stripe/webhook/handlers/utils.ts @@ -2,11 +2,6 @@ import type { Stripe } from "@rallly/billing"; import { stripe } from "@rallly/billing"; import type { Prisma } from "@rallly/database"; import { prisma } from "@rallly/database"; -import { z } from "zod"; - -export const subscriptionMetadataSchema = z.object({ - userId: z.string(), -}); export function toDate(date: number) { return new Date(date * 1000); diff --git a/apps/web/src/auth/adapters/prisma.ts b/apps/web/src/auth/adapters/prisma.ts index 56cd58c20..5f97940ca 100644 --- a/apps/web/src/auth/adapters/prisma.ts +++ b/apps/web/src/auth/adapters/prisma.ts @@ -37,5 +37,27 @@ export function CustomPrismaAdapter(options: { }, }); }, + createUser: async (user) => { + const newUser = await prisma.user.create({ + data: { + name: user.name ?? "Unknown", + email: user.email, + emailVerified: user.emailVerified, + image: user.image, + timeZone: user.timeZone, + weekStart: user.weekStart, + timeFormat: user.timeFormat, + locale: user.locale, + role: "user", + spaces: { + create: { + name: "Personal", + }, + }, + }, + }); + + return newUser; + }, } as Adapter; } diff --git a/apps/web/src/auth/helpers/merge-user.ts b/apps/web/src/auth/helpers/merge-user.ts index 4cfab3bd5..d51334f98 100644 --- a/apps/web/src/auth/helpers/merge-user.ts +++ b/apps/web/src/auth/helpers/merge-user.ts @@ -1,3 +1,4 @@ +import { getDefaultSpace } from "@/features/spaces/queries"; import { prisma } from "@rallly/database"; import { posthog } from "@rallly/posthog/server"; import * as Sentry from "@sentry/nextjs"; @@ -6,13 +7,9 @@ export const mergeGuestsIntoUser = async ( userId: string, guestIds: string[], ) => { - const count = await prisma.user.count({ - where: { - id: userId, - }, - }); + const space = await getDefaultSpace({ ownerId: userId }); - if (count === 0) { + if (!space) { console.warn(`User ${userId} not found`); return; } @@ -29,6 +26,7 @@ export const mergeGuestsIntoUser = async ( data: { guestId: null, userId: userId, + spaceId: space.id, }, }), diff --git a/apps/web/src/auth/queries.ts b/apps/web/src/auth/queries.ts index 1ce41b3ef..1cb6098b1 100644 --- a/apps/web/src/auth/queries.ts +++ b/apps/web/src/auth/queries.ts @@ -1,3 +1,4 @@ +import { getDefaultSpace } from "@/features/spaces/queries"; import { getUser } from "@/features/user/queries"; import { auth } from "@/next-auth"; import { notFound, redirect } from "next/navigation"; @@ -34,3 +35,21 @@ export const requireAdmin = cache(async () => { return user; }); + +export const getActiveSpace = cache(async () => { + const session = await auth(); + + if (!session?.user?.id) { + return null; + } + + const user = await getUser(session.user.id); + + if (!user) { + return null; + } + + const space = await getDefaultSpace({ ownerId: user.id }); + + return space; +}); diff --git a/apps/web/src/features/spaces/queries.ts b/apps/web/src/features/spaces/queries.ts new file mode 100644 index 000000000..b6b8325fe --- /dev/null +++ b/apps/web/src/features/spaces/queries.ts @@ -0,0 +1,38 @@ +import { prisma } from "@rallly/database"; + +export async function listSpaces({ ownerId }: { ownerId: string }) { + const spaces = await prisma.space.findMany({ + where: { + ownerId, + }, + }); + + return spaces; +} + +export async function getDefaultSpace({ ownerId }: { ownerId: string }) { + const space = await prisma.space.findFirst({ + where: { + ownerId, + }, + orderBy: { + createdAt: "asc", + }, + }); + + if (!space) { + throw new Error(`Space with owner ID ${ownerId} not found`); + } + + return space; +} + +export async function getSpace({ id }: { id: string }) { + const space = await prisma.space.findUnique({ + where: { + id, + }, + }); + + return space; +} diff --git a/apps/web/src/features/subscription/schema.ts b/apps/web/src/features/subscription/schema.ts index 2b2c61e6d..2ec2a54b4 100644 --- a/apps/web/src/features/subscription/schema.ts +++ b/apps/web/src/features/subscription/schema.ts @@ -2,8 +2,16 @@ import { z } from "zod"; export const subscriptionCheckoutMetadataSchema = z.object({ userId: z.string(), + spaceId: z.string().optional(), }); export type SubscriptionCheckoutMetadata = z.infer< typeof subscriptionCheckoutMetadataSchema >; + +export const subscriptionMetadataSchema = z.object({ + userId: z.string(), + spaceId: z.string().optional(), +}); + +export type SubscriptionMetadata = z.infer; diff --git a/apps/web/src/trpc/routers/auth.ts b/apps/web/src/trpc/routers/auth.ts index 997ee752c..988b55faa 100644 --- a/apps/web/src/trpc/routers/auth.ts +++ b/apps/web/src/trpc/routers/auth.ts @@ -133,6 +133,11 @@ export const auth = router({ timeFormat: input.timeFormat, weekStart: input.weekStart, locale: input.locale, + spaces: { + create: { + name: "Personal", + }, + }, }, }); diff --git a/apps/web/src/trpc/routers/polls.ts b/apps/web/src/trpc/routers/polls.ts index 654fd1aa6..db9c00ce9 100644 --- a/apps/web/src/trpc/routers/polls.ts +++ b/apps/web/src/trpc/routers/polls.ts @@ -12,6 +12,7 @@ import { z } from "zod"; import { moderateContent } from "@/features/moderation"; import { getEmailClient } from "@/utils/emails"; +import { getActiveSpace } from "@/auth/queries"; import { getTimeZoneAbbreviation } from "../../utils/date"; import { createRateLimitMiddleware, @@ -180,6 +181,8 @@ export const polls = router({ const participantUrlId = nanoid(); const pollId = nanoid(); + const space = await getActiveSpace(); + const poll = await prisma.poll.create({ select: { adminUrlId: true, @@ -228,6 +231,7 @@ export const polls = router({ disableComments: input.disableComments, hideScores: input.hideScores, requireParticipantEmail: input.requireParticipantEmail, + spaceId: space?.id, }, }); @@ -551,6 +555,7 @@ export const polls = router({ title: true, location: true, description: true, + spaceId: true, user: { select: { name: true, @@ -620,6 +625,13 @@ export const polls = router({ eventStart = eventStart.utc(); } + if (!poll.spaceId) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Poll has no space", + }); + } + await prisma.poll.update({ where: { id: input.pollId, @@ -635,6 +647,7 @@ export const polls = router({ location: poll.location, timeZone: poll.timeZone, userId: ctx.user.id, + spaceId: poll.spaceId, allDay: option.duration === 0, status: "confirmed", invites: { diff --git a/packages/database/prisma/migrations/20250614062855_create_spaces_model/migration.sql b/packages/database/prisma/migrations/20250614062855_create_spaces_model/migration.sql new file mode 100644 index 000000000..01dfa28cc --- /dev/null +++ b/packages/database/prisma/migrations/20250614062855_create_spaces_model/migration.sql @@ -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; diff --git a/packages/database/prisma/migrations/20250614110551_create_spaces/migration.sql b/packages/database/prisma/migrations/20250614110551_create_spaces/migration.sql new file mode 100644 index 000000000..01228df29 --- /dev/null +++ b/packages/database/prisma/migrations/20250614110551_create_spaces/migration.sql @@ -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; \ No newline at end of file diff --git a/packages/database/prisma/migrations/20250614115818_migrate_events_to_spaces/migration.sql b/packages/database/prisma/migrations/20250614115818_migrate_events_to_spaces/migration.sql new file mode 100644 index 000000000..fd66396d5 --- /dev/null +++ b/packages/database/prisma/migrations/20250614115818_migrate_events_to_spaces/migration.sql @@ -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; diff --git a/packages/database/prisma/models/billing.prisma b/packages/database/prisma/models/billing.prisma index e7024074f..e533e2682 100644 --- a/packages/database/prisma/models/billing.prisma +++ b/packages/database/prisma/models/billing.prisma @@ -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") } diff --git a/packages/database/prisma/models/event.prisma b/packages/database/prisma/models/event.prisma index 1b3fd1901..6112809e8 100644 --- a/packages/database/prisma/models/event.prisma +++ b/packages/database/prisma/models/event.prisma @@ -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[] diff --git a/packages/database/prisma/models/poll.prisma b/packages/database/prisma/models/poll.prisma index 05c2f55ba..075358d6b 100644 --- a/packages/database/prisma/models/poll.prisma +++ b/packages/database/prisma/models/poll.prisma @@ -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") diff --git a/packages/database/prisma/models/user.prisma b/packages/database/prisma/models/user.prisma index cf9b026e7..501954f09 100644 --- a/packages/database/prisma/models/user.prisma +++ b/packages/database/prisma/models/user.prisma @@ -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 diff --git a/packages/database/prisma/seed/polls.ts b/packages/database/prisma/seed/polls.ts index c1bcd4617..bb198bc9a 100644 --- a/packages/database/prisma/seed/polls.ts +++ b/packages/database/prisma/seed/polls.ts @@ -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); diff --git a/packages/database/prisma/seed/scheduled-events.ts b/packages/database/prisma/seed/scheduled-events.ts index 6c70f1e8c..22414fba2 100644 --- a/packages/database/prisma/seed/scheduled-events.ts +++ b/packages/database/prisma/seed/scheduled-events.ts @@ -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([ - "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([ + "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); diff --git a/packages/database/prisma/seed/users.ts b/packages/database/prisma/seed/users.ts index 67badda48..0af376d32 100644 --- a/packages/database/prisma/seed/users.ts +++ b/packages/database/prisma/seed/users.ts @@ -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", }, }, },