mirror of
https://github.com/lukevella/rallly.git
synced 2025-07-08 20:17: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
|
@ -5,6 +5,10 @@ import type { NextRequest } from "next/server";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
SubscriptionCheckoutMetadata,
|
||||||
|
SubscriptionMetadata,
|
||||||
|
} from "@/features/subscription/schema";
|
||||||
import { auth } from "@/next-auth";
|
import { auth } from "@/next-auth";
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
|
@ -46,11 +50,26 @@ export async function POST(request: NextRequest) {
|
||||||
active: true,
|
active: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
spaces: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
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;
|
let customerId = user.customerId;
|
||||||
|
@ -83,6 +102,8 @@ export async function POST(request: NextRequest) {
|
||||||
|
|
||||||
const proPricingData = await getProPricing();
|
const proPricingData = await getProPricing();
|
||||||
|
|
||||||
|
const spaceId = user.spaces[0].id;
|
||||||
|
|
||||||
const session = await stripe.checkout.sessions.create({
|
const session = await stripe.checkout.sessions.create({
|
||||||
success_url: absoluteUrl(
|
success_url: absoluteUrl(
|
||||||
return_path ?? "/api/stripe/portal?session_id={CHECKOUT_SESSION_ID}",
|
return_path ?? "/api/stripe/portal?session_id={CHECKOUT_SESSION_ID}",
|
||||||
|
@ -101,11 +122,13 @@ export async function POST(request: NextRequest) {
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
userId: userSession.user.id,
|
userId: userSession.user.id,
|
||||||
},
|
spaceId,
|
||||||
|
} satisfies SubscriptionCheckoutMetadata,
|
||||||
subscription_data: {
|
subscription_data: {
|
||||||
metadata: {
|
metadata: {
|
||||||
userId: userSession.user.id,
|
userId: userSession.user.id,
|
||||||
},
|
spaceId,
|
||||||
|
} satisfies SubscriptionMetadata,
|
||||||
},
|
},
|
||||||
line_items: [
|
line_items: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import type { Stripe } from "@rallly/billing";
|
import type { Stripe } from "@rallly/billing";
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
|
|
||||||
|
import { getDefaultSpace, getSpace } from "@/features/spaces/queries";
|
||||||
|
import { subscriptionMetadataSchema } from "@/features/subscription/schema";
|
||||||
import {
|
import {
|
||||||
getExpandedSubscription,
|
getExpandedSubscription,
|
||||||
getSubscriptionDetails,
|
getSubscriptionDetails,
|
||||||
isSubscriptionActive,
|
isSubscriptionActive,
|
||||||
subscriptionMetadataSchema,
|
|
||||||
toDate,
|
toDate,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
|
@ -29,13 +30,46 @@ export async function onCustomerSubscriptionCreated(event: Stripe.Event) {
|
||||||
// Check if user already has a subscription
|
// Check if user already has a subscription
|
||||||
const existingUser = await prisma.user.findUnique({
|
const existingUser = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
include: { subscription: true },
|
select: {
|
||||||
|
subscription: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
throw new Error(`User with ID ${userId} not found`);
|
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 user already has a subscription, update it or replace it
|
||||||
if (existingUser.subscription) {
|
if (existingUser.subscription) {
|
||||||
// Update the existing subscription with new data
|
// Update the existing subscription with new data
|
||||||
|
@ -53,18 +87,15 @@ export async function onCustomerSubscriptionCreated(event: Stripe.Event) {
|
||||||
periodStart: toDate(subscription.current_period_start),
|
periodStart: toDate(subscription.current_period_start),
|
||||||
periodEnd: toDate(subscription.current_period_end),
|
periodEnd: toDate(subscription.current_period_end),
|
||||||
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||||
|
spaceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Create a new subscription for the user
|
// Create a new subscription for the user
|
||||||
await prisma.user.update({
|
await prisma.subscription.create({
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
data: {
|
data: {
|
||||||
subscription: {
|
|
||||||
create: {
|
|
||||||
id: subscription.id,
|
id: subscription.id,
|
||||||
|
userId,
|
||||||
active: isActive,
|
active: isActive,
|
||||||
priceId,
|
priceId,
|
||||||
currency,
|
currency,
|
||||||
|
@ -75,8 +106,7 @@ export async function onCustomerSubscriptionCreated(event: Stripe.Event) {
|
||||||
periodStart: toDate(subscription.current_period_start),
|
periodStart: toDate(subscription.current_period_start),
|
||||||
periodEnd: toDate(subscription.current_period_end),
|
periodEnd: toDate(subscription.current_period_end),
|
||||||
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||||
},
|
spaceId,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@ import type { Stripe } from "@rallly/billing";
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
import { posthog } from "@rallly/posthog/server";
|
import { posthog } from "@rallly/posthog/server";
|
||||||
|
|
||||||
|
import { subscriptionMetadataSchema } from "@/features/subscription/schema";
|
||||||
import {
|
import {
|
||||||
getExpandedSubscription,
|
getExpandedSubscription,
|
||||||
getSubscriptionDetails,
|
getSubscriptionDetails,
|
||||||
isSubscriptionActive,
|
isSubscriptionActive,
|
||||||
subscriptionMetadataSchema,
|
|
||||||
toDate,
|
toDate,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,6 @@ import type { Stripe } from "@rallly/billing";
|
||||||
import { stripe } from "@rallly/billing";
|
import { stripe } from "@rallly/billing";
|
||||||
import type { Prisma } from "@rallly/database";
|
import type { Prisma } from "@rallly/database";
|
||||||
import { 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) {
|
export function toDate(date: number) {
|
||||||
return new Date(date * 1000);
|
return new Date(date * 1000);
|
||||||
|
|
|
@ -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;
|
} as Adapter;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getDefaultSpace } from "@/features/spaces/queries";
|
||||||
import { prisma } from "@rallly/database";
|
import { prisma } from "@rallly/database";
|
||||||
import { posthog } from "@rallly/posthog/server";
|
import { posthog } from "@rallly/posthog/server";
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
@ -6,13 +7,9 @@ export const mergeGuestsIntoUser = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
guestIds: string[],
|
guestIds: string[],
|
||||||
) => {
|
) => {
|
||||||
const count = await prisma.user.count({
|
const space = await getDefaultSpace({ ownerId: userId });
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (count === 0) {
|
if (!space) {
|
||||||
console.warn(`User ${userId} not found`);
|
console.warn(`User ${userId} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -29,6 +26,7 @@ export const mergeGuestsIntoUser = async (
|
||||||
data: {
|
data: {
|
||||||
guestId: null,
|
guestId: null,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
spaceId: space.id,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { getDefaultSpace } from "@/features/spaces/queries";
|
||||||
import { getUser } from "@/features/user/queries";
|
import { getUser } from "@/features/user/queries";
|
||||||
import { auth } from "@/next-auth";
|
import { auth } from "@/next-auth";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
@ -34,3 +35,21 @@ export const requireAdmin = cache(async () => {
|
||||||
|
|
||||||
return user;
|
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;
|
||||||
|
});
|
||||||
|
|
38
apps/web/src/features/spaces/queries.ts
Normal file
38
apps/web/src/features/spaces/queries.ts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -2,8 +2,16 @@ import { z } from "zod";
|
||||||
|
|
||||||
export const subscriptionCheckoutMetadataSchema = z.object({
|
export const subscriptionCheckoutMetadataSchema = z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
|
spaceId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SubscriptionCheckoutMetadata = z.infer<
|
export type SubscriptionCheckoutMetadata = z.infer<
|
||||||
typeof subscriptionCheckoutMetadataSchema
|
typeof subscriptionCheckoutMetadataSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export const subscriptionMetadataSchema = z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
spaceId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SubscriptionMetadata = z.infer<typeof subscriptionMetadataSchema>;
|
||||||
|
|
|
@ -133,6 +133,11 @@ export const auth = router({
|
||||||
timeFormat: input.timeFormat,
|
timeFormat: input.timeFormat,
|
||||||
weekStart: input.weekStart,
|
weekStart: input.weekStart,
|
||||||
locale: input.locale,
|
locale: input.locale,
|
||||||
|
spaces: {
|
||||||
|
create: {
|
||||||
|
name: "Personal",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { z } from "zod";
|
||||||
import { moderateContent } from "@/features/moderation";
|
import { moderateContent } from "@/features/moderation";
|
||||||
import { getEmailClient } from "@/utils/emails";
|
import { getEmailClient } from "@/utils/emails";
|
||||||
|
|
||||||
|
import { getActiveSpace } from "@/auth/queries";
|
||||||
import { getTimeZoneAbbreviation } from "../../utils/date";
|
import { getTimeZoneAbbreviation } from "../../utils/date";
|
||||||
import {
|
import {
|
||||||
createRateLimitMiddleware,
|
createRateLimitMiddleware,
|
||||||
|
@ -180,6 +181,8 @@ export const polls = router({
|
||||||
const participantUrlId = nanoid();
|
const participantUrlId = nanoid();
|
||||||
const pollId = nanoid();
|
const pollId = nanoid();
|
||||||
|
|
||||||
|
const space = await getActiveSpace();
|
||||||
|
|
||||||
const poll = await prisma.poll.create({
|
const poll = await prisma.poll.create({
|
||||||
select: {
|
select: {
|
||||||
adminUrlId: true,
|
adminUrlId: true,
|
||||||
|
@ -228,6 +231,7 @@ export const polls = router({
|
||||||
disableComments: input.disableComments,
|
disableComments: input.disableComments,
|
||||||
hideScores: input.hideScores,
|
hideScores: input.hideScores,
|
||||||
requireParticipantEmail: input.requireParticipantEmail,
|
requireParticipantEmail: input.requireParticipantEmail,
|
||||||
|
spaceId: space?.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -551,6 +555,7 @@ export const polls = router({
|
||||||
title: true,
|
title: true,
|
||||||
location: true,
|
location: true,
|
||||||
description: true,
|
description: true,
|
||||||
|
spaceId: true,
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
|
@ -620,6 +625,13 @@ export const polls = router({
|
||||||
eventStart = eventStart.utc();
|
eventStart = eventStart.utc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!poll.spaceId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Poll has no space",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.poll.update({
|
await prisma.poll.update({
|
||||||
where: {
|
where: {
|
||||||
id: input.pollId,
|
id: input.pollId,
|
||||||
|
@ -635,6 +647,7 @@ export const polls = router({
|
||||||
location: poll.location,
|
location: poll.location,
|
||||||
timeZone: poll.timeZone,
|
timeZone: poll.timeZone,
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
|
spaceId: poll.spaceId,
|
||||||
allDay: option.duration === 0,
|
allDay: option.duration === 0,
|
||||||
status: "confirmed",
|
status: "confirmed",
|
||||||
invites: {
|
invites: {
|
||||||
|
|
|
@ -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")
|
periodEnd DateTime @map("period_end")
|
||||||
cancelAtPeriodEnd Boolean @default(false) @map("cancel_at_period_end")
|
cancelAtPeriodEnd Boolean @default(false) @map("cancel_at_period_end")
|
||||||
userId String @unique @map("user_id")
|
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")
|
@@map("subscriptions")
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ enum ScheduledEventInviteStatus {
|
||||||
model ScheduledEvent {
|
model ScheduledEvent {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String @map("user_id")
|
userId String @map("user_id")
|
||||||
|
spaceId String @map("space_id")
|
||||||
title String
|
title String
|
||||||
description String?
|
description String?
|
||||||
location String?
|
location String?
|
||||||
|
@ -31,6 +32,7 @@ model ScheduledEvent {
|
||||||
deletedAt DateTime? @map("deleted_at")
|
deletedAt DateTime? @map("deleted_at")
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
|
||||||
rescheduledDates RescheduledEventDate[]
|
rescheduledDates RescheduledEventDate[]
|
||||||
invites ScheduledEventInvite[]
|
invites ScheduledEventInvite[]
|
||||||
polls Poll[]
|
polls Poll[]
|
||||||
|
|
|
@ -46,6 +46,8 @@ model Poll {
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
votes Vote[]
|
votes Vote[]
|
||||||
views PollView[]
|
views PollView[]
|
||||||
|
space Space? @relation(fields: [spaceId], references: [id], onDelete: SetNull)
|
||||||
|
spaceId String? @map("space_id")
|
||||||
|
|
||||||
@@index([guestId])
|
@@index([guestId])
|
||||||
@@map("polls")
|
@@map("polls")
|
||||||
|
|
|
@ -57,6 +57,9 @@ model User {
|
||||||
participants Participant[]
|
participants Participant[]
|
||||||
paymentMethods PaymentMethod[]
|
paymentMethods PaymentMethod[]
|
||||||
subscription Subscription? @relation("UserToSubscription")
|
subscription Subscription? @relation("UserToSubscription")
|
||||||
|
|
||||||
|
spaces Space[] @relation("UserSpaces")
|
||||||
|
|
||||||
pollViews PollView[]
|
pollViews PollView[]
|
||||||
scheduledEvents ScheduledEvent[]
|
scheduledEvents ScheduledEvent[]
|
||||||
scheduledEventInvites ScheduledEventInvite[]
|
scheduledEventInvites ScheduledEventInvite[]
|
||||||
|
@ -64,6 +67,21 @@ model User {
|
||||||
@@map("users")
|
@@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 {
|
model VerificationToken {
|
||||||
identifier String @db.Citext
|
identifier String @db.Citext
|
||||||
token String @unique
|
token String @unique
|
||||||
|
|
|
@ -41,7 +41,13 @@ function generateDescription() {
|
||||||
return faker.helpers.arrayElement(descriptions);
|
return faker.helpers.arrayElement(descriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPollForUser(userId: string) {
|
async function createPollForUser({
|
||||||
|
userId,
|
||||||
|
spaceId,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
spaceId: string;
|
||||||
|
}) {
|
||||||
const duration = 60 * randInt(8);
|
const duration = 60 * randInt(8);
|
||||||
let cursor = dayjs().add(randInt(30), "day").second(0).minute(0);
|
let cursor = dayjs().add(randInt(30), "day").second(0).minute(0);
|
||||||
const numberOfOptions = randInt(5, 2); // Reduced for realism
|
const numberOfOptions = randInt(5, 2); // Reduced for realism
|
||||||
|
@ -62,6 +68,11 @@ async function createPollForUser(userId: string) {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
space: {
|
||||||
|
connect: {
|
||||||
|
id: spaceId,
|
||||||
|
},
|
||||||
|
},
|
||||||
status: faker.helpers.arrayElement(["live", "paused", "finalized"]),
|
status: faker.helpers.arrayElement(["live", "paused", "finalized"]),
|
||||||
timeZone: duration !== 0 ? "Europe/London" : undefined,
|
timeZone: duration !== 0 ? "Europe/London" : undefined,
|
||||||
options: {
|
options: {
|
||||||
|
@ -108,8 +119,18 @@ async function createPollForUser(userId: string) {
|
||||||
|
|
||||||
export async function seedPolls(userId: string) {
|
export async function seedPolls(userId: string) {
|
||||||
console.info("Seeding polls...");
|
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(() =>
|
const pollPromises = Array.from({ length: 20 }).map(() =>
|
||||||
createPollForUser(userId),
|
createPollForUser({ userId, spaceId: space.id }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(pollPromises);
|
await Promise.all(pollPromises);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import type { ScheduledEventInviteStatus } from "@prisma/client";
|
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 dayjs from "dayjs";
|
||||||
|
|
||||||
import { prisma } from "@rallly/database";
|
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 { title, description } = generateEventDetails();
|
||||||
const isAllDay = Math.random() < 0.3; // ~30% chance of being all-day
|
const isAllDay = Math.random() < 0.3; // ~30% chance of being all-day
|
||||||
|
|
||||||
|
@ -101,14 +107,20 @@ async function createScheduledEventForUser(userId: string) {
|
||||||
]);
|
]);
|
||||||
const timeZone = faker.address.timeZone();
|
const timeZone = faker.address.timeZone();
|
||||||
|
|
||||||
const data: Prisma.ScheduledEventCreateInput = {
|
await prisma.scheduledEvent.create({
|
||||||
|
data: {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
start: startTime, // Use correct model field name 'start'
|
start: startTime, // Use correct model field name 'start'
|
||||||
end: endTime, // Use correct model field name 'end'
|
end: endTime, // Use correct model field name 'end'
|
||||||
timeZone,
|
timeZone,
|
||||||
status, // Assign the randomly selected valid status
|
status, // Assign the randomly selected valid status
|
||||||
user: { connect: { id: userId } }, // Connect to existing user
|
user: {
|
||||||
|
connect: { id: userId },
|
||||||
|
}, // Connect to existing user
|
||||||
|
space: {
|
||||||
|
connect: { id: spaceId },
|
||||||
|
},
|
||||||
allDay: isAllDay,
|
allDay: isAllDay,
|
||||||
location: faker.datatype.boolean()
|
location: faker.datatype.boolean()
|
||||||
? faker.address.streetAddress()
|
? faker.address.streetAddress()
|
||||||
|
@ -127,15 +139,24 @@ async function createScheduledEventForUser(userId: string) {
|
||||||
]),
|
]),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
});
|
||||||
await prisma.scheduledEvent.create({ data });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function seedScheduledEvents(userId: string) {
|
export async function seedScheduledEvents(userId: string) {
|
||||||
console.info("Seeding scheduled events...");
|
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) =>
|
const eventPromises = Array.from({ length: 15 }).map((_, i) =>
|
||||||
createScheduledEventForUser(userId),
|
createScheduledEventForUser({ userId, spaceId: space.id }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(eventPromises);
|
await Promise.all(eventPromises);
|
||||||
|
|
|
@ -11,6 +11,12 @@ export async function seedUsers() {
|
||||||
name: "Dev User",
|
name: "Dev User",
|
||||||
email: "dev@rallly.co",
|
email: "dev@rallly.co",
|
||||||
timeZone: "America/New_York",
|
timeZone: "America/New_York",
|
||||||
|
spaces: {
|
||||||
|
create: {
|
||||||
|
id: "space-1",
|
||||||
|
name: "Personal",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,6 +27,12 @@ export async function seedUsers() {
|
||||||
id: "pro-user",
|
id: "pro-user",
|
||||||
name: "Pro User",
|
name: "Pro User",
|
||||||
email: "dev+pro@rallly.co",
|
email: "dev+pro@rallly.co",
|
||||||
|
spaces: {
|
||||||
|
create: {
|
||||||
|
id: "space-2",
|
||||||
|
name: "Personal",
|
||||||
|
},
|
||||||
|
},
|
||||||
subscription: {
|
subscription: {
|
||||||
create: {
|
create: {
|
||||||
id: "sub_123",
|
id: "sub_123",
|
||||||
|
@ -32,6 +44,7 @@ export async function seedUsers() {
|
||||||
priceId: "price_123",
|
priceId: "price_123",
|
||||||
periodStart: new Date(),
|
periodStart: new Date(),
|
||||||
periodEnd: dayjs().add(1, "month").toDate(),
|
periodEnd: dayjs().add(1, "month").toDate(),
|
||||||
|
spaceId: "space-2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue