2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import { z } from "zod";
import { _ApiKeyModel as ApiKey } from "@calcom/prisma/zod";
export const apiKeyCreateBodySchema = ApiKey.pick({
note: true,
expiresAt: true,
userId: true,
})
.partial({ userId: true })
.merge(z.object({ neverExpires: z.boolean().optional() }))
.strict();
export const apiKeyEditBodySchema = ApiKey.pick({
note: true,
})
.partial()
.strict();
export const apiKeyPublicSchema = ApiKey.pick({
id: true,
userId: true,
note: true,
createdAt: true,
expiresAt: true,
lastUsedAt: true,
/** We might never want to expose these. Leaving this a as reminder. */
// hashedKey: true,
});

View File

@@ -0,0 +1,39 @@
import { z } from "zod";
import { _AttendeeModel as Attendee } from "@calcom/prisma/zod";
import { timeZone } from "~/lib/validations/shared/timeZone";
export const schemaAttendeeBaseBodyParams = Attendee.pick({
bookingId: true,
email: true,
name: true,
timeZone: true,
});
const schemaAttendeeCreateParams = z
.object({
bookingId: z.number().int(),
email: z.string().email(),
name: z.string(),
timeZone: timeZone,
})
.strict();
const schemaAttendeeEditParams = z
.object({
name: z.string().optional(),
email: z.string().email().optional(),
timeZone: timeZone.optional(),
})
.strict();
export const schemaAttendeeEditBodyParams = schemaAttendeeBaseBodyParams.merge(schemaAttendeeEditParams);
export const schemaAttendeeCreateBodyParams = schemaAttendeeBaseBodyParams.merge(schemaAttendeeCreateParams);
export const schemaAttendeeReadPublic = Attendee.pick({
id: true,
bookingId: true,
name: true,
email: true,
timeZone: true,
});

View File

@@ -0,0 +1,56 @@
import { z } from "zod";
import { _AvailabilityModel as Availability, _ScheduleModel as Schedule } from "@calcom/prisma/zod";
import { denullishShape } from "@calcom/prisma/zod-utils";
export const schemaAvailabilityBaseBodyParams = /** We make all these properties required */ denullishShape(
Availability.pick({
/** We need to pass the schedule where this availability belongs to */
scheduleId: true,
})
);
export const schemaAvailabilityReadPublic = Availability.pick({
id: true,
startTime: true,
endTime: true,
date: true,
scheduleId: true,
days: true,
// eventTypeId: true /** @deprecated */,
// userId: true /** @deprecated */,
}).merge(z.object({ success: z.boolean().optional(), Schedule: Schedule.partial() }).partial());
const schemaAvailabilityCreateParams = z
.object({
startTime: z.date().or(z.string()),
endTime: z.date().or(z.string()),
days: z.array(z.number()).optional(),
date: z.date().or(z.string()).optional(),
})
.strict();
const schemaAvailabilityEditParams = z
.object({
startTime: z.date().or(z.string()).optional(),
endTime: z.date().or(z.string()).optional(),
days: z.array(z.number()).optional(),
date: z.date().or(z.string()).optional(),
})
.strict();
export const schemaAvailabilityEditBodyParams = schemaAvailabilityEditParams;
export const schemaAvailabilityCreateBodyParams = schemaAvailabilityBaseBodyParams.merge(
schemaAvailabilityCreateParams
);
export const schemaAvailabilityReadBodyParams = z
.object({
userId: z.union([z.number(), z.array(z.number())]),
})
.partial();
export const schemaSingleAvailabilityReadBodyParams = z.object({
userId: z.number(),
});

View File

