From 8f6f34931b8a5160b778165947a15946d1ae507f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Fri, 21 Jan 2022 14:35:31 -0700 Subject: [PATCH] Allow choosing destination calendar per event type (#1514) --- .eslintignore | 1 + components/DestinationCalendarSelector.tsx | 92 +++++++ components/eventtype/CreateEventType.tsx | 77 +++--- .../integrations/CalendarListContainer.tsx | 82 +----- components/ui/Scheduler.tsx | 8 +- ee/components/stripe/Payment.tsx | 13 +- .../api/integrations/stripepayment/webhook.ts | 2 + lib/events/EventManager.ts | 4 + lib/integrations/calendar/constants/types.ts | 1 + .../calendar/interfaces/Calendar.ts | 2 +- .../calendar/services/BaseCalendarService.ts | 11 +- .../event-types/create-event-type.ts | 3 + .../event-types/delete-event-type.ts | 3 + .../event-types/update-event-type.ts | 3 + lib/types/event-type.ts | 7 + package.json | 9 +- pages/api/availability/eventtype.ts | 223 +--------------- pages/api/book/confirm.ts | 2 + pages/api/book/event.ts | 8 +- pages/api/cancel.ts | 4 +- pages/api/cron/bookingReminder.ts | 3 + pages/event-types/[type].tsx | 208 ++++++++------- prisma/schema.prisma | 13 +- prisma/zod-utils.ts | 11 + prisma/zod/attendee.ts | 27 ++ prisma/zod/availability.ts | 32 +++ prisma/zod/booking.ts | 65 +++++ prisma/zod/bookingreference.ts | 29 ++ prisma/zod/credential.ts | 34 +++ prisma/zod/dailyeventreference.ts | 26 ++ prisma/zod/destinationcalendar.ts | 39 +++ prisma/zod/eventtype.ts | 82 ++++++ prisma/zod/eventtypeCustom.ts | 17 ++ prisma/zod/eventtypecustominput.ts | 29 ++ prisma/zod/index.ts | 19 ++ prisma/zod/membership.ts | 29 ++ prisma/zod/payment.ts | 42 +++ prisma/zod/remindermail.ts | 12 + prisma/zod/resetpasswordrequest.ts | 11 + prisma/zod/schedule.ts | 37 +++ prisma/zod/selectedcalendar.ts | 25 ++ prisma/zod/team.ts | 30 +++ prisma/zod/user.ts | 93 +++++++ prisma/zod/verificationrequest.ts | 12 + prisma/zod/webhook.ts | 30 +++ server/routers/viewer.tsx | 90 +++---- server/routers/viewer/eventTypes.tsx | 249 ++++++++++++++++++ yarn.lock | 109 ++++++-- 48 files changed, 1448 insertions(+), 510 deletions(-) create mode 100644 components/DestinationCalendarSelector.tsx create mode 100644 prisma/zod-utils.ts create mode 100644 prisma/zod/attendee.ts create mode 100644 prisma/zod/availability.ts create mode 100644 prisma/zod/booking.ts create mode 100644 prisma/zod/bookingreference.ts create mode 100644 prisma/zod/credential.ts create mode 100644 prisma/zod/dailyeventreference.ts create mode 100644 prisma/zod/destinationcalendar.ts create mode 100644 prisma/zod/eventtype.ts create mode 100644 prisma/zod/eventtypeCustom.ts create mode 100644 prisma/zod/eventtypecustominput.ts create mode 100644 prisma/zod/index.ts create mode 100644 prisma/zod/membership.ts create mode 100644 prisma/zod/payment.ts create mode 100644 prisma/zod/remindermail.ts create mode 100644 prisma/zod/resetpasswordrequest.ts create mode 100644 prisma/zod/schedule.ts create mode 100644 prisma/zod/selectedcalendar.ts create mode 100644 prisma/zod/team.ts create mode 100644 prisma/zod/user.ts create mode 100644 prisma/zod/verificationrequest.ts create mode 100644 prisma/zod/webhook.ts create mode 100644 server/routers/viewer/eventTypes.tsx diff --git a/.eslintignore b/.eslintignore index 3c3629e647..7ca119ecc7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ node_modules +prisma/zod diff --git a/components/DestinationCalendarSelector.tsx b/components/DestinationCalendarSelector.tsx new file mode 100644 index 0000000000..ef9dd1f740 --- /dev/null +++ b/components/DestinationCalendarSelector.tsx @@ -0,0 +1,92 @@ +import React, { useEffect, useState } from "react"; +import Select from "react-select"; + +import { useLocale } from "@lib/hooks/useLocale"; +import { trpc } from "@lib/trpc"; + +import Button from "@components/ui/Button"; + +interface Props { + onChange: (value: { externalId: string; integration: string }) => void; + isLoading?: boolean; + hidePlaceholder?: boolean; + /** The external Id of the connected calendar */ + value: string | undefined; +} + +const DestinationCalendarSelector = ({ + onChange, + isLoading, + value, + hidePlaceholder, +}: Props): JSX.Element | null => { + const { t } = useLocale(); + const query = trpc.useQuery(["viewer.connectedCalendars"]); + const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null); + + useEffect(() => { + if (!selectedOption) { + const selected = query.data?.connectedCalendars + .map((connected) => connected.calendars ?? []) + .flat() + .find((cal) => cal.externalId === value); + + if (selected) { + setSelectedOption({ + value: `${selected.integration}:${selected.externalId}`, + label: selected.name || "", + }); + } + } + }, [query.data?.connectedCalendars, selectedOption, value]); + + if (!query.data?.connectedCalendars.length) { + return null; + } + const options = + query.data.connectedCalendars.map((selectedCalendar) => ({ + key: selectedCalendar.credentialId, + label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`, + options: (selectedCalendar.calendars ?? []).map((cal) => ({ + label: cal.name || "", + value: `${cal.integration}:${cal.externalId}`, + })), + })) ?? []; + return ( +
+ {/* There's no easy way to customize the displayed value for a Select, so we fake it. */} + {!hidePlaceholder && ( +
+ +
+ )} + }
{t("minutes")} diff --git a/components/integrations/CalendarListContainer.tsx b/components/integrations/CalendarListContainer.tsx index 23e7579ab8..65dad1b859 100644 --- a/components/integrations/CalendarListContainer.tsx +++ b/components/integrations/CalendarListContainer.tsx @@ -1,12 +1,12 @@ -import React, { Fragment, useState } from "react"; +import React, { Fragment } from "react"; import { useMutation } from "react-query"; -import Select from "react-select"; import { QueryCell } from "@lib/QueryCell"; import { useLocale } from "@lib/hooks/useLocale"; import showToast from "@lib/notification"; import { trpc } from "@lib/trpc"; +import DestinationCalendarSelector from "@components/DestinationCalendarSelector"; import { List } from "@components/List"; import { ShellSubHeading } from "@components/Shell"; import { Alert } from "@components/ui/Alert"; @@ -161,76 +161,6 @@ function ConnectedCalendarsList(props: Props) { ); } -function PrimaryCalendarSelector() { - const { t } = useLocale(); - const query = trpc.useQuery(["viewer.connectedCalendars"], { - suspense: true, - }); - const [selectedOption, setSelectedOption] = useState(() => { - const selected = query.data?.connectedCalendars - .map((connected) => connected.calendars ?? []) - .flat() - .find((cal) => cal.externalId === query.data.destinationCalendar?.externalId); - - if (!selected) { - return null; - } - - return { - value: `${selected.integration}:${selected.externalId}`, - label: selected.name, - }; - }); - - const mutation = trpc.useMutation("viewer.setUserDestinationCalendar"); - - if (!query.data?.connectedCalendars.length) { - return null; - } - const options = - query.data.connectedCalendars.map((selectedCalendar) => ({ - key: selectedCalendar.credentialId, - label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`, - options: (selectedCalendar.calendars ?? []).map((cal) => ({ - label: cal.name || "", - value: `${cal.integration}:${cal.externalId}`, - })), - })) ?? []; - return ( -
- {/* There's no easy way to customize the displayed value for a Select, so we fake it. */} -
- -
- { - formMethods.setValue("periodDays", Number(e.target.value)); - }} /> setRequirePayment(event.target.checked)} + onChange={(event) => { + setRequirePayment(event.target.checked); + if (!event.target.checked) { + formMethods.setValue("price", 0); + } + }} id="requirePayment" name="requirePayment" type="checkbox" @@ -1063,16 +1069,25 @@ const EventTypePage = (props: inferSSRProps) => {
- 0 ? eventType.price / 100.0 : undefined - } - {...formMethods.register("price")} + ( + { + field.onChange(e.target.valueAsNumber * 100); + }} + value={field.value > 0 ? field.value / 100 : 0} + /> + )} />
@@ -1103,7 +1118,9 @@ const EventTypePage = (props: inferSSRProps) => { - +
@@ -1111,14 +1128,14 @@ const EventTypePage = (props: inferSSRProps) => {
( { - formMethods.setValue("isHidden", isChecked); + formMethods.setValue("hidden", isChecked); }} label={t("hide_event_type")} /> @@ -1397,6 +1414,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => userId: true, price: true, currency: true, + destinationCalendar: true, }, }); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a6f7355434..0fdf55e269 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,6 +10,13 @@ generator client { provider = "prisma-client-js" } +generator zod { + provider = "zod-prisma" + output = "./zod" + imports = "./zod-utils" + relationModel = "default" +} + enum SchedulingType { ROUND_ROBIN @map("roundRobin") COLLECTIVE @map("collective") @@ -23,10 +30,13 @@ enum PeriodType { model EventType { id Int @id @default(autoincrement()) + /// @zod.nonempty() title String + /// @zod.custom(imports.eventTypeSlug) slug String description String? position Int @default(0) + /// @zod.custom(imports.eventTypeLocations) locations Json? length Int hidden Boolean @default(false) @@ -36,7 +46,7 @@ model EventType { teamId Int? bookings Booking[] availability Availability[] - destinationCalendar DestinationCalendar[] + destinationCalendar DestinationCalendar? eventName String? customInputs EventTypeCustomInput[] timeZone String? @@ -93,6 +103,7 @@ model User { id Int @id @default(autoincrement()) username String? @unique name String? + /// @zod.email() email String @unique emailVerified DateTime? password String? diff --git a/prisma/zod-utils.ts b/prisma/zod-utils.ts new file mode 100644 index 0000000000..22a7fd6f4d --- /dev/null +++ b/prisma/zod-utils.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +import { LocationType } from "@lib/location"; + +export const eventTypeLocations = z.array( + z.object({ type: z.nativeEnum(LocationType), address: z.string().optional() }) +); + +export const eventTypeSlug = z.string().transform((val) => val.trim()); +export const stringToDate = z.string().transform((a) => new Date(a)); +export const stringOrNumber = z.union([z.string().transform((v) => parseInt(v, 10)), z.number().int()]); diff --git a/prisma/zod/attendee.ts b/prisma/zod/attendee.ts new file mode 100644 index 0000000000..05e2bdcded --- /dev/null +++ b/prisma/zod/attendee.ts @@ -0,0 +1,27 @@ +import * as z from "zod"; + +import * as imports from "../zod-utils"; +import { CompleteBooking, BookingModel } from "./index"; + +export const _AttendeeModel = z.object({ + id: z.number().int(), + email: z.string(), + name: z.string(), + timeZone: z.string(), + bookingId: z.number().int().nullish(), +}); + +export interface CompleteAttendee extends z.infer { + booking?: CompleteBooking | null; +} + +/** + * AttendeeModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const AttendeeModel: z.ZodSchema = z.lazy(() => + _AttendeeModel.extend({ + booking: BookingModel.nullish(), + }) +); diff --git a/prisma/zod/availability.ts b/prisma/zod/availability.ts new file mode 100644 index 0000000000..881062a5cd --- /dev/null +++ b/prisma/zod/availability.ts @@ -0,0 +1,32 @@ +import * as z from "zod"; + +import * as imports from "../zod-utils"; +import { CompleteUser, UserModel, CompleteEventType, EventTypeModel } from "./index"; + +export const _AvailabilityModel = z.object({ + id: z.number().int(), + label: z.string().nullish(), + userId: z.number().int().nullish(), + eventTypeId: z.number().int().nullish(), + days: z.number().int().array(), + startTime: z.date(), + endTime: z.date(), + date: z.date().nullish(), +}); + +export interface CompleteAvailability extends z.infer { + user?: CompleteUser | null; + eventType?: CompleteEventType | null; +} + +/** + * AvailabilityModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const AvailabilityModel: z.ZodSchema = z.lazy(() => + _AvailabilityModel.extend({ + user: UserModel.nullish(), + eventType: EventTypeModel.nullish(), + }) +); diff --git a/prisma/zod/booking.ts b/prisma/zod/booking.ts new file mode 100644 index 0000000000..c656a54e9e --- /dev/null +++ b/prisma/zod/booking.ts @@ -0,0 +1,65 @@ +import * as z from "zod"; + +import { BookingStatus } from "../../node_modules/@prisma/client"; +import * as imports from "../zod-utils"; +import { + CompleteUser, + UserModel, + CompleteBookingReference, + BookingReferenceModel, + CompleteEventType, + EventTypeModel, + CompleteAttendee, + AttendeeModel, + CompleteDailyEventReference, + DailyEventReferenceModel, + CompletePayment, + PaymentModel, + CompleteDestinationCalendar, + DestinationCalendarModel, +} from "./index"; + +export const _BookingModel = z.object({ + id: z.number().int(), + uid: z.string(), + userId: z.number().int().nullish(), + eventTypeId: z.number().int().nullish(), + title: z.string(), + description: z.string().nullish(), + startTime: z.date(), + endTime: z.date(), + location: z.string().nullish(), + createdAt: z.date(), + updatedAt: z.date().nullish(), + confirmed: z.boolean(), + rejected: z.boolean(), + status: z.nativeEnum(BookingStatus), + paid: z.boolean(), +}); + +export interface CompleteBooking extends z.infer { + user?: CompleteUser | null; + references: CompleteBookingReference[]; + eventType?: CompleteEventType | null; + attendees: CompleteAttendee[]; + dailyRef?: CompleteDailyEventReference | null; + payment: CompletePayment[]; + destinationCalendar?: CompleteDestinationCalendar | null; +} + +/** + * BookingModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const BookingModel: z.ZodSchema = z.lazy(() => + _BookingModel.extend({ + user: UserModel.nullish(), + references: BookingReferenceModel.array(), + eventType: EventTypeModel.nullish(), + attendees: AttendeeModel.array(), + dailyRef: DailyEventReferenceModel.nullish(), + payment: PaymentModel.array(), + destinationCalendar: DestinationCalendarModel.nullish(), + }) +); diff --git a/prisma/zod/bookingreference.ts b/prisma/zod/bookingreference.ts new file mode 100644 index 0000000000..e27651bc34 --- /dev/null +++ b/prisma/zod/bookingreference.ts @@ -0,0 +1,29 @@ +import * as z from "zod"; + +import * as imports from "../zod-utils"; +import { CompleteBooking, BookingModel } from "./index"; + +export const _BookingReferenceModel = z.object({ + id: z.number().int(), + type: z.string(), + uid: z.string(), + meetingId: z.string().nullish(), + meetingPassword: z.string().nullish(), + meetingUrl: z.string().nullish(), + bookingId: z.number().int().nullish(), +}); + +export interface CompleteBookingReference extends z.infer { + booking?: CompleteBooking | null; +} + +/** + * BookingReferenceModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const BookingReferenceModel: z.ZodSchema = z.lazy(() => + _BookingReferenceModel.extend({ + booking: BookingModel.nullish(), + }) +); diff --git a/prisma/zod/credential.ts b/prisma/zod/credential.ts new file mode 100644 index 0000000000..37aeeb4a67 --- /dev/null +++ b/prisma/zod/credential.ts @@ -0,0 +1,34 @@ +import * as z from "zod"; + +import * as imports from "../zod-utils"; +import { CompleteUser, UserModel } from "./index"; + +// Helper schema for JSON fields +type Literal = boolean | number | string; +type Json = Literal | { [key: string]: Json } | Json[]; +const literalSchema = z.union([z.string(), z.number(), z.boolean()]); +const jsonSchema: z.ZodSchema = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); + +export const _CredentialModel = z.object({ + id: z.number().int(), + type: z.string(), + key: jsonSchema, + userId: z.number().int().nullish(), +}); + +export interface CompleteCredential extends z.infer { + user?: CompleteUser | null; +} + +/** + * CredentialModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const CredentialModel: z.ZodSchema = z.lazy(() => + _CredentialModel.extend({ + user: UserModel.nullish(), + }) +); diff --git a/prisma/zod/dailyeventreference.ts b/prisma/zod/dailyeventreference.ts new file mode 100644 index 0000000000..925b1ac1cb --- /dev/null +++ b/prisma/zod/dailyeventreference.ts @@ -0,0 +1,26 @@ +import * as z from "zod"; + +import * as imports from "../zod-utils"; +import { CompleteBooking, BookingModel } from "./index"; + +export const _DailyEventReferenceModel = z.object({ + id: z.number().int(), + dailyurl: z.string(), + dailytoken: z.string(), + bookingId: z.number().int().nullish(), +}); + +export interface CompleteDailyEventReference extends z.infer { + booking?: CompleteBooking | null; +} + +/** + * DailyEventReferenceModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const DailyEventReferenceModel: z.ZodSchema = z.lazy(() => + _DailyEventReferenceModel.extend({ + booking: BookingModel.nullish(), + }) +); diff --git a/prisma/zod/destinationcalendar.ts b/prisma/zod/destinationcalendar.ts new file mode 100644 index 0000000000..f9538ec85f --- /dev/null +++ b/prisma/zod/destinationcalendar.ts @@ -0,0 +1,39 @@ +import * as z from "zod"; + +import * as imports from "../zod-utils"; +import { + CompleteUser, + UserModel, + CompleteBooking, + BookingModel, + CompleteEventType, + EventTypeModel, +} from "./index"; + +export const _DestinationCalendarModel = z.object({ + id: z.number().int(), + integration: z.string(), + externalId: z.string(), + userId: z.number().int().nullish(), + bookingId: z.number().int().nullish(), + eventTypeId: z.number().int().nullish(), +}); + +export interface CompleteDestinationCalendar extends z.infer { + user?: CompleteUser | null; + booking?: CompleteBooking | null; + eventType?: CompleteEventType | null; +} + +/** + * DestinationCalendarModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const DestinationCalendarModel: z.ZodSchema = z.lazy(() => + _DestinationCalendarModel.extend({ + user: UserModel.nullish(), + booking: BookingModel.nullish(), + eventType: EventTypeModel.nullish(), + }) +); diff --git a/prisma/zod/eventtype.ts b/prisma/zod/eventtype.ts new file mode 100644 index 0000000000..95e1c7f57a --- /dev/null +++ b/prisma/zod/eventtype.ts @@ -0,0 +1,82 @@ +import * as z from "zod"; + +import { PeriodType, SchedulingType } from "../../node_modules/@prisma/client"; +import * as imports from "../zod-utils"; +import { + CompleteUser, + UserModel, + CompleteTeam, + TeamModel, + CompleteBooking, + BookingModel, + CompleteAvailability, + AvailabilityModel, + CompleteDestinationCalendar, + DestinationCalendarModel, + CompleteEventTypeCustomInput, + EventTypeCustomInputModel, + CompleteSchedule, + ScheduleModel, +} from "./index"; + +// Helper schema for JSON fields +type Literal = boolean | number | string; +type Json = Literal | { [key: string]: Json } | Json[]; +const literalSchema = z.union([z.string(), z.number(), z.boolean()]); +const jsonSchema: z.ZodSchema = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); + +export const _EventTypeModel = z.object({ + id: z.number().int(), + title: z.string().nonempty(), + slug: imports.eventTypeSlug, + description: z.string().nullish(), + position: z.number().int(), + locations: imports.eventTypeLocations, + length: z.number().int(), + hidden: z.boolean(), + userId: z.number().int().nullish(), + teamId: z.number().int().nullish(), + eventName: z.string().nullish(), + timeZone: z.string().nullish(), + periodType: z.nativeEnum(PeriodType), + periodStartDate: z.date().nullish(), + periodEndDate: z.date().nullish(), + periodDays: z.number().int().nullish(), + periodCountCalendarDays: z.boolean().nullish(), + requiresConfirmation: z.boolean(), + disableGuests: z.boolean(), + minimumBookingNotice: z.number().int(), + schedulingType: z.nativeEnum(SchedulingType).nullish(), + price: z.number().int(), + currency: z.string(), + slotInterval: z.number().int().nullish(), +}); + +export interface CompleteEventType extends z.infer { + users: CompleteUser[]; + team?: CompleteTeam | null; + bookings: CompleteBooking[]; + availability: CompleteAvailability[]; + destinationCalendar?: CompleteDestinationCalendar | null; + customInputs: CompleteEventTypeCustomInput[]; + Schedule: CompleteSchedule[]; +} + +/** + * EventTypeModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const EventTypeModel: z.ZodSchema = z.lazy(() => + _EventTypeModel.extend({ + users: UserModel.array(), + team: TeamModel.nullish(), + bookings: BookingModel.array(), + availability: AvailabilityModel.array(), + destinationCalendar: DestinationCalendarModel.nullish(), + customInputs: EventTypeCustomInputModel.array(), + Schedule: ScheduleModel.array(), + }) +); diff --git a/prisma/zod/eventtypeCustom.ts b/prisma/zod/eventtypeCustom.ts new file mode 100644 index 0000000000..1629590765 --- /dev/null +++ b/prisma/zod/eventtypeCustom.ts @@ -0,0 +1,17 @@ +import { _EventTypeModel } from "prisma/zod"; + +const createEventTypeBaseInput = _EventTypeModel + .pick({ + title: true, + slug: true, + description: true, + length: true, + teamId: true, + schedulingType: true, + }) + .refine((data) => (data.teamId ? data.teamId && data.schedulingType : true), { + path: ["schedulingType"], + message: "You must select a scheduling type for team events", + }); + +export const createEventTypeInput = createEventTypeBaseInput; diff --git a/prisma/zod/eventtypecustominput.ts b/prisma/zod/eventtypecustominput.ts new file mode 100644 index 0000000000..41a3b3451c --- /dev/null +++ b/prisma/zod/eventtypecustominput.ts @@ -0,0 +1,29 @@ +import * as z from "zod"; + +import { EventTypeCustomInputType } from "../../node_modules/@prisma/client"; +import * as imports from "../zod-utils"; +import { CompleteEventType, EventTypeModel } from "./index"; + +export const _EventTypeCustomInputModel = z.object({ + id: z.number().int(), + eventTypeId: z.number().int(), + label: z.string(), + type: z.nativeEnum(EventTypeCustomInputType), + required: z.boolean(), + placeholder: z.string(), +}); + +export interface CompleteEventTypeCustomInput extends z.infer { + eventType: CompleteEventType; +} + +/** + * EventTypeCustomInputModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const EventTypeCustomInputModel: z.ZodSchema = z.lazy(() => + _EventTypeCustomInputModel.extend({ + eventType: EventTypeModel, + }) +); diff --git a/prisma/zod/index.ts b/prisma/zod/index.ts new file mode 100644 index 0000000000..508f41f708 --- /dev/null +++ b/prisma/zod/index.ts @@ -0,0 +1,19 @@ +export * from "./eventtype"; +export * from "./credential"; +export * from "./destinationcalendar"; +export * from "./user"; +export * from "./team"; +export * from "./membership"; +export * from "./verificationrequest"; +export * from "./bookingreference"; +export * from "./attendee"; +export * from "./dailyeventreference"; +export * from "./booking"; +export * from "./schedule"; +export * from "./availability"; +export * from "./selectedcalendar"; +export * from "./eventtypecustominput"; +export * from "./resetpasswordrequest"; +export * from "./remindermail"; +export * from "./payment"; +export * from "./webhook"; diff --git a/prisma/zod/membership.ts b/prisma/zod/membership.ts new file mode 100644 index 0000000000..177ccad4ef --- /dev/null +++ b/prisma/zod/membership.ts @@ -0,0 +1,29 @@ +import * as z from "zod"; + +import { MembershipRole } from "../../node_modules/@prisma/client"; +import * as imports from "../zod-utils"; +import { CompleteTeam, TeamModel, CompleteUser, UserModel } from "./index"; + +export const _MembershipModel = z.object({ + teamId: z.number().int(), + userId: z.number().int(), + accepted: z.boolean(), + role: z.nativeEnum(MembershipRole), +}); + +export interface CompleteMembership extends z.infer { + team: CompleteTeam; + user: CompleteUser; +} + +/** + * MembershipModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const MembershipModel: z.ZodSchema = z.lazy(() => + _MembershipModel.extend({ + team: TeamModel, + user: UserModel, + }) +); diff --git a/prisma/zod/payment.ts b/prisma/zod/payment.ts new file mode 100644 index 0000000000..f49fc148a0 --- /dev/null +++ b/prisma/zod/payment.ts @@ -0,0 +1,42 @@ +import * as z from "zod"; + +import { PaymentType } from "../../node_modules/@prisma/client"; +import * as imports from "../zod-utils"; +import { CompleteBooking, BookingModel } from "./index"; + +// Helper schema for JSON fields +type Literal = boolean | number | string; +type Json = Literal | { [key: string]: Json } | Json[]; +const literalSchema = z.union([z.string(), z.number(), z.boolean()]); +const jsonSchema: z.ZodSchema = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); + +export const _PaymentModel = z.object({ + id: z.number().int(), + uid: z.string(), + type: z.nativeEnum(PaymentType), + bookingId: z.number().int(), + amount: z.number().int(), + fee: z.number().int(), + currency: z.string(), + success: z.boolean(), + refunded: z.boolean(), + data: jsonSchema, + externalId: z.string(), +}); + +export interface CompletePayment extends z.infer { + booking?: CompleteBooking | null; +} + +/** + * PaymentModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const PaymentModel: z.ZodSchema = z.lazy(() => + _PaymentModel.extend({ + booking: BookingModel.nullish(), + }) +); diff --git a/prisma/zod/remindermail.ts b/prisma/zod/remindermail.ts new file mode 100644 index 0000000000..c705e96573 --- /dev/null +++ b/prisma/zod/remindermail.ts @@ -0,0 +1,12 @@ +import * as z from "zod"; + +import { ReminderType } from "../../node_modules/@prisma/client"; +import * as imports from "../zod-utils"; + +export const _ReminderMailModel = z.object({ + id: z.number().int(), + referenceId: z.number().int(), + reminderType: z.nativeEnum(ReminderType), + elapsedMinutes: z.number().int(), + createdAt: z.date(), +}); diff --git a/prisma/zod/resetpasswordrequest.ts b/prisma/zod/resetpasswordrequest.ts new file mode 100644 index 0000000000..89e481d996 --- /dev/null +++ b/prisma/zod/resetpasswordrequest.ts @@ -0,0 +1,11 @@ +import * as z from "zod"; + +import * as imports from "../zod-utils"; + +export const _ResetPasswordRequestModel = z.object({ + id: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + email: z.string(), + expires: z.date(), +}); diff --git a/prisma/zod/schedule.ts b/prisma/zod/schedule.ts new file mode 100644 index 0000000000..c822393331 --- /dev/null +++ b/prisma/zod/schedule.ts @@ -0,0 +1,37 @@ +import * as z from "zod"; + +import * as imports from "../zod-utils"; +import { CompleteUser, UserModel, CompleteEventType, EventTypeModel } from "./index"; + +// Helper schema for JSON fields +type Literal = boolean | number | string; +type Json = Literal | { [key: string]: Json } | Json[]; +const literalSchema = z.union([z.string(), z.number(), z.boolean()]); +const jsonSchema: z.ZodSchema = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); + +export const _ScheduleModel = z.object({ + id: z.number().int(), + userId: z.number().int().nullish(), + eventTypeId: z.number().int().nullish(), + title: z.string().nullish(), + freeBusyTimes: jsonSchema, +}); + +export interface CompleteSchedule extends z.infer { + user?: CompleteUser | null; + eventType?: CompleteEventType | null; +} + +/** + * ScheduleModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const ScheduleModel: z.ZodSchema = z.lazy(() => + _ScheduleModel.extend({ + user: UserModel.nullish(), + eventType: EventTypeModel.nullish(), + }) +); diff --git a/prisma/zod/selectedcalendar.ts b/prisma/zod/selectedcalendar.ts new file mode 100644 index 0000000000..0245a1ed90 --- /dev/null +++ b/prisma/zod/selectedcalendar.ts @@ -0,0 +1,25 @@ +import * as z from "zod"; + +import * as imports from "../zod-utils"; +import { CompleteUser, UserModel } from "./index"; + +export const _SelectedCalendarModel = z.object({ + userId: z.number().int(), + integration: z.string(), + externalId: z.string(), +}); + +export interface CompleteSelectedCalendar extends z.infer { + user: CompleteUser; +} + +/** + * SelectedCalendarModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const SelectedCalendarModel: z.ZodSchema = z.lazy(() => + _SelectedCalendarModel.extend({ + user: UserModel, + }) +); diff --git a/prisma/zod/team.ts b/prisma/zod/team.ts new file mode 100644 index 0000000000..53368aace8 --- /dev/null +++ b/prisma/zod/team.ts @@ -0,0 +1,30 @@ +import * as z from "zod"; + +import * as imports from "../zod-utils"; +import { CompleteMembership, MembershipModel, CompleteEventType, EventTypeModel } from "./index"; + +export const _TeamModel = z.object({ + id: z.number().int(), + name: z.string().nullish(), + slug: z.string().nullish(), + logo: z.string().nullish(), + bio: z.string().nullish(), + hideBranding: z.boolean(), +}); + +export interface CompleteTeam extends z.infer { + members: CompleteMembership[]; + eventTypes: CompleteEventType[]; +} + +/** + * TeamModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const TeamModel: z.ZodSchema = z.lazy(() => + _TeamModel.extend({ + members: MembershipModel.array(), + eventTypes: EventTypeModel.array(), + }) +); diff --git a/prisma/zod/user.ts b/prisma/zod/user.ts new file mode 100644 index 0000000000..ca050096d3 --- /dev/null +++ b/prisma/zod/user.ts @@ -0,0 +1,93 @@ +import * as z from "zod"; + +import { IdentityProvider, UserPlan } from "../../node_modules/@prisma/client"; +import * as imports from "../zod-utils"; +import { + CompleteEventType, + EventTypeModel, + CompleteCredential, + CredentialModel, + CompleteMembership, + MembershipModel, + CompleteBooking, + BookingModel, + CompleteAvailability, + AvailabilityModel, + CompleteSelectedCalendar, + SelectedCalendarModel, + CompleteSchedule, + ScheduleModel, + CompleteWebhook, + WebhookModel, + CompleteDestinationCalendar, + DestinationCalendarModel, +} from "./index"; + +// Helper schema for JSON fields +type Literal = boolean | number | string; +type Json = Literal | { [key: string]: Json } | Json[]; +const literalSchema = z.union([z.string(), z.number(), z.boolean()]); +const jsonSchema: z.ZodSchema = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); + +export const _UserModel = z.object({ + id: z.number().int(), + username: z.string().nullish(), + name: z.string().nullish(), + email: z.string().email(), + emailVerified: z.date().nullish(), + password: z.string().nullish(), + bio: z.string().nullish(), + avatar: z.string().nullish(), + timeZone: z.string(), + weekStart: z.string(), + startTime: z.number().int(), + endTime: z.number().int(), + bufferTime: z.number().int(), + hideBranding: z.boolean(), + theme: z.string().nullish(), + createdDate: z.date(), + completedOnboarding: z.boolean(), + locale: z.string().nullish(), + twoFactorSecret: z.string().nullish(), + twoFactorEnabled: z.boolean(), + identityProvider: z.nativeEnum(IdentityProvider), + identityProviderId: z.string().nullish(), + invitedTo: z.number().int().nullish(), + plan: z.nativeEnum(UserPlan), + brandColor: z.string(), + away: z.boolean(), + metadata: jsonSchema, +}); + +export interface CompleteUser extends z.infer { + eventTypes: CompleteEventType[]; + credentials: CompleteCredential[]; + teams: CompleteMembership[]; + bookings: CompleteBooking[]; + availability: CompleteAvailability[]; + selectedCalendars: CompleteSelectedCalendar[]; + Schedule: CompleteSchedule[]; + webhooks: CompleteWebhook[]; + destinationCalendar?: CompleteDestinationCalendar | null; +} + +/** + * UserModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const UserModel: z.ZodSchema = z.lazy(() => + _UserModel.extend({ + eventTypes: EventTypeModel.array(), + credentials: CredentialModel.array(), + teams: MembershipModel.array(), + bookings: BookingModel.array(), + availability: AvailabilityModel.array(), + selectedCalendars: SelectedCalendarModel.array(), + Schedule: ScheduleModel.array(), + webhooks: WebhookModel.array(), + destinationCalendar: DestinationCalendarModel.nullish(), + }) +); diff --git a/prisma/zod/verificationrequest.ts b/prisma/zod/verificationrequest.ts new file mode 100644 index 0000000000..b5c00b5fd1 --- /dev/null +++ b/prisma/zod/verificationrequest.ts @@ -0,0 +1,12 @@ +import * as z from "zod"; + +import * as imports from "../zod-utils"; + +export const _VerificationRequestModel = z.object({ + id: z.number().int(), + identifier: z.string(), + token: z.string(), + expires: z.date(), + createdAt: z.date(), + updatedAt: z.date(), +}); diff --git a/prisma/zod/webhook.ts b/prisma/zod/webhook.ts new file mode 100644 index 0000000000..4077ca1a56 --- /dev/null +++ b/prisma/zod/webhook.ts @@ -0,0 +1,30 @@ +import * as z from "zod"; + +import { WebhookTriggerEvents } from "../../node_modules/@prisma/client"; +import * as imports from "../zod-utils"; +import { CompleteUser, UserModel } from "./index"; + +export const _WebhookModel = z.object({ + id: z.string(), + userId: z.number().int(), + subscriberUrl: z.string(), + payloadTemplate: z.string().nullish(), + createdAt: z.date(), + active: z.boolean(), + eventTriggers: z.nativeEnum(WebhookTriggerEvents).array(), +}); + +export interface CompleteWebhook extends z.infer { + user: CompleteUser; +} + +/** + * WebhookModel contains all relations on your model in addition to the scalars + * + * NOTE: Lazy required in case of potential circular dependencies within schema + */ +export const WebhookModel: z.ZodSchema = z.lazy(() => + _WebhookModel.extend({ + user: UserModel, + }) +); diff --git a/server/routers/viewer.tsx b/server/routers/viewer.tsx index ff675d0351..c4d6fb8ea1 100644 --- a/server/routers/viewer.tsx +++ b/server/routers/viewer.tsx @@ -20,6 +20,7 @@ import { import slugify from "@lib/slugify"; import { Schedule } from "@lib/types/schedule"; +import { eventTypesRouter } from "@server/routers/viewer/eventTypes"; import { TRPCError } from "@trpc/server"; import { createProtectedRouter, createRouter } from "../createRouter"; @@ -61,45 +62,27 @@ const publicViewerRouter = createRouter() // routes only available to authenticated users const loggedInViewerRouter = createProtectedRouter() .query("me", { - resolve({ ctx }) { - const { - // pick only the part we want to expose in the API - id, - name, - username, - email, - startTime, - endTime, - bufferTime, - locale, - avatar, - createdDate, - completedOnboarding, - twoFactorEnabled, - identityProvider, - brandColor, - plan, - away, - } = ctx.user; - const me = { - id, - name, - username, - email, - startTime, - endTime, - bufferTime, - locale, - avatar, - createdDate, - completedOnboarding, - twoFactorEnabled, - identityProvider, - brandColor, - plan, - away, + resolve({ ctx: { user } }) { + // Destructuring here only makes it more illegible + // pick only the part we want to expose in the API + return { + id: user.id, + name: user.name, + username: user.username, + email: user.email, + startTime: user.startTime, + endTime: user.endTime, + bufferTime: user.bufferTime, + locale: user.locale, + avatar: user.avatar, + createdDate: user.createdDate, + completedOnboarding: user.completedOnboarding, + twoFactorEnabled: user.twoFactorEnabled, + identityProvider: user.identityProvider, + brandColor: user.brandColor, + plan: user.plan, + away: user.away, }; - return me; }, }) .mutation("deleteMe", { @@ -442,34 +425,40 @@ const loggedInViewerRouter = createProtectedRouter() }; }, }) - .mutation("setUserDestinationCalendar", { + .mutation("setDestinationCalendar", { input: z.object({ integration: z.string(), externalId: z.string(), + eventTypeId: z.number().optional(), + bookingId: z.number().optional(), }), async resolve({ ctx, input }) { const { user } = ctx; - const userId = ctx.user.id; + const { integration, externalId, eventTypeId, bookingId } = input; const calendarCredentials = getCalendarCredentials(user.credentials, user.id); const connectedCalendars = await getConnectedCalendars(calendarCredentials, user.selectedCalendars); const allCals = connectedCalendars.map((cal) => cal.calendars ?? []).flat(); - if ( - !allCals.find((cal) => cal.externalId === input.externalId && cal.integration === input.integration) - ) { + if (!allCals.find((cal) => cal.externalId === externalId && cal.integration === integration)) { throw new TRPCError({ code: "BAD_REQUEST", message: `Could not find calendar ${input.externalId}` }); } + + let where; + + if (eventTypeId) where = { eventTypeId }; + else if (bookingId) where = { bookingId }; + else where = { userId: user.id }; + await ctx.prisma.destinationCalendar.upsert({ - where: { - userId, - }, + where, update: { - ...input, - userId, + integration, + externalId, }, create: { - ...input, - userId, + ...where, + integration, + externalId, }, }); }, @@ -782,5 +771,6 @@ const loggedInViewerRouter = createProtectedRouter() export const viewerRouter = createRouter() .merge(publicViewerRouter) .merge(loggedInViewerRouter) + .merge("eventTypes.", eventTypesRouter) .merge("teams.", viewerTeamsRouter) .merge("webhook.", webhookRouter); diff --git a/server/routers/viewer/eventTypes.tsx b/server/routers/viewer/eventTypes.tsx new file mode 100644 index 0000000000..6bf48a88aa --- /dev/null +++ b/server/routers/viewer/eventTypes.tsx @@ -0,0 +1,249 @@ +import { EventTypeCustomInput, MembershipRole, PeriodType, Prisma } from "@prisma/client"; +import { + _AvailabilityModel, + _DestinationCalendarModel, + _EventTypeCustomInputModel, + _EventTypeModel, +} from "prisma/zod"; +import { stringOrNumber } from "prisma/zod-utils"; +import { createEventTypeInput } from "prisma/zod/eventtypeCustom"; +import { z } from "zod"; + +import { createProtectedRouter } from "@server/createRouter"; +import { viewerRouter } from "@server/routers/viewer"; +import { TRPCError } from "@trpc/server"; + +function isPeriodType(keyInput: string): keyInput is PeriodType { + return Object.keys(PeriodType).includes(keyInput); +} + +function handlePeriodType(periodType: string | undefined): PeriodType | undefined { + if (typeof periodType !== "string") return undefined; + const passedPeriodType = periodType.toUpperCase(); + if (!isPeriodType(passedPeriodType)) return undefined; + return PeriodType[passedPeriodType]; +} + +function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: number) { + const cInputsIdsToDelete = customInputs.filter((input) => input.id > 0).map((e) => e.id); + const cInputsToCreate = customInputs + .filter((input) => input.id < 0) + .map((input) => ({ + type: input.type, + label: input.label, + required: input.required, + placeholder: input.placeholder, + })); + const cInputsToUpdate = customInputs + .filter((input) => input.id > 0) + .map((input) => ({ + data: { + type: input.type, + label: input.label, + required: input.required, + placeholder: input.placeholder, + }, + where: { + id: input.id, + }, + })); + + return { + deleteMany: { + eventTypeId, + NOT: { + id: { in: cInputsIdsToDelete }, + }, + }, + createMany: { + data: cInputsToCreate, + }, + update: cInputsToUpdate, + }; +} + +const AvailabilityInput = _AvailabilityModel.pick({ + days: true, + startTime: true, + endTime: true, +}); + +const EventTypeUpdateInput = _EventTypeModel + /** Optional fields */ + .extend({ + availability: z + .object({ + openingHours: z.array(AvailabilityInput).optional(), + dateOverrides: z.array(AvailabilityInput).optional(), + }) + .optional(), + customInputs: z.array(_EventTypeCustomInputModel), + destinationCalendar: _DestinationCalendarModel.pick({ + integration: true, + externalId: true, + }), + users: z.array(stringOrNumber).optional(), + }) + .partial() + .merge( + _EventTypeModel + /** Required fields */ + .pick({ + id: true, + }) + ); + +export const eventTypesRouter = createProtectedRouter() + .query("list", { + async resolve({ ctx }) { + return await ctx.prisma.webhook.findMany({ + where: { + userId: ctx.user.id, + }, + }); + }, + }) + .mutation("create", { + input: createEventTypeInput, + async resolve({ ctx, input }) { + const { schedulingType, teamId, ...rest } = input; + const data: Prisma.EventTypeCreateInput = { + ...rest, + users: { + connect: { + id: ctx.user.id, + }, + }, + }; + + if (teamId && schedulingType) { + data.team = { + connect: { + id: teamId, + }, + }; + data.schedulingType = schedulingType; + } + + const eventType = await ctx.prisma.eventType.create({ data }); + + return { eventType }; + }, + }) + // Prevent non-owners to update/delete a team event + .middleware(async ({ ctx, rawInput, next }) => { + const event = await ctx.prisma.eventType.findUnique({ + where: { id: (rawInput as Record<"id", number>)?.id }, + include: { + users: true, + team: { + select: { + members: { + select: { + userId: true, + role: true, + }, + }, + }, + }, + }, + }); + + if (!event) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + + const isAuthorized = (function () { + if (event.team) { + return event.team.members + .filter((member) => member.role === MembershipRole.OWNER || member.role === MembershipRole.ADMIN) + .map((member) => member.userId) + .includes(ctx.user.id); + } + return event.userId === ctx.user.id || event.users.find((user) => user.id === ctx.user.id); + })(); + + if (!isAuthorized) { + console.warn(`User ${ctx.user.id} attempted to an access an event ${event.id} they do not own.`); + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + return next(); + }) + .mutation("update", { + input: EventTypeUpdateInput.strict(), + async resolve({ ctx, input }) { + const { availability, periodType, locations, destinationCalendar, customInputs, users, id, ...rest } = + input; + const data: Prisma.EventTypeUpdateInput = rest; + data.locations = locations ?? undefined; + + if (periodType) { + data.periodType = handlePeriodType(periodType); + } + + if (destinationCalendar) { + /** We connect or create a destination calendar to the event type instead of the user */ + await viewerRouter.createCaller(ctx).mutation("setDestinationCalendar", { + ...destinationCalendar, + eventTypeId: id, + }); + } + + if (customInputs) { + data.customInputs = handleCustomInputs(customInputs, id); + } + + if (users) { + data.users = { + set: [], + connect: users.map((userId) => ({ id: userId })), + }; + } + + if (availability?.openingHours) { + await ctx.prisma.availability.deleteMany({ + where: { + eventTypeId: input.id, + }, + }); + + data.availability = { + createMany: { + data: availability.openingHours, + }, + }; + } + + const eventType = await ctx.prisma.eventType.update({ + where: { id }, + data, + }); + + return { eventType }; + }, + }) + .mutation("delete", { + input: z.object({ + id: z.number(), + }), + async resolve({ ctx, input }) { + const { id } = input; + + await ctx.prisma.eventTypeCustomInput.deleteMany({ + where: { + eventTypeId: id, + }, + }); + + await ctx.prisma.eventType.delete({ + where: { + id, + }, + }); + + return { + id, + }; + }, + }); diff --git a/yarn.lock b/yarn.lock index 95a71c302e..a73431481c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1855,6 +1855,15 @@ dependencies: "@prisma/engines-version" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db" +"@prisma/debug@3.8.1": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-3.8.1.tgz#3c6717d6e0501651709714774ea6d90127c6a2d3" + integrity sha512-ft4VPTYME1UBJ7trfrBuF2w9jX1ipDy786T9fAEskNGb+y26gPDqz5fiEWc2kgHNeVdz/qTI/V3wXILRyEcgxQ== + dependencies: + "@types/debug" "4.1.7" + ms "2.1.3" + strip-ansi "6.0.1" + "@prisma/engines-version@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db": version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db" resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#c45323e420f47dd950b22c873bdcf38f75e65779" @@ -1865,6 +1874,16 @@ resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#b6cf70bc05dd2a62168a16f3ea58a1b011074621" integrity sha512-Q9CwN6e5E5Abso7J3A1fHbcF4NXGRINyMnf7WQ07fXaebxTTARY5BNUzy2Mo5uH82eRVO5v7ImNuR044KTjLJg== +"@prisma/generator-helper@~3.8.1": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@prisma/generator-helper/-/generator-helper-3.8.1.tgz#eb1dcc8382faa17c784a9d0e0d79fd207a222aa4" + integrity sha512-3zSy+XTEjmjLj6NO+/YPN1Cu7or3xA11TOoOnLRJ9G4pTT67RJXjK0L9Xy5n+3I0Xlb7xrWCgo8MvQQLMWzxPA== + dependencies: + "@prisma/debug" "3.8.1" + "@types/cross-spawn" "6.0.2" + chalk "4.1.2" + cross-spawn "7.0.3" + "@radix-ui/number@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-0.1.0.tgz#73ad13d5cc5f75fa5e147d72e5d5d5e50d688256" @@ -2380,6 +2399,16 @@ dependencies: tslib "^2.1.0" +"@ts-morph/common@~0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.12.2.tgz#61d07a47d622d231e833c44471ab306faaa41aed" + integrity sha512-m5KjptpIf1K0t0QL38uE+ol1n+aNn9MgRq++G3Zym1FlqfN+rThsXlp3cAgib14pIeXF7jk3UtJQOviwawFyYg== + dependencies: + fast-glob "^3.2.7" + minimatch "^3.0.4" + mkdirp "^1.0.4" + path-browserify "^1.0.1" + "@tsconfig/node10@^1.0.7": version "1.0.8" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" @@ -2464,6 +2493,20 @@ resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.2.tgz#e3530eac9dd136bfdfb0e43df2c4c5ce1f77dfae" integrity sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ== +"@types/cross-spawn@6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.2.tgz#168309de311cd30a2b8ae720de6475c2fbf33ac7" + integrity sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw== + dependencies: + "@types/node" "*" + +"@types/debug@4.1.7": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" + integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== + dependencies: + "@types/ms" "*" + "@types/engine.io@*": version "3.1.7" resolved "https://registry.yarnpkg.com/@types/engine.io/-/engine.io-3.1.7.tgz#86e541a5dc52fb7e97735383564a6ae4cfe2e8f5" @@ -2546,6 +2589,11 @@ "@types/node" "*" "@types/socket.io" "2.1.13" +"@types/ms@*": + version "0.7.31" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + "@types/node@*", "@types/node@>=8.1.0": version "16.11.6" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" @@ -3679,7 +3727,7 @@ chalk@4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: +chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3840,6 +3888,13 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= +code-block-writer@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-11.0.0.tgz#5956fb186617f6740e2c3257757fea79315dd7d4" + integrity sha512-GEqWvEWWsOvER+g9keO4ohFoD3ymwyCnqY3hoTr7GZipYFwEhMHJw+TtV0rfgRhNImM6QWZGO2XYjlJVyYT62w== + dependencies: + tslib "2.3.1" + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -4062,6 +4117,15 @@ cross-fetch@3.1.4: dependencies: node-fetch "2.6.1" +cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -4073,15 +4137,6 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - crypto-browserify@3.12.0, crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -8151,6 +8206,11 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parenthesis@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/parenthesis/-/parenthesis-3.1.8.tgz#3457fccb8f05db27572b841dad9d2630b912f125" + integrity sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw== + parse-asn1@^5.0.0, parse-asn1@^5.1.5: version "5.1.6" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" @@ -8230,7 +8290,7 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= -path-browserify@1.0.1: +path-browserify@1.0.1, path-browserify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== @@ -10404,6 +10464,14 @@ ts-jest@^26.0.0: semver "7.x" yargs-parser "20.x" +ts-morph@^13.0.2: + version "13.0.2" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-13.0.2.tgz#55546023493ef82389d9e4f28848a556c784bac4" + integrity sha512-SjeeHaRf/mFsNeR3KTJnx39JyEOzT4e+DX28gQx5zjzEOuFs2eGrqeN2PLKs/+AibSxPmzV7RD8nJVKmFJqtLA== + dependencies: + "@ts-morph/common" "~0.12.2" + code-block-writer "^11.0.0" + ts-node@^10.2.1: version "10.4.0" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" @@ -10440,16 +10508,16 @@ tslib@2.0.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== +tslib@2.3.1, tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - tslib@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" @@ -11192,6 +11260,15 @@ zen-observable@0.8.15: resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== +zod-prisma@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/zod-prisma/-/zod-prisma-0.5.2.tgz#b089e531756073333f986db98190c55c44078db8" + integrity sha512-uL7LDCum1LsJbxq4SrrQYkYG7cnAYJCWkLQWVW+e0AJo6UJRjjKb2tmRmU55BLAI6rBT72SWDyHrV28o/7O2pQ== + dependencies: + "@prisma/generator-helper" "~3.8.1" + parenthesis "^3.1.8" + ts-morph "^13.0.2" + zod@^3.8.2: version "3.11.6" resolved "https://registry.yarnpkg.com/zod/-/zod-3.11.6.tgz#e43a5e0c213ae2e02aefe7cb2b1a6fa3d7f1f483"