@@ -0,0 +1,28 @@
import { _BookingReferenceModel as BookingReference } from "@calcom/prisma/zod";
import { denullishShape } from "@calcom/prisma/zod-utils";
export const schemaBookingReferenceBaseBodyParams = BookingReference.pick({
type: true,
bookingId: true,
uid: true,
meetingId: true,
meetingPassword: true,
meetingUrl: true,
deleted: true,
}).partial();
export const schemaBookingReferenceReadPublic = BookingReference.pick({
id: true,
type: true,
bookingId: true,
uid: true,
meetingId: true,
meetingPassword: true,
meetingUrl: true,
deleted: true,
});
export const schemaBookingCreateBodyParams = BookingReference.omit({ id: true, bookingId: true })
.merge(denullishShape(BookingReference.pick({ bookingId: true })))
.strict();
export const schemaBookingEditBodyParams = schemaBookingCreateBodyParams.partial();

View File

@@ -0,0 +1,88 @@
import { z } from "zod";
import { _BookingModel as Booking, _AttendeeModel, _UserModel, _PaymentModel } from "@calcom/prisma/zod";
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
import { schemaQueryUserId } from "./shared/queryUserId";
const schemaBookingBaseBodyParams = Booking.pick({
uid: true,
userId: true,
eventTypeId: true,
title: true,
description: true,
startTime: true,
endTime: true,
status: true,
}).partial();
export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial());
export const schemaBookingGetParams = z.object({
dateFrom: iso8601.optional(),
dateTo: iso8601.optional(),
order: z.enum(["asc", "desc"]).default("asc"),
sortBy: z.enum(["createdAt", "updatedAt"]).optional(),
});
const schemaBookingEditParams = z
.object({
title: z.string().optional(),
startTime: iso8601.optional(),
endTime: iso8601.optional(),
// Not supporting responses in edit as that might require re-triggering emails
// responses
})
.strict();
export const schemaBookingEditBodyParams = schemaBookingBaseBodyParams
.merge(schemaBookingEditParams)
.omit({ uid: true });
export const schemaBookingReadPublic = Booking.extend({
attendees: z
.array(
_AttendeeModel.pick({
email: true,
name: true,
timeZone: true,
locale: true,
})
)
.optional(),
user: _UserModel
.pick({
email: true,
name: true,
timeZone: true,
locale: true,
})
.nullish(),
payment: z
.array(
_PaymentModel.pick({
id: true,
success: true,
paymentOption: true,
})
)
.optional(),
responses: z.record(z.any()).nullable(),
}).pick({
id: true,
userId: true,
description: true,
eventTypeId: true,
uid: true,
title: true,
startTime: true,
endTime: true,
timeZone: true,
attendees: true,
user: true,
payment: true,
metadata: true,
status: true,
responses: true,
fromReschedule: true,
});

View File

@@ -0,0 +1,18 @@
import { z } from "zod";
const CalendarSchema = z.object({
externalId: z.string(),
name: z.string(),
primary: z.boolean(),
readOnly: z.boolean(),
});
const IntegrationSchema = z.object({
name: z.string(),
appId: z.string(),
userId: z.number(),
integration: z.string(),
calendars: z.array(CalendarSchema),
});
export const schemaConnectedCalendarsReadPublic = z.array(IntegrationSchema);

View File

@@ -0,0 +1,64 @@
import { z } from "zod";
import { HttpError } from "@calcom/lib/http-error";
const userId = z.string().transform((val) => {
const userIdInt = parseInt(val);
if (isNaN(userIdInt)) {
throw new HttpError({ message: "userId is not a valid number", statusCode: 400 });
}
return userIdInt;
});
const appSlug = z.string();
const credentialId = z.string().transform((val) => {
const credentialIdInt = parseInt(val);
if (isNaN(credentialIdInt)) {
throw new HttpError({ message: "credentialId is not a valid number", statusCode: 400 });
}
return credentialIdInt;
});
const encryptedKey = z.string();
export const schemaCredentialGetParams = z.object({
userId,
appSlug: appSlug.optional(),
});
export const schemaCredentialPostParams = z.object({
userId,
createSelectedCalendar: z
.string()
.optional()
.transform((val) => {
return val === "true";
}),
createDestinationCalendar: z
.string()
.optional()
.transform((val) => {
return val === "true";
}),
});
export const schemaCredentialPostBody = z.object({
appSlug,
encryptedKey,
});
export const schemaCredentialPatchParams = z.object({
userId,
credentialId,
});
export const schemaCredentialPatchBody = z.object({
encryptedKey,
});
export const schemaCredentialDeleteParams = z.object({
userId,
credentialId,
});

View File

@@ -0,0 +1,48 @@
import { z } from "zod";
import { _DestinationCalendarModel as DestinationCalendar } from "@calcom/prisma/zod";
export const schemaDestinationCalendarBaseBodyParams = DestinationCalendar.pick({
integration: true,
externalId: true,
eventTypeId: true,
bookingId: true,
userId: true,
}).partial();
const schemaDestinationCalendarCreateParams = z
.object({
integration: z.string(),
externalId: z.string(),
eventTypeId: z.number().optional(),
bookingId: z.number().optional(),
userId: z.number().optional(),
})
.strict();
export const schemaDestinationCalendarCreateBodyParams = schemaDestinationCalendarBaseBodyParams.merge(
schemaDestinationCalendarCreateParams
);
const schemaDestinationCalendarEditParams = z
.object({
integration: z.string().optional(),
externalId: z.string().optional(),
eventTypeId: z.number().optional(),
bookingId: z.number().optional(),
userId: z.number().optional(),
})
.strict();
export const schemaDestinationCalendarEditBodyParams = schemaDestinationCalendarBaseBodyParams.merge(
schemaDestinationCalendarEditParams
);
export const schemaDestinationCalendarReadPublic = DestinationCalendar.pick({
id: true,
integration: true,
externalId: true,
eventTypeId: true,
bookingId: true,
userId: true,
});

View File

@@ -0,0 +1,13 @@
import { _EventTypeCustomInputModel as EventTypeCustomInput } from "@calcom/prisma/zod";
export const schemaEventTypeCustomInputBaseBodyParams = EventTypeCustomInput.omit({
id: true,
});
export const schemaEventTypeCustomInputPublic = EventTypeCustomInput.omit({});
export const schemaEventTypeCustomInputBodyParams = schemaEventTypeCustomInputBaseBodyParams.strict();
export const schemaEventTypeCustomInputEditBodyParams = schemaEventTypeCustomInputBaseBodyParams
.partial()
.strict();

View File

@@ -0,0 +1,173 @@
import { z } from "zod";
import slugify from "@calcom/lib/slugify";
import { _EventTypeModel as EventType, _HostModel } from "@calcom/prisma/zod";
import { customInputSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import { Frequency } from "~/lib/types";
import { jsonSchema } from "./shared/jsonSchema";
import { schemaQueryUserId } from "./shared/queryUserId";
import { timeZone } from "./shared/timeZone";
const recurringEventInputSchema = z.object({
dtstart: z.string().optional(),
interval: z.number().int().optional(),
count: z.number().int().optional(),
freq: z.nativeEnum(Frequency).optional(),
until: z.string().optional(),
tzid: timeZone.optional(),
});
const hostSchema = _HostModel.pick({
isFixed: true,
userId: true,
});
export const childrenSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
});
export const schemaEventTypeBaseBodyParams = EventType.pick({
title: true,
description: true,
slug: true,
length: true,
hidden: true,
position: true,
eventName: true,
timeZone: true,
schedulingType: true,
// START Limit future bookings
periodType: true,
periodStartDate: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
// END Limit future bookings
requiresConfirmation: true,
disableGuests: true,
hideCalendarNotes: true,
minimumBookingNotice: true,
parentId: true,
beforeEventBuffer: true,
afterEventBuffer: true,
teamId: true,
price: true,
currency: true,
slotInterval: true,
successRedirectUrl: true,
locations: true,
bookingLimits: true,
onlyShowFirstAvailableSlot: true,
durationLimits: true,
assignAllTeamMembers: true,
})
.merge(
z.object({
children: z.array(childrenSchema).optional().default([]),
hosts: z.array(hostSchema).optional().default([]),
})
)
.partial()
.strict();
const schemaEventTypeCreateParams = z
.object({
title: z.string(),
slug: z.string().transform((s) => slugify(s)),
description: z.string().optional().nullable(),
length: z.number().int(),
metadata: z.any().optional(),
recurringEvent: recurringEventInputSchema.optional(),
seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(),
seatsShowAvailabilityCount: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
scheduleId: z.number().optional(),
parentId: z.number().optional(),
})
.strict();
export const schemaEventTypeCreateBodyParams = schemaEventTypeBaseBodyParams
.merge(schemaEventTypeCreateParams)
.merge(schemaQueryUserId.partial());
const schemaEventTypeEditParams = z
.object({
title: z.string().optional(),
slug: z
.string()
.transform((s) => slugify(s))
.optional(),
length: z.number().int().optional(),
seatsPerTimeSlot: z.number().optional(),
seatsShowAttendees: z.boolean().optional(),
seatsShowAvailabilityCount: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
scheduleId: z.number().optional(),
})
.strict();
export const schemaEventTypeEditBodyParams = schemaEventTypeBaseBodyParams.merge(schemaEventTypeEditParams);
export const schemaEventTypeReadPublic = EventType.pick({
id: true,
title: true,
slug: true,
length: true,
hidden: true,
position: true,
userId: true,
teamId: true,
scheduleId: true,
eventName: true,
timeZone: true,
periodType: true,
periodStartDate: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
requiresConfirmation: true,
recurringEvent: true,
disableGuests: true,
hideCalendarNotes: true,
minimumBookingNotice: true,
beforeEventBuffer: true,
afterEventBuffer: true,
schedulingType: true,
price: true,
currency: true,
slotInterval: true,
parentId: true,
successRedirectUrl: true,
description: true,
locations: true,
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
bookingFields: true,
bookingLimits: true,
onlyShowFirstAvailableSlot: true,
durationLimits: true,
}).merge(
z.object({
children: z.array(childrenSchema).optional().default([]),
hosts: z.array(hostSchema).optional().default([]),
locations: z
.array(
z.object({
link: z.string().optional(),
address: z.string().optional(),
hostPhoneNumber: z.string().optional(),
type: z.any().optional(),
})
)
.nullable(),
metadata: jsonSchema.nullable(),
customInputs: customInputSchema.array().optional(),
link: z.string().optional(),
bookingFields: eventTypeBookingFields.optional().nullable(),
})
);

View File

@@ -0,0 +1,68 @@
import { z } from "zod";
import { MembershipRole } from "@calcom/prisma/enums";
import { _MembershipModel as Membership, _TeamModel } from "@calcom/prisma/zod";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { schemaQueryIdAsString } from "~/lib/validations/shared/queryIdString";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
export const schemaMembershipBaseBodyParams = Membership.omit({});
const schemaMembershipRequiredParams = z.object({
teamId: z.number(),
});
export const membershipCreateBodySchema = Membership.omit({ id: true })
.partial({
accepted: true,
role: true,
disableImpersonation: true,
})
.transform((v) => ({
accepted: false,
role: MembershipRole.MEMBER,
disableImpersonation: false,
...v,
}));
export const membershipEditBodySchema = Membership.omit({
/** To avoid complication, let's avoid updating these, instead you can delete and create a new invite */
teamId: true,
userId: true,
id: true,
})
.partial({
accepted: true,
role: true,
disableImpersonation: true,
})
.strict();
export const schemaMembershipBodyParams = schemaMembershipBaseBodyParams.merge(
schemaMembershipRequiredParams
);
export const schemaMembershipPublic = Membership.merge(z.object({ team: _TeamModel }).partial());
/** We extract userId and teamId from compound ID string */
export const membershipIdSchema = schemaQueryIdAsString
// So we can query additional team data in memberships
.merge(z.object({ teamId: z.union([stringOrNumber, z.array(stringOrNumber)]) }).partial())
.transform((v, ctx) => {
const [userIdStr, teamIdStr] = v.id.split("_");
const userIdInt = schemaQueryIdParseInt.safeParse({ id: userIdStr });
const teamIdInt = schemaQueryIdParseInt.safeParse({ id: teamIdStr });
if (!userIdInt.success) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "userId is not a number" });
return z.NEVER;
}
if (!teamIdInt.success) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "teamId is not a number " });
return z.NEVER;
}
return {
userId: userIdInt.data.id,
teamId: teamIdInt.data.id,
};
});

View File

@@ -0,0 +1,5 @@
import { _PaymentModel as Payment } from "@calcom/prisma/zod";
// FIXME: Payment seems a delicate endpoint, do we need to remove anything here?
export const schemaPaymentBodyParams = Payment.omit({ id: true });
export const schemaPaymentPublic = Payment.omit({ externalId: true });

View File

@@ -0,0 +1,17 @@
import { z } from "zod";
import { _ReminderMailModel as ReminderMail } from "@calcom/prisma/zod";
export const schemaReminderMailBaseBodyParams = ReminderMail.omit({ id: true }).partial();
export const schemaReminderMailPublic = ReminderMail.omit({});
const schemaReminderMailRequiredParams = z.object({
referenceId: z.number().int(),
reminderType: z.enum(["PENDING_BOOKING_CONFIRMATION"]),
elapsedMinutes: z.number().int(),
});
export const schemaReminderMailBodyParams = schemaReminderMailBaseBodyParams.merge(
schemaReminderMailRequiredParams
);

View File

@@ -0,0 +1,43 @@
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { _ScheduleModel as Schedule, _AvailabilityModel as Availability } from "@calcom/prisma/zod";
import { timeZone } from "./shared/timeZone";
const schemaScheduleBaseBodyParams = Schedule.omit({ id: true, timeZone: true }).partial();
export const schemaSingleScheduleBodyParams = schemaScheduleBaseBodyParams.merge(
z.object({ userId: z.number().optional(), timeZone: timeZone.optional() })
);
export const schemaCreateScheduleBodyParams = schemaScheduleBaseBodyParams.merge(
z.object({ userId: z.number().optional(), name: z.string(), timeZone })
);
export const schemaSchedulePublic = z
.object({ id: z.number() })
.merge(Schedule)
.merge(
z.object({
availability: z
.array(
Availability.pick({
id: true,
eventTypeId: true,
date: true,
days: true,
startTime: true,
endTime: true,
})
)
.transform((v) =>
v.map((item) => ({
...item,
startTime: dayjs.utc(item.startTime).format("HH:mm:ss"),
endTime: dayjs.utc(item.endTime).format("HH:mm:ss"),
}))
)
.optional(),
})
);

View File

@@ -0,0 +1,48 @@
import z from "zod";
import { _SelectedCalendarModel as SelectedCalendar } from "@calcom/prisma/zod";
import { schemaQueryIdAsString } from "./shared/queryIdString";
import { schemaQueryIdParseInt } from "./shared/queryIdTransformParseInt";
export const schemaSelectedCalendarBaseBodyParams = SelectedCalendar;
export const schemaSelectedCalendarPublic = SelectedCalendar.omit({});
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams.partial({
userId: true,
});
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams.partial();
export const selectedCalendarIdSchema = schemaQueryIdAsString.transform((v, ctx) => {
/** We can assume the first part is the userId since it's an integer */
const [userIdStr, ...rest] = v.id.split("_");
/** We can assume that the remainder is both the integration type and external id combined */
const integration_externalId = rest.join("_");
/**
* Since we only handle calendars here we can split by `_calendar_` and re add it later on.
* This handle special cases like `google_calendar_c_blabla@group.calendar.google.com` and
* `hubspot_other_calendar`.
**/
const [_integration, externalId] = integration_externalId.split("_calendar_");
const userIdInt = schemaQueryIdParseInt.safeParse({ id: userIdStr });
if (!userIdInt.success) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "userId is not a number" });
return z.NEVER;
}
if (!_integration) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Missing integration" });
return z.NEVER;
}
if (!externalId) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Missing externalId" });
return z.NEVER;
}
return {
userId: userIdInt.data.id,
/** We re-add the split `_calendar` string */
integration: `${_integration}_calendar`,
externalId,
};
});

View File

@@ -0,0 +1,11 @@
import { z } from "zod";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const baseApiParams = z.object({
// since we added apiKey as query param this is required by next-validations helper
// for query params to work properly and not fail.
apiKey: z.string().optional(),
// version required for supporting /v1/ redirect to query in api as *?version=1
version: z.string().optional(),
});

View File

@@ -0,0 +1,9 @@
import { z } from "zod";
// 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()]);
export const jsonSchema: z.ZodSchema<Json> = z.lazy(() =>
z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])
);

View File

@@ -0,0 +1,20 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryAttendeeEmail = baseApiParams.extend({
attendeeEmail: z.string().email(),
});
export const schemaQuerySingleOrMultipleAttendeeEmails = z.object({
attendeeEmail: z.union([z.string().email(), z.array(z.string().email())]).optional(),
});
export const withValidQueryAttendeeEmail = withValidation({
schema: schemaQueryAttendeeEmail,
type: "Zod",
mode: "query",
});

View File

@@ -0,0 +1,19 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
/** Used for UUID style id queries */
export const schemaQueryIdAsString = baseApiParams
.extend({
id: z.string(),
})
.strict();
export const withValidQueryIdString = withValidation({
schema: schemaQueryIdAsString,
type: "Zod",
mode: "query",
});

View File

@@ -0,0 +1,20 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryIdParseInt = baseApiParams.extend({
id: z.coerce.number(),
});
export const withValidQueryIdTransformParseInt = withValidation({
schema: schemaQueryIdParseInt,
type: "Zod",
mode: "query",
});
export const getTranscriptFromRecordingId = schemaQueryIdParseInt.extend({
recordingId: z.string(),
});

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
export const schemaQuerySlug = baseApiParams.extend({
slug: z.string().optional(),
});

View File

@@ -0,0 +1,21 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryTeamId = baseApiParams
.extend({
teamId: z
.string()
.regex(/^\d+$/)
.transform((id) => parseInt(id)),
})
.strict();
export const withValidQueryTeamId = withValidation({
schema: schemaQueryTeamId,
type: "Zod",
mode: "query",
});

View File

@@ -0,0 +1,20 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryUserEmail = baseApiParams.extend({
email: z.string().email(),
});
export const schemaQuerySingleOrMultipleUserEmails = z.object({
email: z.union([z.string().email(), z.array(z.string().email())]),
});
export const withValidQueryUserEmail = withValidation({
schema: schemaQueryUserEmail,
type: "Zod",
mode: "query",
});

View File

@@ -0,0 +1,26 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryUserId = baseApiParams.extend({
userId: stringOrNumber,
});
export const schemaQuerySingleOrMultipleUserIds = z.object({
userId: z.union([stringOrNumber, z.array(stringOrNumber)]),
});
export const schemaQuerySingleOrMultipleTeamIds = z.object({
teamId: z.union([stringOrNumber, z.array(stringOrNumber)]),
});
export const withValidQueryUserId = withValidation({
schema: schemaQueryUserId,
type: "Zod",
mode: "query",
});

View File

@@ -0,0 +1,7 @@
import tzdata from "tzdata";
import { z } from "zod";
// @note: This is a custom validation that checks if the timezone is valid and exists in the tzdb library
export const timeZone = z.string().refine((tz: string) => Object.keys(tzdata.zones).includes(tz), {
message: `Expected one of the following: ${Object.keys(tzdata.zones).join(", ")}`,
});

View File

@@ -0,0 +1,30 @@
import { z } from "zod";
import { _TeamModel as Team } from "@calcom/prisma/zod";
export const schemaTeamBaseBodyParams = Team.omit({ id: true, createdAt: true }).partial({
hideBranding: true,
metadata: true,
pendingPayment: true,
isOrganization: true,
isPlatform: true,
smsLockState: true,
});
const schemaTeamRequiredParams = z.object({
name: z.string().max(255),
});
export const schemaTeamBodyParams = schemaTeamBaseBodyParams.merge(schemaTeamRequiredParams).strict();
export const schemaTeamUpdateBodyParams = schemaTeamBodyParams.partial();
const schemaOwnerId = z.object({
ownerId: z.number().optional(),
});
export const schemaTeamCreateBodyParams = schemaTeamBodyParams.merge(schemaOwnerId).strict();
export const schemaTeamReadPublic = Team.omit({});
export const schemaTeamsReadPublic = z.array(schemaTeamReadPublic);

View File

@@ -0,0 +1,179 @@
import { z } from "zod";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import { _UserModel as User } from "@calcom/prisma/zod";
import { iso8601 } from "@calcom/prisma/zod-utils";
import { isValidBase64Image } from "~/lib/utils/isValidBase64Image";
import { timeZone } from "~/lib/validations/shared/timeZone";
// @note: These are the ONLY values allowed as weekStart. So user don't introduce bad data.
enum weekdays {
MONDAY = "Monday",
TUESDAY = "Tuesday",
WEDNESDAY = "Wednesday",
THURSDAY = "Thursday",
FRIDAY = "Friday",
SATURDAY = "Saturday",
SUNDAY = "Sunday",
}
// @note: extracted from apps/web/next-i18next.config.js, update if new locales.
enum locales {
EN = "en",
FR = "fr",
IT = "it",
RU = "ru",
ES = "es",
DE = "de",
PT = "pt",
RO = "ro",
NL = "nl",
PT_BR = "pt-BR",
// ES_419 = "es-419", // Disabled until Crowdin reaches at least 80% completion
KO = "ko",
JA = "ja",
PL = "pl",
AR = "ar",
IW = "iw",
ZH_CN = "zh-CN",
ZH_TW = "zh-TW",
CS = "cs",
SR = "sr",
SV = "sv",
VI = "vi",
}
enum theme {
DARK = "dark",
LIGHT = "light",
}
enum timeFormat {
TWELVE = 12,
TWENTY_FOUR = 24,
}
const usernameSchema = z
.string()
.transform((v) => v.toLowerCase())
// .refine(() => {})
.superRefine(async (val, ctx) => {
if (val) {
const result = await checkUsername(val);
if (!result.available) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "already_in_use_error" });
if (result.premium) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "premium_username" });
}
});
// @note: These are the values that are editable via PATCH method on the user Model
export const schemaUserBaseBodyParams = User.pick({
name: true,
email: true,
username: true,
bio: true,
timeZone: true,
weekStart: true,
theme: true,
appTheme: true,
defaultScheduleId: true,
locale: true,
hideBranding: true,
timeFormat: true,
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
role: true,
// @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI.
// avatar: true,
}).partial();
// @note: partial() is used to allow for the user to edit only the fields they want to edit making all optional,
// if want to make any required do it in the schemaRequiredParams
// Here we can both require or not (adding optional or nullish) and also rewrite validations for any value
// for example making weekStart only accept weekdays as input
const schemaUserEditParams = z.object({
email: z.string().email().toLowerCase(),
username: usernameSchema,
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
hideBranding: z.boolean().optional(),
timeZone: timeZone.optional(),
theme: z.nativeEnum(theme).optional().nullable(),
appTheme: z.nativeEnum(theme).optional().nullable(),
timeFormat: z.nativeEnum(timeFormat).optional(),
defaultScheduleId: z
.number()
.refine((id: number) => id > 0)
.optional()
.nullable(),
locale: z.nativeEnum(locales).optional().nullable(),
avatar: z.string().refine(isValidBase64Image).optional(),
});
// @note: These are the values that are editable via PATCH method on the user Model,
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
const schemaUserCreateParams = z.object({
email: z.string().email().toLowerCase(),
username: usernameSchema,
weekStart: z.nativeEnum(weekdays).optional(),
brandColor: z.string().min(4).max(9).regex(/^#/).optional(),
darkBrandColor: z.string().min(4).max(9).regex(/^#/).optional(),
hideBranding: z.boolean().optional(),
timeZone: timeZone.optional(),
theme: z.nativeEnum(theme).optional().nullable(),
appTheme: z.nativeEnum(theme).optional().nullable(),
timeFormat: z.nativeEnum(timeFormat).optional(),
defaultScheduleId: z
.number()
.refine((id: number) => id > 0)
.optional()
.nullable(),
locale: z.nativeEnum(locales).optional(),
createdDate: iso8601.optional(),
avatar: z.string().refine(isValidBase64Image).optional(),
});
// @note: These are the values that are editable via PATCH method on the user Model,
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
export const schemaUserEditBodyParams = schemaUserBaseBodyParams
.merge(schemaUserEditParams)
.omit({})
.partial()
.strict();
export const schemaUserCreateBodyParams = schemaUserBaseBodyParams
.merge(schemaUserCreateParams)
.omit({})
.strict();
// @note: These are the values that are always returned when reading a user
export const schemaUserReadPublic = User.pick({
id: true,
username: true,
name: true,
email: true,
emailVerified: true,
bio: true,
avatar: true,
timeZone: true,
weekStart: true,
endTime: true,
bufferTime: true,
appTheme: true,
theme: true,
defaultScheduleId: true,
locale: true,
timeFormat: true,
hideBranding: true,
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
createdDate: true,
verified: true,
invitedTo: true,
role: true,
});
export const schemaUsersReadPublic = z.array(schemaUserReadPublic);

View File

@@ -0,0 +1,57 @@
import { z } from "zod";
import { WEBHOOK_TRIGGER_EVENTS } from "@calcom/features/webhooks/lib/constants";
import { _WebhookModel as Webhook } from "@calcom/prisma/zod";
const schemaWebhookBaseBodyParams = Webhook.pick({
userId: true,
eventTypeId: true,
eventTriggers: true,
active: true,
subscriberUrl: true,
payloadTemplate: true,
});
export const schemaWebhookCreateParams = z
.object({
// subscriberUrl: z.string().url(),
// eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array(),
// active: z.boolean(),
payloadTemplate: z.string().optional().nullable(),
eventTypeId: z.number().optional(),
userId: z.number().optional(),
secret: z.string().optional().nullable(),
// API shouldn't mess with Apps webhooks yet (ie. Zapier)
// appId: z.string().optional().nullable(),
})
.strict();
export const schemaWebhookCreateBodyParams = schemaWebhookBaseBodyParams.merge(schemaWebhookCreateParams);
export const schemaWebhookEditBodyParams = schemaWebhookBaseBodyParams
.merge(
z.object({
eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(),
secret: z.string().optional().nullable(),
})
)
.partial()
.strict();
export const schemaWebhookReadPublic = Webhook.pick({
id: true,
userId: true,
eventTypeId: true,
payloadTemplate: true,
eventTriggers: true,
// FIXME: We have some invalid urls saved in the DB
// subscriberUrl: true,
/** @todo: find out how to properly add back and validate those. */
// eventType: true,
// app: true,
appId: true,
}).merge(
z.object({
subscriberUrl: z.string(),
})
);