refactor: v2 and atoms booking fields (#16685)

* refactor: define inputs and outputs by separating default and custom fields

* refactor: event type api <-> internal transformers

* refactor: v2 use refactored libraries to transform input and output booking fields

* refactor: dont allow custom slugs with default reserved slugs

* refactor: input service store only specific default system fields

* refactor: atoms display only specific default system fields

* refactor: delete unused system field constants

* fix: tests

* fix: old event types

* fix: imports

* fix: import

* chore: release libraries and use v2 with them

* fix: unit tests

* refactor: push email in case of missing phone

* refactor: satisfies check for event-type locations
This commit is contained in:
Lauris Skraucis
2024-09-18 13:59:31 +02:00
committed by GitHub
parent 2b43d9a716
commit 5b5a42e72d
34 changed files with 7108 additions and 1187 deletions
+1 -1
View File
@@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.34",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.36",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -15,6 +15,7 @@ import {
updateEventType,
EventTypesPublic,
getEventTypesPublic,
systemBeforeFieldEmail,
} from "@calcom/platform-libraries";
import { EventType } from "@calcom/prisma/client";
@@ -127,8 +128,17 @@ export class EventTypesService_2024_04_15 {
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) {
this.checkCanUpdateEventType(user.id, eventTypeId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
const bookingFields = [...(body.bookingFields || [])];
if (
!bookingFields.find((field) => field.type === "email") &&
!bookingFields.find((field) => field.type === "phone")
) {
bookingFields.push(systemBeforeFieldEmail);
}
await updateEventType({
input: { id: eventTypeId, ...body },
input: { id: eventTypeId, ...body, bookingFields },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -250,7 +250,6 @@ describe("Event types Endpoints", () => {
expect(createdEventType.description).toEqual(body.description);
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
expect(createdEventType.locations).toEqual(body.locations);
expect(createdEventType.bookingFields).toEqual(body.bookingFields);
expect(createdEventType.ownerId).toEqual(user.id);
expect(createdEventType.scheduleId).toEqual(firstSchedule.id);
expect(createdEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
@@ -259,6 +258,16 @@ describe("Event types Endpoints", () => {
expect(createdEventType.offsetStart).toEqual(body.offsetStart);
expect(createdEventType.bookingWindow).toEqual(body.bookingWindow);
expect(createdEventType.recurrence).toEqual(body.recurrence);
const responseBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", type: "name" },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
...responseBookingFields.map((field) => ({ isDefault: false, ...field })),
];
expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
eventType = responseBody.data;
});
});
@@ -476,6 +485,16 @@ describe("Event types Endpoints", () => {
let legacyEventTypeId1: number;
let legacyEventTypeId2: number;
const expectedReturnSystemFields = [
{ isDefault: true, required: true, slug: "name", type: "name" },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, type: "radioInput", slug: "location", required: false },
{ isDefault: true, required: true, slug: "title", type: "text" },
{ isDefault: true, required: false, slug: "notes", type: "textarea" },
{ isDefault: true, required: false, slug: "guests", type: "multiemail" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
];
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
@@ -545,7 +564,7 @@ describe("Event types Endpoints", () => {
.expect(400);
});
it("should return empty bookingFields if system fields are the only one in database", async () => {
it("should return system bookingFields stored in database", async () => {
const legacyEventTypeInput = {
title: "legacy event type",
description: "legacy event type description",
@@ -638,11 +657,11 @@ describe("Event types Endpoints", () => {
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([]);
expect(fetchedEventType.bookingFields).toEqual(expectedReturnSystemFields);
});
});
it("should return user created bookingFields among system fields in the database", async () => {
it("should return user created bookingFields with system fields", async () => {
const userDefinedBookingField = {
name: "team",
type: "textarea",
@@ -755,7 +774,9 @@ describe("Event types Endpoints", () => {
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
...expectedReturnSystemFields,
{
isDefault: false,
type: userDefinedBookingField.type,
slug: userDefinedBookingField.name,
label: userDefinedBookingField.label,
@@ -6,8 +6,8 @@ import { Injectable } from "@nestjs/common";
import {
getEventTypeById,
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
} from "@calcom/platform-libraries";
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import type { PrismaClient } from "@calcom/prisma";
@@ -30,8 +30,8 @@ type InputEventTransformed = Omit<
> & {
length: number;
slug: string;
locations?: ReturnType<typeof transformApiEventTypeLocations>;
bookingFields?: ReturnType<typeof transformApiEventTypeBookingFields>;
locations?: ReturnType<typeof transformLocationsApiToInternal>;
bookingFields?: ReturnType<typeof transformBookingFieldsApiToInternal>;
};
@Injectable()
@@ -1,12 +1,16 @@
import { Injectable } from "@nestjs/common";
import {
transformApiEventTypeBookingFields,
transformApiEventTypeLocations,
transformApiEventTypeIntervalLimits,
transformApiEventTypeFutureBookingLimits,
transformApiEventTypeRecurrence,
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
transformRecurrenceApiToInternal,
systemBeforeFieldName,
systemBeforeFieldEmail,
systemAfterFieldRescheduleReason,
} from "@calcom/platform-libraries";
import { systemBeforeFieldLocation } from "@calcom/platform-libraries";
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
@Injectable()
@@ -30,11 +34,12 @@ export class InputEventTypesService_2024_06_14 {
...rest
} = inputEventType;
const hasMultipleLocations = (locations || defaultLocations).length > 1;
const eventType = {
...rest,
length: lengthInMinutes,
locations: this.transformInputLocations(locations || defaultLocations),
bookingFields: this.transformInputBookingFields(bookingFields),
bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations),
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -59,11 +64,15 @@ export class InputEventTypesService_2024_06_14 {
...rest
} = inputEventType;
const hasMultipleLocations = !!(locations && locations?.length > 1);
const eventType = {
...rest,
length: lengthInMinutes,
locations: locations ? this.transformInputLocations(locations) : undefined,
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
bookingFields: bookingFields
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
: undefined,
schedule: scheduleId,
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
@@ -77,23 +86,35 @@ export class InputEventTypesService_2024_06_14 {
}
transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) {
return transformApiEventTypeLocations(inputLocations);
return transformLocationsApiToInternal(inputLocations);
}
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
return transformApiEventTypeBookingFields(inputBookingFields);
transformInputBookingFields(
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
hasMultipleLocations: boolean
) {
const defaultFieldsBefore = [systemBeforeFieldName, systemBeforeFieldEmail];
// note(Lauris): if event type has multiple locations then a radio button booking field has to be displayed to allow booker to pick location
if (hasMultipleLocations) {
defaultFieldsBefore.push(systemBeforeFieldLocation);
}
const customFields = transformBookingFieldsApiToInternal(inputBookingFields);
const defaultFieldsAfter = [systemAfterFieldRescheduleReason];
return [...defaultFieldsBefore, ...customFields, ...defaultFieldsAfter];
}
transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) {
return transformApiEventTypeIntervalLimits(inputBookingFields);
return transformIntervalLimitsApiToInternal(inputBookingFields);
}
transformInputBookingWindow(inputBookingWindow: CreateEventTypeInput_2024_06_14["bookingWindow"]) {
const res = transformApiEventTypeFutureBookingLimits(inputBookingWindow);
const res = transformFutureBookingLimitsApiToInternal(inputBookingWindow);
return !!res ? res : {};
}
transformInputRecurrignEvent(recurrence: CreateEventTypeInput_2024_06_14["recurrence"]) {
return transformApiEventTypeRecurrence(recurrence);
return transformRecurrenceApiToInternal(recurrence);
}
}
@@ -4,17 +4,17 @@ import type { EventType, User, Schedule } from "@prisma/client";
import {
EventTypeMetaDataSchema,
userMetadata,
getResponseEventTypeLocations,
getResponseEventTypeBookingFields,
transformLocationsInternalToApi,
transformBookingFieldsInternalToApi,
parseRecurringEvent,
TransformedLocationsSchema,
BookingFieldsSchema,
SystemField,
UserField,
CustomField,
parseBookingLimit,
getResponseEventTypeIntervalLimits,
getResponseEventTypeFutureBookingLimits,
getResponseEventTypeRecurrence,
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
} from "@calcom/platform-libraries";
import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types";
@@ -144,19 +144,20 @@ export class OutputEventTypesService_2024_06_14 {
transformLocations(locations: any) {
if (!locations) return [];
return getResponseEventTypeLocations(TransformedLocationsSchema.parse(locations));
return transformLocationsInternalToApi(TransformedLocationsSchema.parse(locations));
}
transformBookingFields(inputBookingFields: (SystemField | UserField)[] | null) {
if (!inputBookingFields) return [];
const userFields = inputBookingFields.filter((field) => field.editable === "user") as UserField[];
return getResponseEventTypeBookingFields(userFields);
transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) {
if (!bookingFields) return [];
return transformBookingFieldsInternalToApi(bookingFields);
}
transformRecurringEvent(recurringEvent: any) {
if (!recurringEvent) return null;
const recurringEventParsed = parseRecurringEvent(recurringEvent);
return getResponseEventTypeRecurrence(recurringEventParsed);
if (!recurringEventParsed) return null;
return transformRecurrenceInternalToApi(recurringEventParsed);
}
transformMetadata(metadata: any) {
@@ -182,10 +183,10 @@ export class OutputEventTypesService_2024_06_14 {
transformIntervalLimits(bookingLimits: any) {
const bookingLimitsParsed = parseBookingLimit(bookingLimits);
return getResponseEventTypeIntervalLimits(bookingLimitsParsed);
return transformIntervalLimitsInternalToApi(bookingLimitsParsed);
}
transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) {
return getResponseEventTypeFutureBookingLimits(bookingLimits);
return transformFutureBookingLimitsInternalToApi(bookingLimits);
}
}
+3
View File
@@ -22,4 +22,7 @@ const env: Partial<Omit<Environment, "NODE_ENV">> = {
process.env = {
...env,
...process.env,
NEXT_PUBLIC_VAPID_PUBLIC_KEY:
"BIds0AQJ96xGBjTSMHTOqLBLutQE7Lu32KKdgSdy7A2cS4mKI2cgb3iGkhDJa5Siy-stezyuPm8qpbhmNxdNHMw",
VAPID_PRIVATE_KEY: "6cJtkASCar5sZWguIAW7OjvyixpBw9p8zL8WDDwk9Jk",
};
@@ -1,279 +0,0 @@
import { z } from "zod";
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookingWindowPeriodOutputTypeEnum_2024_06_14,
BookingLimitsEnum_2024_06_14,
Frequency,
} from "@calcom/platform-enums/monorepo";
import {
type CreateEventTypeInput_2024_06_14,
type Integration_2024_06_14,
type BusinessDaysWindow_2024_06_14,
type RangeWindow_2024_06_14,
type TransformFutureBookingsLimitSchema_2024_06_14,
type BookingLimitsKeyOutputType_2024_06_14,
type TransformBookingLimitsSchema_2024_06_14,
type TransformRecurringEventSchema_2024_06_14,
} from "@calcom/platform-types";
const integrationsMapping: Record<Integration_2024_06_14, string> = {
"cal-video": "integrations:daily",
};
function transformApiEventTypeLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) {
if (!inputLocations) {
return [];
}
return inputLocations.map((location) => {
const { type } = location;
switch (type) {
case "address":
return { type: "inPerson", address: location.address, displayLocationPublicly: location.public };
case "link":
return { type: "link", link: location.link, displayLocationPublicly: location.public };
case "integration":
const integrationLabel = integrationsMapping[location.integration];
return { type: integrationLabel };
case "phone":
return {
type: "userPhone",
hostPhoneNumber: location.phone,
displayLocationPublicly: location.public,
};
default:
throw new Error(`Unsupported location type '${type}'`);
}
});
}
const integrationsMappingSchema = {
"cal-video": z.literal("integrations:daily"),
};
const InPersonSchema = z.object({
type: z.literal("inPerson"),
address: z.string(),
displayLocationPublicly: z.boolean().default(false),
});
const LinkSchema = z.object({
type: z.literal("link"),
link: z.string().url(),
displayLocationPublicly: z.boolean().default(false),
});
const IntegrationSchema = z.object({
type: z.union([integrationsMappingSchema["cal-video"], integrationsMappingSchema["cal-video"]]),
});
const UserPhoneSchema = z.object({
type: z.literal("userPhone"),
hostPhoneNumber: z.string(),
displayLocationPublicly: z.boolean().default(false),
});
const TransformedLocationSchema = z.union([InPersonSchema, LinkSchema, IntegrationSchema, UserPhoneSchema]);
export const TransformedLocationsSchema = z.array(TransformedLocationSchema);
function transformApiEventTypeBookingFields(
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]
) {
if (!inputBookingFields) {
return [];
}
const customBookingFields = inputBookingFields.map((field) => {
const commonFields: UserField = {
name: field.slug,
type: field.type,
label: field.label,
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: field.required,
placeholder: "placeholder" in field && field.placeholder ? field.placeholder : "",
};
const options = "options" in field && field.options ? transformSelectOptions(field.options) : undefined;
if (!options) {
return commonFields;
}
return {
...commonFields,
options,
};
});
return customBookingFields;
}
function transformApiEventTypeIntervalLimits(
inputBookingLimits: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]
) {
const res: TransformBookingLimitsSchema_2024_06_14 = {};
inputBookingLimits &&
Object.entries(inputBookingLimits).map(([key, value]) => {
const outputKey: BookingLimitsKeyOutputType_2024_06_14 = BookingLimitsEnum_2024_06_14[
key as keyof typeof BookingLimitsEnum_2024_06_14
] satisfies BookingLimitsKeyOutputType_2024_06_14;
res[outputKey] = value;
});
return res;
}
function transformApiEventTypeFutureBookingLimits(
inputBookingLimits: CreateEventTypeInput_2024_06_14["bookingWindow"]
): TransformFutureBookingsLimitSchema_2024_06_14 | undefined {
switch (inputBookingLimits?.type) {
case BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays:
return {
periodDays: (inputBookingLimits as BusinessDaysWindow_2024_06_14).value,
periodType: !!(inputBookingLimits as BusinessDaysWindow_2024_06_14).rolling
? BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING_WINDOW
: BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING,
periodCountCalendarDays: false,
};
case BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays:
return {
periodDays: (inputBookingLimits as BusinessDaysWindow_2024_06_14).value,
periodType: !!(inputBookingLimits as BusinessDaysWindow_2024_06_14).rolling
? BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING_WINDOW
: BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING,
periodCountCalendarDays: true,
};
case BookingWindowPeriodInputTypeEnum_2024_06_14.range:
return {
periodType: BookingWindowPeriodOutputTypeEnum_2024_06_14.RANGE,
periodStartDate: new Date((inputBookingLimits as RangeWindow_2024_06_14).value[0]),
periodEndDate: new Date((inputBookingLimits as RangeWindow_2024_06_14).value[1]),
};
default:
return undefined;
}
}
function transformApiEventTypeRecurrence(
recurrence: CreateEventTypeInput_2024_06_14["recurrence"]
): TransformRecurringEventSchema_2024_06_14 | undefined {
if (!recurrence) return undefined;
return {
interval: recurrence.interval,
count: recurrence.occurrences,
freq: Frequency[recurrence.frequency as keyof typeof Frequency],
} satisfies TransformRecurringEventSchema_2024_06_14;
}
export function transformSelectOptions(options: string[]) {
return options.map((option) => ({
label: option,
value: option,
}));
}
const FieldTypeEnum = z.enum([
"number",
"boolean",
"address",
"name",
"text",
"textarea",
"email",
"phone",
"multiemail",
"select",
"multiselect",
"checkbox",
"radio",
"radioInput",
]);
const UserFieldsSchema = z.object({
name: z.string(),
type: FieldTypeEnum,
label: z.string(),
sources: z.array(
z.object({
id: z.literal("user"),
type: z.literal("user"),
label: z.literal("User"),
fieldRequired: z.literal(true),
})
),
editable: z.literal("user"),
required: z.boolean(),
placeholder: z.string().optional(),
options: z
.array(
z.object({
label: z.string(),
value: z.string(),
})
)
.optional(),
});
const SystemFieldsSchema = z.object({
name: z.string(),
type: FieldTypeEnum,
defaultLabel: z.string(),
label: z.string().optional(),
editable: z.enum(["system-but-optional", "system"]),
sources: z.array(
z.object({
id: z.literal("default"),
type: z.literal("default"),
label: z.literal("Default"),
})
),
views: z
.array(
z.object({
id: z.enum(["reschedule"]),
label: z.string(),
})
)
.optional(),
defaultPlaceholder: z.enum(["", "share_additional_notes", "email", "reschedule_placeholder"]).optional(),
hidden: z.boolean().optional(),
required: z.boolean(),
hideWhenJustOneOption: z.boolean().optional(),
getOptionsAt: z.enum(["locations"]).optional(),
optionsInputs: z
.object({
attendeeInPerson: z.object({
type: z.literal("address"),
required: z.boolean(),
placeholder: z.string(),
}),
phone: z.object({
type: z.literal("phone"),
required: z.boolean(),
placeholder: z.string(),
}),
})
.optional(),
});
export type SystemField = z.infer<typeof SystemFieldsSchema>;
export type UserField = z.infer<typeof UserFieldsSchema>;
export const BookingFieldsSchema = z.array(z.union([UserFieldsSchema, SystemFieldsSchema]));
export {
transformApiEventTypeLocations,
transformApiEventTypeBookingFields,
transformApiEventTypeIntervalLimits,
transformApiEventTypeFutureBookingLimits,
transformApiEventTypeRecurrence,
};
@@ -1,279 +0,0 @@
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookingWindowPeriodOutputTypeEnum_2024_06_14,
BookingLimitsEnum_2024_06_14,
Frequency,
FrequencyInput,
} from "@calcom/platform-enums/monorepo";
import type {
AddressLocation_2024_06_14,
IntegrationLocation_2024_06_14,
LinkLocation_2024_06_14,
PhoneLocation_2024_06_14,
Integration_2024_06_14,
BookingLimitsKeysInputType,
TransformBookingLimitsSchema_2024_06_14,
TransformFutureBookingsLimitSchema_2024_06_14,
BookingWindow_2024_06_14,
RangeWindow_2024_06_14,
CalendarDaysWindow_2024_06_14,
BusinessDaysWindow_2024_06_14,
BookingField_2024_06_14,
Recurrence_2024_06_14,
TransformRecurringEventSchema_2024_06_14,
} from "@calcom/platform-types";
import type { transformApiEventTypeBookingFields, transformApiEventTypeLocations } from "./api-request";
const reverseIntegrationsMapping: Record<string, Integration_2024_06_14> = {
"integrations:daily": "cal-video",
};
function getResponseEventTypeLocations(
transformedLocations: ReturnType<typeof transformApiEventTypeLocations>
) {
if (!transformedLocations) {
return [];
}
return transformedLocations.map((location) => {
switch (location.type) {
case "inPerson": {
if (!location.address) {
throw new Error("Address location must have an address");
}
const addressLocation: AddressLocation_2024_06_14 = {
type: "address",
address: location.address,
public: location.displayLocationPublicly,
};
return addressLocation;
}
case "link": {
if (!location.link) {
throw new Error("Link location must have a link");
}
const linkLocation: LinkLocation_2024_06_14 = {
type: "link",
link: location.link,
public: location.displayLocationPublicly,
};
return linkLocation;
}
case "userPhone": {
if (!location.hostPhoneNumber) {
throw new Error("Phone location must have a phone number");
}
const phoneLocation: PhoneLocation_2024_06_14 = {
type: "phone",
phone: location.hostPhoneNumber,
public: location.displayLocationPublicly,
};
return phoneLocation;
}
default: {
const integrationType = reverseIntegrationsMapping[location.type];
if (!integrationType) {
throw new Error(`Unsupported integration type '${location.type}'.`);
}
const integration: IntegrationLocation_2024_06_14 = {
type: "integration",
integration: integrationType,
};
return integration;
}
}
});
}
function getResponseEventTypeBookingFields(
transformedBookingFields: ReturnType<typeof transformApiEventTypeBookingFields>
): BookingField_2024_06_14[] {
if (!transformedBookingFields) {
return [];
}
return transformedBookingFields.map((field) => {
switch (field.type) {
case "name":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "email":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "phone":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "address":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "text":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "number":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "textarea":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "multiemail":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "boolean":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
};
case "select":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
options: field.options ? field.options.map((option) => option.value) : [],
};
case "multiselect":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
};
case "checkbox":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
};
case "radio":
return {
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
};
default:
throw new Error(`Unsupported booking field type '${field.type}'.`);
}
});
}
function getResponseEventTypeIntervalLimits(
transformedBookingFields: TransformBookingLimitsSchema_2024_06_14 | null
) {
if (!transformedBookingFields) {
return undefined;
}
const res: { [K in BookingLimitsKeysInputType]?: number } = {};
transformedBookingFields &&
Object.entries(transformedBookingFields).map(([key, value]) => {
const outputKey: BookingLimitsKeysInputType | undefined = Object.keys(
BookingLimitsEnum_2024_06_14
).find(
(item) => BookingLimitsEnum_2024_06_14[item as keyof typeof BookingLimitsEnum_2024_06_14] === key
) as BookingLimitsKeysInputType;
if (outputKey) {
res[outputKey] = value as number;
}
});
return res;
}
function getResponseEventTypeFutureBookingLimits(
transformedFutureBookingsLimitsFields: TransformFutureBookingsLimitSchema_2024_06_14
): BookingWindow_2024_06_14 | undefined {
switch (transformedFutureBookingsLimitsFields?.periodType) {
case BookingWindowPeriodOutputTypeEnum_2024_06_14.RANGE:
return {
type: BookingWindowPeriodInputTypeEnum_2024_06_14.range,
value: [
transformedFutureBookingsLimitsFields?.periodStartDate?.toISOString().split("T")[0],
transformedFutureBookingsLimitsFields?.periodEndDate?.toISOString().split("T")[0],
],
} as RangeWindow_2024_06_14;
case BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING_WINDOW:
return {
type: transformedFutureBookingsLimitsFields.periodCountCalendarDays
? BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays
: BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays,
value: transformedFutureBookingsLimitsFields.periodDays,
rolling: true,
} as CalendarDaysWindow_2024_06_14 | BusinessDaysWindow_2024_06_14;
case BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING:
return {
type: transformedFutureBookingsLimitsFields.periodCountCalendarDays
? BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays
: BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays,
value: transformedFutureBookingsLimitsFields.periodDays,
rolling: false,
} as CalendarDaysWindow_2024_06_14 | BusinessDaysWindow_2024_06_14;
default:
return undefined;
}
}
function getResponseEventTypeRecurrence(
transformRecurringEvent: TransformRecurringEventSchema_2024_06_14
): Recurrence_2024_06_14 {
return {
interval: transformRecurringEvent.interval,
occurrences: transformRecurringEvent.count,
frequency: FrequencyInput[Frequency[transformRecurringEvent.freq] as keyof typeof FrequencyInput],
} satisfies Recurrence_2024_06_14;
}
export {
getResponseEventTypeLocations,
getResponseEventTypeBookingFields,
getResponseEventTypeIntervalLimits,
getResponseEventTypeFutureBookingLimits,
getResponseEventTypeRecurrence,
};
@@ -2,24 +2,24 @@ import { describe, expect, it } from "vitest";
import { FrequencyInput } from "@calcom/platform-enums/monorepo";
import type {
BookingField_2024_06_14,
InputBookingField_2024_06_14,
Location_2024_06_14,
BookingLimitsCount_2024_06_14,
BookingWindow_2024_06_14,
Recurrence_2024_06_14,
} from "@calcom/platform-types";
import type { UserField } from "./api-request";
import type { CustomField } from "../internal-to-api/booking-fields";
import {
transformApiEventTypeLocations,
transformApiEventTypeBookingFields,
transformSelectOptions,
transformApiEventTypeIntervalLimits,
transformApiEventTypeFutureBookingLimits,
transformApiEventTypeRecurrence,
} from "./api-request";
transformLocationsApiToInternal,
transformBookingFieldsApiToInternal,
transformSelectOptionsApiToInternal,
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
transformRecurrenceApiToInternal,
} from "./index";
describe("transformApiEventTypeLocations", () => {
describe("transformLocationsApiToInternal", () => {
it("should transform address", () => {
const input: Location_2024_06_14[] = [
{
@@ -31,7 +31,7 @@ describe("transformApiEventTypeLocations", () => {
const expectedOutput = [{ type: "inPerson", address: "London road 10-1", displayLocationPublicly: true }];
const result = transformApiEventTypeLocations(input);
const result = transformLocationsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -49,7 +49,7 @@ describe("transformApiEventTypeLocations", () => {
{ type: "link", link: "https://customvideo.com/join/123456", displayLocationPublicly: true },
];
const result = transformApiEventTypeLocations(input);
const result = transformLocationsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -64,7 +64,7 @@ describe("transformApiEventTypeLocations", () => {
const expectedOutput = [{ type: "integrations:daily" }];
const result = transformApiEventTypeLocations(input);
const result = transformLocationsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -82,85 +82,15 @@ describe("transformApiEventTypeLocations", () => {
{ type: "userPhone", hostPhoneNumber: "+37120993151", displayLocationPublicly: true },
];
const result = transformApiEventTypeLocations(input);
const result = transformLocationsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
});
describe("transformApiEventTypeBookingFields", () => {
it("should transform name field", () => {
const bookingField: BookingField_2024_06_14 = {
type: "name",
slug: "name",
label: "Your name",
required: true,
placeholder: "alice",
};
const input: BookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
{
name: bookingField.slug,
type: bookingField.type,
label: bookingField.label,
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: bookingField.required,
placeholder: bookingField.placeholder,
},
];
const result = transformApiEventTypeBookingFields(input);
expect(result).toEqual(expectedOutput);
});
it("should transform email field", () => {
const bookingField: BookingField_2024_06_14 = {
type: "email",
slug: "email",
label: "Your email",
required: true,
placeholder: "example@example.com",
};
const input: BookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
{
name: bookingField.slug,
type: bookingField.type,
label: bookingField.label,
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: bookingField.required,
placeholder: bookingField.placeholder,
},
];
const result = transformApiEventTypeBookingFields(input);
expect(result).toEqual(expectedOutput);
});
describe("transformBookingFieldsApiToInternal", () => {
it("should transform phone field", () => {
const bookingField: BookingField_2024_06_14 = {
const bookingField: InputBookingField_2024_06_14 = {
type: "phone",
slug: "phone",
label: "Your phone number",
@@ -168,9 +98,9 @@ describe("transformApiEventTypeBookingFields", () => {
placeholder: "123456789",
};
const input: BookingField_2024_06_14[] = [bookingField];
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
@@ -189,13 +119,13 @@ describe("transformApiEventTypeBookingFields", () => {
},
];
const result = transformApiEventTypeBookingFields(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform address field", () => {
const bookingField: BookingField_2024_06_14 = {
const bookingField: InputBookingField_2024_06_14 = {
type: "address",
slug: "address",
label: "Your address",
@@ -203,9 +133,9 @@ describe("transformApiEventTypeBookingFields", () => {
placeholder: "1234 Main St",
};
const input: BookingField_2024_06_14[] = [bookingField];
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
@@ -224,13 +154,13 @@ describe("transformApiEventTypeBookingFields", () => {
},
];
const result = transformApiEventTypeBookingFields(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform text field", () => {
const bookingField: BookingField_2024_06_14 = {
const bookingField: InputBookingField_2024_06_14 = {
type: "text",
slug: "text",
label: "Your text",
@@ -238,9 +168,9 @@ describe("transformApiEventTypeBookingFields", () => {
placeholder: "Enter your text",
};
const input: BookingField_2024_06_14[] = [bookingField];
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
@@ -259,13 +189,13 @@ describe("transformApiEventTypeBookingFields", () => {
},
];
const result = transformApiEventTypeBookingFields(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform number field", () => {
const bookingField: BookingField_2024_06_14 = {
const bookingField: InputBookingField_2024_06_14 = {
type: "number",
slug: "number",
label: "Your number",
@@ -273,9 +203,9 @@ describe("transformApiEventTypeBookingFields", () => {
placeholder: "100",
};
const input: BookingField_2024_06_14[] = [bookingField];
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
@@ -294,13 +224,13 @@ describe("transformApiEventTypeBookingFields", () => {
},
];
const result = transformApiEventTypeBookingFields(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform textarea field", () => {
const bookingField: BookingField_2024_06_14 = {
const bookingField: InputBookingField_2024_06_14 = {
type: "textarea",
slug: "textarea",
label: "Your detailed information",
@@ -308,9 +238,9 @@ describe("transformApiEventTypeBookingFields", () => {
placeholder: "Detailed description here...",
};
const input: BookingField_2024_06_14[] = [bookingField];
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
@@ -329,13 +259,13 @@ describe("transformApiEventTypeBookingFields", () => {
},
];
const result = transformApiEventTypeBookingFields(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform select field", () => {
const bookingField: BookingField_2024_06_14 = {
const bookingField: InputBookingField_2024_06_14 = {
type: "select",
slug: "select",
label: "Your selection",
@@ -344,9 +274,9 @@ describe("transformApiEventTypeBookingFields", () => {
options: ["Option 1", "Option 2"],
};
const input: BookingField_2024_06_14[] = [bookingField];
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
@@ -362,17 +292,17 @@ describe("transformApiEventTypeBookingFields", () => {
editable: "user",
required: bookingField.required,
placeholder: bookingField.placeholder,
options: transformSelectOptions(bookingField.options),
options: transformSelectOptionsApiToInternal(bookingField.options),
},
];
const result = transformApiEventTypeBookingFields(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform multiselect field", () => {
const bookingField: BookingField_2024_06_14 = {
const bookingField: InputBookingField_2024_06_14 = {
type: "multiselect",
slug: "multiselect",
label: "Your multiple selections",
@@ -380,9 +310,9 @@ describe("transformApiEventTypeBookingFields", () => {
options: ["Option 1", "Option 2"],
};
const input: BookingField_2024_06_14[] = [bookingField];
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
@@ -398,17 +328,17 @@ describe("transformApiEventTypeBookingFields", () => {
editable: "user",
required: bookingField.required,
placeholder: "",
options: transformSelectOptions(bookingField.options),
options: transformSelectOptionsApiToInternal(bookingField.options),
},
];
const result = transformApiEventTypeBookingFields(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform multiemail field", () => {
const bookingField: BookingField_2024_06_14 = {
const bookingField: InputBookingField_2024_06_14 = {
type: "multiemail",
slug: "multiemail",
label: "Your multiple emails",
@@ -416,9 +346,9 @@ describe("transformApiEventTypeBookingFields", () => {
placeholder: "example@example.com",
};
const input: BookingField_2024_06_14[] = [bookingField];
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
@@ -437,13 +367,13 @@ describe("transformApiEventTypeBookingFields", () => {
},
];
const result = transformApiEventTypeBookingFields(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform checkbox field", () => {
const bookingField: BookingField_2024_06_14 = {
const bookingField: InputBookingField_2024_06_14 = {
type: "checkbox",
slug: "checkbox",
label: "Your checkboxes",
@@ -451,9 +381,9 @@ describe("transformApiEventTypeBookingFields", () => {
options: ["Checkbox 1", "Checkbox 2"],
};
const input: BookingField_2024_06_14[] = [bookingField];
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
@@ -469,17 +399,17 @@ describe("transformApiEventTypeBookingFields", () => {
editable: "user",
required: bookingField.required,
placeholder: "",
options: transformSelectOptions(bookingField.options),
options: transformSelectOptionsApiToInternal(bookingField.options),
},
];
const result = transformApiEventTypeBookingFields(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform radio field", () => {
const bookingField: BookingField_2024_06_14 = {
const bookingField: InputBookingField_2024_06_14 = {
type: "radio",
slug: "radio",
label: "Your radio buttons",
@@ -487,9 +417,9 @@ describe("transformApiEventTypeBookingFields", () => {
options: ["Radio 1", "Radio 2"],
};
const input: BookingField_2024_06_14[] = [bookingField];
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
@@ -505,26 +435,26 @@ describe("transformApiEventTypeBookingFields", () => {
editable: "user",
required: bookingField.required,
placeholder: "",
options: transformSelectOptions(bookingField.options),
options: transformSelectOptionsApiToInternal(bookingField.options),
},
];
const result = transformApiEventTypeBookingFields(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform boolean field", () => {
const bookingField: BookingField_2024_06_14 = {
const bookingField: InputBookingField_2024_06_14 = {
type: "boolean",
slug: "boolean",
label: "Agree to terms?",
required: true,
};
const input: BookingField_2024_06_14[] = [bookingField];
const input: InputBookingField_2024_06_14[] = [bookingField];
const expectedOutput: UserField[] = [
const expectedOutput: CustomField[] = [
{
name: bookingField.slug,
type: bookingField.type,
@@ -543,13 +473,13 @@ describe("transformApiEventTypeBookingFields", () => {
},
];
const result = transformApiEventTypeBookingFields(input);
const result = transformBookingFieldsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
});
describe("transformApiEventTypeIntervalLimits", () => {
describe("transformIntervalLimitsApiToInternal", () => {
it("should transform booking limits count or booking limits duration", () => {
const input: BookingLimitsCount_2024_06_14 = {
day: 2,
@@ -564,13 +494,13 @@ describe("transformApiEventTypeIntervalLimits", () => {
PER_MONTH: 22,
PER_YEAR: 33,
};
const result = transformApiEventTypeIntervalLimits(input);
const result = transformIntervalLimitsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
});
describe("transformApiEventTypeFutureBookingLimits", () => {
describe("transformFutureBookingLimitsApiToInternal", () => {
it("should transform range type", () => {
const input: BookingWindow_2024_06_14 = {
type: "range",
@@ -583,7 +513,7 @@ describe("transformApiEventTypeFutureBookingLimits", () => {
periodEndDate: new Date("2024-08-28"),
};
const result = transformApiEventTypeFutureBookingLimits(input);
const result = transformFutureBookingLimitsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -600,7 +530,7 @@ describe("transformApiEventTypeFutureBookingLimits", () => {
periodCountCalendarDays: true,
};
const result = transformApiEventTypeFutureBookingLimits(input);
const result = transformFutureBookingLimitsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -617,7 +547,7 @@ describe("transformApiEventTypeFutureBookingLimits", () => {
periodCountCalendarDays: true,
};
const result = transformApiEventTypeFutureBookingLimits(input);
const result = transformFutureBookingLimitsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -634,7 +564,7 @@ describe("transformApiEventTypeFutureBookingLimits", () => {
periodCountCalendarDays: false,
};
const result = transformApiEventTypeFutureBookingLimits(input);
const result = transformFutureBookingLimitsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -651,13 +581,13 @@ describe("transformApiEventTypeFutureBookingLimits", () => {
periodCountCalendarDays: false,
};
const result = transformApiEventTypeFutureBookingLimits(input);
const result = transformFutureBookingLimitsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
});
describe("transformApiEventTypeRecurrence", () => {
describe("transformRecurrenceApiToInternal", () => {
it("should transform recurrence", () => {
const input: Recurrence_2024_06_14 = {
frequency: FrequencyInput.weekly,
@@ -669,7 +599,7 @@ describe("transformApiEventTypeRecurrence", () => {
count: 10,
freq: 2,
};
const result = transformApiEventTypeRecurrence(input);
const result = transformRecurrenceApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
@@ -0,0 +1,51 @@
import { type CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import type { CustomField } from "../internal-to-api";
export function transformBookingFieldsApiToInternal(
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]
) {
if (!inputBookingFields) {
return [];
}
const customBookingFields = inputBookingFields.map((field) => {
const commonFields: CustomField = {
name: field.slug,
type: field.type,
label: field.label,
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: field.required,
placeholder: "placeholder" in field && field.placeholder ? field.placeholder : "",
};
const options =
"options" in field && field.options ? transformSelectOptionsApiToInternal(field.options) : undefined;
if (!options) {
return commonFields;
}
return {
...commonFields,
options,
};
});
return customBookingFields;
}
export function transformSelectOptionsApiToInternal(options: string[]) {
return options.map((option) => ({
label: option,
value: option,
}));
}
@@ -0,0 +1,41 @@
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookingWindowPeriodOutputTypeEnum_2024_06_14,
} from "@calcom/platform-enums/monorepo";
import {
type CreateEventTypeInput_2024_06_14,
type BusinessDaysWindow_2024_06_14,
type RangeWindow_2024_06_14,
type TransformFutureBookingsLimitSchema_2024_06_14,
} from "@calcom/platform-types";
export function transformFutureBookingLimitsApiToInternal(
inputBookingLimits: CreateEventTypeInput_2024_06_14["bookingWindow"]
): TransformFutureBookingsLimitSchema_2024_06_14 | undefined {
switch (inputBookingLimits?.type) {
case BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays:
return {
periodDays: (inputBookingLimits as BusinessDaysWindow_2024_06_14).value,
periodType: !!(inputBookingLimits as BusinessDaysWindow_2024_06_14).rolling
? BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING_WINDOW
: BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING,
periodCountCalendarDays: false,
};
case BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays:
return {
periodDays: (inputBookingLimits as BusinessDaysWindow_2024_06_14).value,
periodType: !!(inputBookingLimits as BusinessDaysWindow_2024_06_14).rolling
? BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING_WINDOW
: BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING,
periodCountCalendarDays: true,
};
case BookingWindowPeriodInputTypeEnum_2024_06_14.range:
return {
periodType: BookingWindowPeriodOutputTypeEnum_2024_06_14.RANGE,
periodStartDate: new Date((inputBookingLimits as RangeWindow_2024_06_14).value[0]),
periodEndDate: new Date((inputBookingLimits as RangeWindow_2024_06_14).value[1]),
};
default:
return undefined;
}
}
@@ -0,0 +1,5 @@
export * from "./booking-fields";
export * from "./locations";
export * from "./recurrence";
export * from "./interval-limits";
export * from "./future-booking-limits";
@@ -0,0 +1,20 @@
import { BookingLimitsEnum_2024_06_14 } from "@calcom/platform-enums/monorepo";
import {
type CreateEventTypeInput_2024_06_14,
type BookingLimitsKeyOutputType_2024_06_14,
type TransformBookingLimitsSchema_2024_06_14,
} from "@calcom/platform-types";
export function transformIntervalLimitsApiToInternal(
inputBookingLimits: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]
) {
const res: TransformBookingLimitsSchema_2024_06_14 = {};
inputBookingLimits &&
Object.entries(inputBookingLimits).map(([key, value]) => {
const outputKey: BookingLimitsKeyOutputType_2024_06_14 = BookingLimitsEnum_2024_06_14[
key as keyof typeof BookingLimitsEnum_2024_06_14
] satisfies BookingLimitsKeyOutputType_2024_06_14;
res[outputKey] = value;
});
return res;
}
@@ -0,0 +1,78 @@
import { z } from "zod";
import { type CreateEventTypeInput_2024_06_14, type Integration_2024_06_14 } from "@calcom/platform-types";
const integrationsMapping: Record<Integration_2024_06_14, "integrations:daily"> = {
"cal-video": "integrations:daily",
};
export function transformLocationsApiToInternal(
inputLocations: CreateEventTypeInput_2024_06_14["locations"]
) {
if (!inputLocations) {
return [];
}
return inputLocations.map((location) => {
const { type } = location;
switch (type) {
case "address":
return {
type: "inPerson",
address: location.address,
displayLocationPublicly: location.public,
} satisfies InPersonLocation;
case "link":
return {
type: "link",
link: location.link,
displayLocationPublicly: location.public,
} satisfies LinkLocation;
case "integration":
const integrationLabel = integrationsMapping[location.integration];
return { type: integrationLabel } satisfies IntegrationLocation;
case "phone":
return {
type: "userPhone",
hostPhoneNumber: location.phone,
displayLocationPublicly: location.public,
} satisfies UserPhoneLocation;
default:
throw new Error(`Unsupported location type '${type}'`);
}
});
}
const integrationsMappingSchema = {
"cal-video": z.literal("integrations:daily"),
};
const InPersonSchema = z.object({
type: z.literal("inPerson"),
address: z.string(),
displayLocationPublicly: z.boolean().default(false),
});
const LinkSchema = z.object({
type: z.literal("link"),
link: z.string().url(),
displayLocationPublicly: z.boolean().default(false),
});
const IntegrationSchema = z.object({
type: z.union([integrationsMappingSchema["cal-video"], integrationsMappingSchema["cal-video"]]),
});
const UserPhoneSchema = z.object({
type: z.literal("userPhone"),
hostPhoneNumber: z.string(),
displayLocationPublicly: z.boolean().default(false),
});
type InPersonLocation = z.infer<typeof InPersonSchema>;
type LinkLocation = z.infer<typeof LinkSchema>;
type IntegrationLocation = z.infer<typeof IntegrationSchema>;
type UserPhoneLocation = z.infer<typeof UserPhoneSchema>;
const TransformedLocationSchema = z.union([InPersonSchema, LinkSchema, IntegrationSchema, UserPhoneSchema]);
export const TransformedLocationsSchema = z.array(TransformedLocationSchema);
@@ -0,0 +1,16 @@
import { Frequency } from "@calcom/platform-enums/monorepo";
import {
type CreateEventTypeInput_2024_06_14,
type TransformRecurringEventSchema_2024_06_14,
} from "@calcom/platform-types";
export function transformRecurrenceApiToInternal(
recurrence: CreateEventTypeInput_2024_06_14["recurrence"]
): TransformRecurringEventSchema_2024_06_14 | undefined {
if (!recurrence) return undefined;
return {
interval: recurrence.interval,
count: recurrence.occurrences,
freq: Frequency[recurrence.frequency as keyof typeof Frequency],
} satisfies TransformRecurringEventSchema_2024_06_14;
}
@@ -1,2 +1,2 @@
export * from "./api-request";
export * from "./api-response";
export * from "./api-to-internal";
export * from "./internal-to-api";
@@ -0,0 +1,419 @@
import { z } from "zod";
import type {
OutputBookingField_2024_06_14,
DefaultFieldOutput_2024_06_14,
CustomFieldOutput_2024_06_14,
} from "@calcom/platform-types";
export function transformBookingFieldsInternalToApi(
databaseBookingFields: (SystemField | CustomField)[]
): OutputBookingField_2024_06_14[] {
const defaultFields: SystemField[] = databaseBookingFields.filter(
(field): field is SystemField => field.editable === "system" || field.editable === "system-but-optional"
);
const customFields: CustomField[] = databaseBookingFields.filter(
(field): field is CustomField => field.editable === "user"
);
const responseDefaultFields: DefaultFieldOutput_2024_06_14[] = defaultFields.map((field) => {
switch (field.name) {
case "name":
return {
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
};
case "email":
return {
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
};
case "location":
return {
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
};
case "rescheduleReason":
return {
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
};
case "title":
return {
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
};
case "notes":
return {
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
};
case "guests":
return {
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
};
default:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
throw new Error(`Unsupported default booking field '${field.name}'.`);
}
});
const responseCustomFields: CustomFieldOutput_2024_06_14[] = customFields.map((field) => {
switch (field.type) {
case "phone":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "address":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "text":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "number":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "textarea":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "multiemail":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
};
case "boolean":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
};
case "select":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
options: field.options ? field.options.map((option) => option.value) : [],
};
case "multiselect":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
};
case "checkbox":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
};
case "radio":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
options: field.options ? field.options?.map((option) => option.value) : [],
};
default:
throw new Error(`Unsupported booking field type '${field.type}'.`);
}
});
return [...responseDefaultFields, ...responseCustomFields];
}
const CustomFieldTypeEnum = z.enum([
"number",
"boolean",
"address",
"text",
"textarea",
"phone",
"multiemail",
"select",
"multiselect",
"checkbox",
"radio",
"radioInput",
]);
const CustomFieldsSchema = z.object({
name: z.string(),
type: CustomFieldTypeEnum,
label: z.string(),
sources: z.array(
z.object({
id: z.literal("user"),
type: z.literal("user"),
label: z.literal("User"),
fieldRequired: z.literal(true),
})
),
editable: z.literal("user"),
required: z.boolean(),
placeholder: z.string().optional(),
options: z
.array(
z.object({
label: z.string(),
value: z.string(),
})
)
.optional(),
});
const SystemFieldSchema = z.object({
defaultLabel: z.string(),
label: z.string().optional(),
editable: z.enum(["system-but-optional", "system"]),
sources: z.array(
z.object({
id: z.literal("default"),
type: z.literal("default"),
label: z.literal("Default"),
})
),
views: z
.array(
z.object({
id: z.enum(["reschedule"]),
label: z.string(),
})
)
.optional(),
defaultPlaceholder: z.enum(["", "share_additional_notes", "email", "reschedule_placeholder"]).optional(),
hidden: z.boolean().optional(),
required: z.boolean(),
hideWhenJustOneOption: z.boolean().optional(),
getOptionsAt: z.enum(["locations"]).optional(),
optionsInputs: z
.object({
attendeeInPerson: z.object({
type: z.literal("address"),
required: z.boolean(),
placeholder: z.string(),
}),
phone: z.object({
type: z.literal("phone"),
required: z.boolean(),
placeholder: z.string(),
}),
})
.optional(),
});
const NameSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("name"),
type: z.literal("name"),
required: z.literal(true),
});
const EmailSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("email"),
type: z.literal("email"),
required: z.literal(true),
});
const RescheduleReasonSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("rescheduleReason"),
type: z.literal("textarea"),
required: z.literal(false),
});
const LocationReasonSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("location"),
type: z.literal("radioInput"),
required: z.literal(false),
});
const TitleSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("title"),
type: z.literal("text"),
required: z.literal(true),
});
const NotesSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("notes"),
type: z.literal("textarea"),
required: z.literal(false),
});
const GuestsSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("guests"),
type: z.literal("multiemail"),
required: z.literal(false),
});
type NameSystemField = z.infer<typeof NameSystemFieldSchema>;
type EmailSystemField = z.infer<typeof EmailSystemFieldSchema>;
type RescheduleReasonSystemField = z.infer<typeof RescheduleReasonSystemFieldSchema>;
type LocationReasonSystemField = z.infer<typeof LocationReasonSystemFieldSchema>;
type TitleSystemField = z.infer<typeof TitleSystemFieldSchema>;
type NotesSystemField = z.infer<typeof NotesSystemFieldSchema>;
type GuestsSystemField = z.infer<typeof GuestsSystemFieldSchema>;
export type SystemField =
| NameSystemField
| EmailSystemField
| RescheduleReasonSystemField
| LocationReasonSystemField
| TitleSystemField
| NotesSystemField
| GuestsSystemField;
export type CustomField = z.infer<typeof CustomFieldsSchema>;
const SystemFieldsSchema = z.union([
NameSystemFieldSchema,
EmailSystemFieldSchema,
LocationReasonSystemFieldSchema,
RescheduleReasonSystemFieldSchema,
TitleSystemFieldSchema,
NotesSystemFieldSchema,
GuestsSystemFieldSchema,
]);
export const BookingFieldsSchema = z.array(z.union([CustomFieldsSchema, SystemFieldsSchema]));
export const systemBeforeFieldName: NameSystemField = {
type: "name",
name: "name",
editable: "system",
defaultLabel: "your_name",
required: true,
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
};
export const systemBeforeFieldEmail: EmailSystemField = {
defaultLabel: "email_address",
type: "email",
name: "email",
required: true,
editable: "system",
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
};
export const systemBeforeFieldLocation: LocationReasonSystemField = {
defaultLabel: "location",
type: "radioInput",
name: "location",
editable: "system",
hideWhenJustOneOption: true,
required: false,
getOptionsAt: "locations",
optionsInputs: {
attendeeInPerson: {
type: "address",
required: true,
placeholder: "",
},
phone: {
type: "phone",
required: true,
placeholder: "",
},
},
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
};
export const systemAfterFieldRescheduleReason: RescheduleReasonSystemField = {
defaultLabel: "reason_for_reschedule",
type: "textarea",
editable: "system-but-optional",
name: "rescheduleReason",
defaultPlaceholder: "reschedule_placeholder",
required: false,
views: [
{
id: "reschedule",
label: "Reschedule View",
},
],
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
};
@@ -0,0 +1,44 @@
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookingWindowPeriodOutputTypeEnum_2024_06_14,
} from "@calcom/platform-enums/monorepo";
import type {
TransformFutureBookingsLimitSchema_2024_06_14,
BookingWindow_2024_06_14,
RangeWindow_2024_06_14,
CalendarDaysWindow_2024_06_14,
BusinessDaysWindow_2024_06_14,
} from "@calcom/platform-types";
export function transformFutureBookingLimitsInternalToApi(
transformedFutureBookingsLimitsFields: TransformFutureBookingsLimitSchema_2024_06_14
): BookingWindow_2024_06_14 | undefined {
switch (transformedFutureBookingsLimitsFields?.periodType) {
case BookingWindowPeriodOutputTypeEnum_2024_06_14.RANGE:
return {
type: BookingWindowPeriodInputTypeEnum_2024_06_14.range,
value: [
transformedFutureBookingsLimitsFields?.periodStartDate?.toISOString().split("T")[0],
transformedFutureBookingsLimitsFields?.periodEndDate?.toISOString().split("T")[0],
],
} as RangeWindow_2024_06_14;
case BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING_WINDOW:
return {
type: transformedFutureBookingsLimitsFields.periodCountCalendarDays
? BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays
: BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays,
value: transformedFutureBookingsLimitsFields.periodDays,
rolling: true,
} as CalendarDaysWindow_2024_06_14 | BusinessDaysWindow_2024_06_14;
case BookingWindowPeriodOutputTypeEnum_2024_06_14.ROLLING:
return {
type: transformedFutureBookingsLimitsFields.periodCountCalendarDays
? BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays
: BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays,
value: transformedFutureBookingsLimitsFields.periodDays,
rolling: false,
} as CalendarDaysWindow_2024_06_14 | BusinessDaysWindow_2024_06_14;
default:
return undefined;
}
}
@@ -0,0 +1,5 @@
export * from "./booking-fields";
export * from "./locations";
export * from "./recurrence";
export * from "./interval-limits";
export * from "./future-booking-limits";
@@ -10,16 +10,16 @@ import type {
TransformRecurringEventSchema_2024_06_14,
} from "@calcom/platform-types";
import type { UserField } from "./api-request";
import {
getResponseEventTypeLocations,
getResponseEventTypeBookingFields,
getResponseEventTypeIntervalLimits,
getResponseEventTypeFutureBookingLimits,
getResponseEventTypeRecurrence,
} from "./api-response";
transformLocationsInternalToApi,
transformBookingFieldsInternalToApi,
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
} from ".";
import type { CustomField } from "./booking-fields";
describe("getResponseEventTypeLocations", () => {
describe("transformLocationsInternalToApi", () => {
it("should reverse transform address location", () => {
const transformedLocation = [
{
@@ -37,7 +37,7 @@ describe("getResponseEventTypeLocations", () => {
},
];
const result = getResponseEventTypeLocations(transformedLocation);
const result = transformLocationsInternalToApi(transformedLocation);
expect(result).toEqual(expectedOutput);
});
@@ -59,7 +59,7 @@ describe("getResponseEventTypeLocations", () => {
},
];
const result = getResponseEventTypeLocations(transformedLocation);
const result = transformLocationsInternalToApi(transformedLocation);
expect(result).toEqual(expectedOutput);
});
@@ -81,7 +81,7 @@ describe("getResponseEventTypeLocations", () => {
},
];
const result = getResponseEventTypeLocations(transformedLocation);
const result = transformLocationsInternalToApi(transformedLocation);
expect(result).toEqual(expectedOutput);
});
@@ -100,85 +100,15 @@ describe("getResponseEventTypeLocations", () => {
},
];
const result = getResponseEventTypeLocations(transformedLocation);
const result = transformLocationsInternalToApi(transformedLocation);
expect(result).toEqual(expectedOutput);
});
});
describe("getResponseEventTypeBookingFields", () => {
it("should reverse transform name field", () => {
const transformedField: UserField[] = [
{
name: "your-name",
type: "name",
label: "Your name",
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: true,
placeholder: "alice",
},
];
const expectedOutput = [
{
type: "name",
slug: "your-name",
label: "Your name",
required: true,
placeholder: "alice",
},
];
const result = getResponseEventTypeBookingFields(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform email field", () => {
const transformedField: UserField[] = [
{
name: "your-email",
type: "email",
label: "Your email",
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: true,
placeholder: "example@example.com",
},
];
const expectedOutput = [
{
type: "email",
slug: "your-email",
label: "Your email",
required: true,
placeholder: "example@example.com",
},
];
const result = getResponseEventTypeBookingFields(transformedField);
expect(result).toEqual(expectedOutput);
});
describe("transformBookingFieldsInternalToApi", () => {
it("should reverse transform phone field", () => {
const transformedField: UserField[] = [
const transformedField: CustomField[] = [
{
name: "your-phone",
type: "phone",
@@ -199,6 +129,7 @@ describe("getResponseEventTypeBookingFields", () => {
const expectedOutput = [
{
isDefault: false,
type: "phone",
slug: "your-phone",
label: "Your phone number",
@@ -207,13 +138,13 @@ describe("getResponseEventTypeBookingFields", () => {
},
];
const result = getResponseEventTypeBookingFields(transformedField);
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform address field", () => {
const transformedField: UserField[] = [
const transformedField: CustomField[] = [
{
name: "your-address",
type: "address",
@@ -234,6 +165,7 @@ describe("getResponseEventTypeBookingFields", () => {
const expectedOutput = [
{
isDefault: false,
type: "address",
slug: "your-address",
label: "Your address",
@@ -242,13 +174,13 @@ describe("getResponseEventTypeBookingFields", () => {
},
];
const result = getResponseEventTypeBookingFields(transformedField);
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform text field", () => {
const transformedField: UserField[] = [
const transformedField: CustomField[] = [
{
name: "your-text",
type: "text",
@@ -269,6 +201,7 @@ describe("getResponseEventTypeBookingFields", () => {
const expectedOutput = [
{
isDefault: false,
type: "text",
slug: "your-text",
label: "Your text",
@@ -277,13 +210,13 @@ describe("getResponseEventTypeBookingFields", () => {
},
];
const result = getResponseEventTypeBookingFields(transformedField);
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform number field", () => {
const transformedField: UserField[] = [
const transformedField: CustomField[] = [
{
name: "your-number",
type: "number",
@@ -304,6 +237,7 @@ describe("getResponseEventTypeBookingFields", () => {
const expectedOutput = [
{
isDefault: false,
type: "number",
slug: "your-number",
label: "Your number",
@@ -312,13 +246,13 @@ describe("getResponseEventTypeBookingFields", () => {
},
];
const result = getResponseEventTypeBookingFields(transformedField);
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform textarea field", () => {
const transformedField: UserField[] = [
const transformedField: CustomField[] = [
{
name: "your-textarea",
type: "textarea",
@@ -339,6 +273,7 @@ describe("getResponseEventTypeBookingFields", () => {
const expectedOutput = [
{
isDefault: false,
type: "textarea",
slug: "your-textarea",
label: "Your detailed information",
@@ -347,13 +282,13 @@ describe("getResponseEventTypeBookingFields", () => {
},
];
const result = getResponseEventTypeBookingFields(transformedField);
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform select field", () => {
const transformedField: UserField[] = [
const transformedField: CustomField[] = [
{
name: "your-select",
type: "select",
@@ -378,6 +313,7 @@ describe("getResponseEventTypeBookingFields", () => {
const expectedOutput = [
{
isDefault: false,
type: "select",
slug: "your-select",
label: "Your selection",
@@ -387,13 +323,13 @@ describe("getResponseEventTypeBookingFields", () => {
},
];
const result = getResponseEventTypeBookingFields(transformedField);
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform multiselect field", () => {
const transformedField: UserField[] = [
const transformedField: CustomField[] = [
{
name: "your-multiselect",
type: "multiselect",
@@ -417,6 +353,7 @@ describe("getResponseEventTypeBookingFields", () => {
const expectedOutput = [
{
isDefault: false,
type: "multiselect",
slug: "your-multiselect",
label: "Your multiple selections",
@@ -425,13 +362,13 @@ describe("getResponseEventTypeBookingFields", () => {
},
];
const result = getResponseEventTypeBookingFields(transformedField);
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform multiemail field", () => {
const transformedField: UserField[] = [
const transformedField: CustomField[] = [
{
name: "your-multiemail",
type: "multiemail",
@@ -452,6 +389,7 @@ describe("getResponseEventTypeBookingFields", () => {
const expectedOutput = [
{
isDefault: false,
type: "multiemail",
slug: "your-multiemail",
label: "Your multiple emails",
@@ -460,13 +398,13 @@ describe("getResponseEventTypeBookingFields", () => {
},
];
const result = getResponseEventTypeBookingFields(transformedField);
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform checkbox field", () => {
const transformedField: UserField[] = [
const transformedField: CustomField[] = [
{
name: "your-checkbox",
type: "checkbox",
@@ -490,6 +428,7 @@ describe("getResponseEventTypeBookingFields", () => {
const expectedOutput = [
{
isDefault: false,
type: "checkbox",
slug: "your-checkbox",
label: "Your checkboxes",
@@ -498,13 +437,13 @@ describe("getResponseEventTypeBookingFields", () => {
},
];
const result = getResponseEventTypeBookingFields(transformedField);
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform radio field", () => {
const transformedField: UserField[] = [
const transformedField: CustomField[] = [
{
name: "your-radio",
type: "radio",
@@ -528,6 +467,7 @@ describe("getResponseEventTypeBookingFields", () => {
const expectedOutput = [
{
isDefault: false,
type: "radio",
slug: "your-radio",
label: "Your radio buttons",
@@ -536,13 +476,13 @@ describe("getResponseEventTypeBookingFields", () => {
},
];
const result = getResponseEventTypeBookingFields(transformedField);
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform boolean field", () => {
const transformedField: UserField[] = [
const transformedField: CustomField[] = [
{
name: "agree-to-terms",
type: "boolean",
@@ -563,6 +503,7 @@ describe("getResponseEventTypeBookingFields", () => {
const expectedOutput = [
{
isDefault: false,
type: "boolean",
slug: "agree-to-terms",
label: "Agree to terms?",
@@ -570,13 +511,13 @@ describe("getResponseEventTypeBookingFields", () => {
},
];
const result = getResponseEventTypeBookingFields(transformedField);
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
});
describe("getResponseEventTypeIntervalLimits", () => {
describe("transformIntervalLimitsInternalToApi", () => {
it("should reverse transform booking limits count or booking limits duration", () => {
const transformedField: TransformBookingLimitsSchema_2024_06_14 = {
PER_DAY: 2,
@@ -591,13 +532,13 @@ describe("getResponseEventTypeIntervalLimits", () => {
month: 22,
year: 33,
};
const result = getResponseEventTypeIntervalLimits(transformedField);
const result = transformIntervalLimitsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
});
describe("getResponseEventTypeFutureBookingLimits", () => {
describe("transformFutureBookingLimitsInternalToApi", () => {
it("should reverse transform range type", () => {
const transformedField: TransformFutureBookingsLimitSchema_2024_06_14 = {
periodType: "RANGE",
@@ -609,7 +550,7 @@ describe("getResponseEventTypeFutureBookingLimits", () => {
value: ["2024-08-06", "2024-08-28"],
};
const result = getResponseEventTypeFutureBookingLimits(transformedField);
const result = transformFutureBookingLimitsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
@@ -625,7 +566,7 @@ describe("getResponseEventTypeFutureBookingLimits", () => {
rolling: false,
};
const result = getResponseEventTypeFutureBookingLimits(transformedField);
const result = transformFutureBookingLimitsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
@@ -642,7 +583,7 @@ describe("getResponseEventTypeFutureBookingLimits", () => {
rolling: true,
};
const result = getResponseEventTypeFutureBookingLimits(transformedField);
const result = transformFutureBookingLimitsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
@@ -659,7 +600,7 @@ describe("getResponseEventTypeFutureBookingLimits", () => {
rolling: false,
};
const result = getResponseEventTypeFutureBookingLimits(transformedField);
const result = transformFutureBookingLimitsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
@@ -676,13 +617,13 @@ describe("getResponseEventTypeFutureBookingLimits", () => {
rolling: true,
};
const result = getResponseEventTypeFutureBookingLimits(transformedField);
const result = transformFutureBookingLimitsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
});
describe("getResponseEventTypeRecurrence", () => {
describe("transformRecurrenceInternalToApi", () => {
it("should reverse transform recurringEvent", () => {
const transformedField: TransformRecurringEventSchema_2024_06_14 = {
interval: 2,
@@ -695,7 +636,7 @@ describe("getResponseEventTypeRecurrence", () => {
interval: 2,
occurrences: 10,
};
const result = getResponseEventTypeRecurrence(transformedField);
const result = transformRecurrenceInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
@@ -0,0 +1,27 @@
import { BookingLimitsEnum_2024_06_14 } from "@calcom/platform-enums/monorepo";
import type {
BookingLimitsKeysInputType,
TransformBookingLimitsSchema_2024_06_14,
} from "@calcom/platform-types";
export function transformIntervalLimitsInternalToApi(
transformedBookingFields: TransformBookingLimitsSchema_2024_06_14 | null
) {
if (!transformedBookingFields) {
return undefined;
}
const res: { [K in BookingLimitsKeysInputType]?: number } = {};
transformedBookingFields &&
Object.entries(transformedBookingFields).map(([key, value]) => {
const outputKey: BookingLimitsKeysInputType | undefined = Object.keys(
BookingLimitsEnum_2024_06_14
).find(
(item) => BookingLimitsEnum_2024_06_14[item as keyof typeof BookingLimitsEnum_2024_06_14] === key
) as BookingLimitsKeysInputType;
if (outputKey) {
res[outputKey] = value as number;
}
});
return res;
}
@@ -0,0 +1,70 @@
import type {
AddressLocation_2024_06_14,
IntegrationLocation_2024_06_14,
LinkLocation_2024_06_14,
PhoneLocation_2024_06_14,
Integration_2024_06_14,
} from "@calcom/platform-types";
import type { transformLocationsApiToInternal } from "../api-to-internal";
const reverseIntegrationsMapping: Record<string, Integration_2024_06_14> = {
"integrations:daily": "cal-video",
};
export function transformLocationsInternalToApi(
transformedLocations: ReturnType<typeof transformLocationsApiToInternal>
) {
if (!transformedLocations) {
return [];
}
return transformedLocations.map((location) => {
switch (location.type) {
case "inPerson": {
if (!location.address) {
throw new Error("Address location must have an address");
}
const addressLocation: AddressLocation_2024_06_14 = {
type: "address",
address: location.address,
public: location.displayLocationPublicly,
};
return addressLocation;
}
case "link": {
if (!location.link) {
throw new Error("Link location must have a link");
}
const linkLocation: LinkLocation_2024_06_14 = {
type: "link",
link: location.link,
public: location.displayLocationPublicly,
};
return linkLocation;
}
case "userPhone": {
if (!location.hostPhoneNumber) {
throw new Error("Phone location must have a phone number");
}
const phoneLocation: PhoneLocation_2024_06_14 = {
type: "phone",
phone: location.hostPhoneNumber,
public: location.displayLocationPublicly,
};
return phoneLocation;
}
default: {
const integrationType = reverseIntegrationsMapping[location.type];
if (!integrationType) {
throw new Error(`Unsupported integration type '${location.type}'.`);
}
const integration: IntegrationLocation_2024_06_14 = {
type: "integration",
integration: integrationType,
};
return integration;
}
}
});
}
@@ -0,0 +1,12 @@
import { Frequency, FrequencyInput } from "@calcom/platform-enums/monorepo";
import type { Recurrence_2024_06_14, TransformRecurringEventSchema_2024_06_14 } from "@calcom/platform-types";
export function transformRecurrenceInternalToApi(
transformRecurringEvent: TransformRecurringEventSchema_2024_06_14
): Recurrence_2024_06_14 {
return {
interval: transformRecurringEvent.interval,
occurrences: transformRecurringEvent.count,
frequency: FrequencyInput[Frequency[transformRecurringEvent.freq] as keyof typeof FrequencyInput],
} satisfies Recurrence_2024_06_14;
}
@@ -1,13 +1,20 @@
import type { BookerProps } from "@calcom/features/bookings/Booker";
import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier";
import { defaultEvents } from "@calcom/lib/defaultEvents";
import type { UserField, SystemField } from "@calcom/lib/event-types/transformers";
import type { CustomField, SystemField } from "@calcom/lib/event-types/transformers";
import {
transformApiEventTypeLocations,
transformApiEventTypeBookingFields,
transformLocationsApiToInternal,
transformBookingFieldsApiToInternal,
systemBeforeFieldName,
systemBeforeFieldEmail,
systemBeforeFieldLocation,
systemAfterFieldRescheduleReason,
} from "@calcom/lib/event-types/transformers";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import type { EventTypeOutput_2024_06_14, TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
import type {
CustomFieldOutput_2024_06_14,
EventTypeOutput_2024_06_14,
TeamEventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
import {
bookerLayoutOptions,
BookerLayouts,
@@ -184,7 +191,7 @@ function isDefaultEvent(eventSlug: string) {
}
function getLocations(locations: EventTypeOutput_2024_06_14["locations"]) {
const transformed = transformApiEventTypeLocations(locations);
const transformed = transformLocationsApiToInternal(locations);
const withPrivateHidden = transformed.map((location) => {
const { displayLocationPublicly, type } = location;
@@ -210,133 +217,29 @@ function getLocations(locations: EventTypeOutput_2024_06_14["locations"]) {
}
function getBookingFields(bookingFields: EventTypeOutput_2024_06_14["bookingFields"]) {
const transformedBookingFields: (SystemField | UserField)[] =
transformApiEventTypeBookingFields(bookingFields);
// These fields should be added before other user fields
const systemBeforeFields: SystemField[] = [
{
type: "name",
// This is the `name` of the main field
name: "name",
editable: "system",
// This Label is used in Email only as of now.
defaultLabel: "your_name",
required: true,
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
},
{
defaultLabel: "email_address",
type: "email",
name: "email",
required: true,
editable: "system",
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
},
{
defaultLabel: "location",
type: "radioInput",
name: "location",
editable: "system",
hideWhenJustOneOption: true,
required: false,
getOptionsAt: "locations",
optionsInputs: {
attendeeInPerson: {
type: "address",
required: true,
placeholder: "",
},
phone: {
type: "phone",
required: true,
placeholder: "",
},
},
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
},
systemBeforeFieldName,
systemBeforeFieldEmail,
systemBeforeFieldLocation,
];
// These fields should be added after other user fields
const systemAfterFields: SystemField[] = [
{
defaultLabel: "reason_for_reschedule",
type: "textarea",
editable: "system-but-optional",
name: "rescheduleReason",
defaultPlaceholder: "reschedule_placeholder",
required: false,
views: [
{
id: "reschedule",
label: "Reschedule View",
},
],
sources: [
{
label: "Default",
id: "default",
type: "default",
},
],
},
const transformedCustomFields: CustomField[] = transformBookingFieldsApiToInternal(
bookingFields.filter((field) => isCustomField(field))
);
const systemAfterFields: SystemField[] = [systemAfterFieldRescheduleReason];
const transformedBookingFields: (SystemField | CustomField)[] = [
...systemBeforeFields,
...transformedCustomFields,
...systemAfterFields,
];
const missingSystemBeforeFields: SystemField[] = [];
for (const field of systemBeforeFields) {
const existingBookingFieldIndex = transformedBookingFields.findIndex(
(f) => getFieldIdentifier(f.name) === getFieldIdentifier(field.name)
);
// Only do a push, we must not update existing system fields as user could have modified any property in it,
if (existingBookingFieldIndex === -1) {
missingSystemBeforeFields.push(field);
} else {
// Adding the fields from Code first and then fields from DB. Allows, the code to push new properties to the field
transformedBookingFields[existingBookingFieldIndex] = {
...field,
...transformedBookingFields[existingBookingFieldIndex],
};
}
}
transformedBookingFields.push(...missingSystemBeforeFields);
const missingSystemAfterFields: SystemField[] = [];
for (const field of systemAfterFields) {
const existingBookingFieldIndex = transformedBookingFields.findIndex(
(f) => getFieldIdentifier(f.name) === getFieldIdentifier(field.name)
);
// Only do a push, we must not update existing system fields as user could have modified any property in it,
if (existingBookingFieldIndex === -1) {
missingSystemAfterFields.push(field);
} else {
transformedBookingFields[existingBookingFieldIndex] = {
// Adding the fields from Code first and then fields from DB. Allows, the code to push new properties to the field
...field,
...transformedBookingFields[existingBookingFieldIndex],
};
}
}
transformedBookingFields.push(...missingSystemAfterFields);
return eventTypeBookingFields.brand<"HAS_SYSTEM_FIELDS">().parse(transformedBookingFields);
}
function isCustomField(
field: EventTypeOutput_2024_06_14["bookingFields"][number]
): field is CustomFieldOutput_2024_06_14 {
return !field.isDefault;
}
+3
View File
@@ -1,3 +1,6 @@
## 0.0.36
Released to support PR https://github.com/calcom/cal.com/pull/16685
## 0.0.31
PR https://github.com/calcom/cal.com/pull/16414 fixed issue of deleting and rescheduling recurring events.
+20 -11
View File
@@ -94,22 +94,31 @@ export { eventTypeBookingFields, eventTypeLocations } from "@calcom/prisma/zod-u
export { EventTypeMetaDataSchema, userMetadata } from "@calcom/prisma/zod-utils";
export {
transformApiEventTypeBookingFields,
transformApiEventTypeIntervalLimits,
transformApiEventTypeLocations,
getResponseEventTypeLocations,
getResponseEventTypeBookingFields,
// note(Lauris): Api to internal
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
transformRecurrenceApiToInternal,
// note(Lauris): Internal to api
transformBookingFieldsInternalToApi,
transformLocationsInternalToApi,
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
// note(Lauris): schemas
TransformedLocationsSchema,
BookingFieldsSchema,
getResponseEventTypeIntervalLimits,
getResponseEventTypeFutureBookingLimits,
transformApiEventTypeFutureBookingLimits,
transformApiEventTypeRecurrence,
getResponseEventTypeRecurrence,
// note(Lauris): constants
systemBeforeFieldName,
systemBeforeFieldEmail,
systemBeforeFieldLocation,
systemAfterFieldRescheduleReason,
} from "@calcom/lib/event-types/transformers";
export type { SystemField, CustomField } from "@calcom/lib/event-types/transformers";
export { parseBookingLimit } from "@calcom/lib";
export type { SystemField, UserField } from "@calcom/lib/event-types/transformers";
export { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
export { dynamicEvent } from "@calcom/lib/defaultEvents";
@@ -5,9 +5,7 @@ import { IsString, IsBoolean, IsArray, IsIn, IsOptional } from "class-validator"
import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator";
import { registerDecorator, validate, ValidatorConstraint } from "class-validator";
const bookingFields = [
"name",
"email",
const inputBookingFieldTypes = [
"phone",
"address",
"text",
@@ -21,53 +19,8 @@ const bookingFields = [
"boolean",
] as const;
export class NameField_2024_06_14 {
@IsIn(bookingFields)
@DocsProperty()
type!: "name";
@IsString()
@DocsProperty()
slug!: string;
@IsString()
@DocsProperty()
label!: string;
@IsBoolean()
@DocsProperty()
required!: boolean;
@IsString()
@IsOptional()
@DocsProperty()
placeholder?: string;
}
export class EmailField_2024_06_14 {
@IsIn(bookingFields)
@DocsProperty()
type!: "email";
@IsString()
@DocsProperty()
slug!: string;
@IsString()
@DocsProperty()
label!: string;
@IsBoolean()
@DocsProperty()
required!: boolean;
@IsString()
@IsOptional()
@DocsProperty()
placeholder?: string;
}
export class PhoneField_2024_06_14 {
@IsIn(bookingFields)
export class PhoneFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty()
type!: "phone";
@@ -89,8 +42,8 @@ export class PhoneField_2024_06_14 {
placeholder?: string;
}
export class AddressField_2024_06_14 {
@IsIn(bookingFields)
export class AddressFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty()
type!: "address";
@@ -113,8 +66,8 @@ export class AddressField_2024_06_14 {
placeholder?: string;
}
export class TextField_2024_06_14 {
@IsIn(bookingFields)
export class TextFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty()
type!: "text";
@@ -137,8 +90,8 @@ export class TextField_2024_06_14 {
placeholder?: string;
}
export class NumberField_2024_06_14 {
@IsIn(bookingFields)
export class NumberFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty()
type!: "number";
@@ -161,8 +114,8 @@ export class NumberField_2024_06_14 {
placeholder?: string;
}
export class TextAreaField_2024_06_14 {
@IsIn(bookingFields)
export class TextAreaFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty()
type!: "textarea";
@@ -185,8 +138,8 @@ export class TextAreaField_2024_06_14 {
placeholder?: string;
}
export class SelectField_2024_06_14 {
@IsIn(bookingFields)
export class SelectFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty()
type!: "select";
@@ -213,8 +166,8 @@ export class SelectField_2024_06_14 {
options!: string[];
}
export class MultiSelectField_2024_06_14 {
@IsIn(bookingFields)
export class MultiSelectFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty()
type!: "multiselect";
@@ -235,8 +188,8 @@ export class MultiSelectField_2024_06_14 {
options!: string[];
}
export class MultiEmailField_2024_06_14 {
@IsIn(bookingFields)
export class MultiEmailFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty()
type!: "multiemail";
@@ -259,8 +212,8 @@ export class MultiEmailField_2024_06_14 {
placeholder?: string;
}
export class CheckboxGroupField_2024_06_14 {
@IsIn(bookingFields)
export class CheckboxGroupFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty()
type!: "checkbox";
@@ -281,8 +234,8 @@ export class CheckboxGroupField_2024_06_14 {
options!: string[];
}
export class RadioGroupField_2024_06_14 {
@IsIn(bookingFields)
export class RadioGroupFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty()
type!: "radio";
@@ -303,8 +256,8 @@ export class RadioGroupField_2024_06_14 {
options!: string[];
}
export class BooleanField_2024_06_14 {
@IsIn(bookingFields)
export class BooleanFieldInput_2024_06_14 {
@IsIn(inputBookingFieldTypes)
@DocsProperty()
type!: "boolean";
@@ -321,39 +274,37 @@ export class BooleanField_2024_06_14 {
required!: boolean;
}
export type BookingField_2024_06_14 =
| NameField_2024_06_14
| EmailField_2024_06_14
| PhoneField_2024_06_14
| AddressField_2024_06_14
| TextField_2024_06_14
| NumberField_2024_06_14
| TextAreaField_2024_06_14
| SelectField_2024_06_14
| MultiSelectField_2024_06_14
| MultiEmailField_2024_06_14
| CheckboxGroupField_2024_06_14
| RadioGroupField_2024_06_14
| BooleanField_2024_06_14;
export type InputBookingField_2024_06_14 =
| PhoneFieldInput_2024_06_14
| AddressFieldInput_2024_06_14
| TextFieldInput_2024_06_14
| NumberFieldInput_2024_06_14
| TextAreaFieldInput_2024_06_14
| SelectFieldInput_2024_06_14
| MultiSelectFieldInput_2024_06_14
| MultiEmailFieldInput_2024_06_14
| CheckboxGroupFieldInput_2024_06_14
| RadioGroupFieldInput_2024_06_14
| BooleanFieldInput_2024_06_14;
@ValidatorConstraint({ async: true })
class BookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface {
private classTypeMap: { [key: string]: new () => BookingField_2024_06_14 } = {
name: NameField_2024_06_14,
email: EmailField_2024_06_14,
phone: PhoneField_2024_06_14,
address: AddressField_2024_06_14,
text: TextField_2024_06_14,
number: NumberField_2024_06_14,
textarea: TextAreaField_2024_06_14,
select: SelectField_2024_06_14,
multiselect: MultiSelectField_2024_06_14,
multiemail: MultiEmailField_2024_06_14,
checkbox: CheckboxGroupField_2024_06_14,
radio: RadioGroupField_2024_06_14,
boolean: BooleanField_2024_06_14,
class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface {
private classTypeMap: { [key: string]: new () => InputBookingField_2024_06_14 } = {
phone: PhoneFieldInput_2024_06_14,
address: AddressFieldInput_2024_06_14,
text: TextFieldInput_2024_06_14,
number: NumberFieldInput_2024_06_14,
textarea: TextAreaFieldInput_2024_06_14,
select: SelectFieldInput_2024_06_14,
multiselect: MultiSelectFieldInput_2024_06_14,
multiemail: MultiEmailFieldInput_2024_06_14,
checkbox: CheckboxGroupFieldInput_2024_06_14,
radio: RadioGroupFieldInput_2024_06_14,
boolean: BooleanFieldInput_2024_06_14,
};
private reservedSystemSlugs = ["name", "email", "location", "rescheduleReason"];
async validate(bookingFields: { type: string; slug: string }[]) {
if (!Array.isArray(bookingFields)) {
throw new BadRequestException(`'bookingFields' must be an array.`);
@@ -374,6 +325,14 @@ class BookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface {
throw new BadRequestException(`Each booking field must have a 'slug' property.`);
}
if (this.reservedSystemSlugs.includes(slug)) {
throw new BadRequestException(
`The slug '${slug}' is reserved and cannot be used, because it is a slug of a default booking field. Reserved slugs are: ${this.reservedSystemSlugs.join(
", "
)}`
);
}
if (slugs.includes(slug)) {
throw new BadRequestException(
`Duplicate bookingFields slug '${slug}' found. All bookingFields slugs must be unique.`
@@ -402,15 +361,15 @@ class BookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface {
}
}
export function ValidateBookingFields_2024_06_14(validationOptions?: ValidationOptions) {
export function ValidateInputBookingFields_2024_06_14(validationOptions?: ValidationOptions) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function (object: any, propertyName: string) {
registerDecorator({
name: "ValidateBookingFields",
name: "ValidateInputBookingFields",
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: new BookingFieldValidator_2024_06_14(),
validator: new InputBookingFieldValidator_2024_06_14(),
});
};
}
@@ -13,8 +13,8 @@ import {
import { SchedulingType } from "@calcom/platform-enums";
import type { BookingField_2024_06_14 } from "./booking-fields.input";
import { ValidateBookingFields_2024_06_14 } from "./booking-fields.input";
import type { InputBookingField_2024_06_14 } from "./booking-fields.input";
import { ValidateInputBookingFields_2024_06_14 } from "./booking-fields.input";
import { BookingLimitsCount_2024_06_14, ValidateBookingLimistsCount } from "./booking-limits-count.input";
import {
BookingLimitsDuration_2024_06_14,
@@ -54,8 +54,8 @@ export class CreateEventTypeInput_2024_06_14 {
locations?: Location_2024_06_14[];
@IsOptional()
@ValidateBookingFields_2024_06_14()
bookingFields?: BookingField_2024_06_14[];
@ValidateInputBookingFields_2024_06_14()
bookingFields?: InputBookingField_2024_06_14[];
@IsBoolean()
@IsOptional()
@@ -2,8 +2,8 @@ import { ApiProperty as DocsProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsString, IsInt, IsBoolean, IsOptional, Min, ValidateNested, IsArray } from "class-validator";
import type { BookingField_2024_06_14 } from "./booking-fields.input";
import { ValidateBookingFields_2024_06_14 } from "./booking-fields.input";
import type { InputBookingField_2024_06_14 } from "./booking-fields.input";
import { ValidateInputBookingFields_2024_06_14 } from "./booking-fields.input";
import { BookingLimitsCount_2024_06_14, ValidateBookingLimistsCount } from "./booking-limits-count.input";
import {
BookingLimitsDuration_2024_06_14,
@@ -38,8 +38,8 @@ export class UpdateEventTypeInput_2024_06_14 {
locations?: Location_2024_06_14[];
@IsOptional()
@ValidateBookingFields_2024_06_14()
bookingFields?: BookingField_2024_06_14[];
@ValidateInputBookingFields_2024_06_14()
bookingFields?: InputBookingField_2024_06_14[];
@IsBoolean()
@IsOptional()
@@ -122,9 +122,9 @@ export class UpdateTeamEventTypeInput_2024_06_14 {
locations?: Location_2024_06_14[];
@IsOptional()
@ValidateBookingFields_2024_06_14()
@ValidateInputBookingFields_2024_06_14()
@DocsProperty()
bookingFields?: BookingField_2024_06_14[];
bookingFields?: InputBookingField_2024_06_14[];
@IsBoolean()
@IsOptional()
@@ -0,0 +1,309 @@
import { BadRequestException } from "@nestjs/common";
import { plainToInstance } from "class-transformer";
import { IsBoolean, IsString } from "class-validator";
import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator";
import { registerDecorator, validate, ValidatorConstraint } from "class-validator";
import {
PhoneFieldInput_2024_06_14,
AddressFieldInput_2024_06_14,
TextFieldInput_2024_06_14,
NumberFieldInput_2024_06_14,
TextAreaFieldInput_2024_06_14,
SelectFieldInput_2024_06_14,
MultiSelectFieldInput_2024_06_14,
MultiEmailFieldInput_2024_06_14,
CheckboxGroupFieldInput_2024_06_14,
RadioGroupFieldInput_2024_06_14,
BooleanFieldInput_2024_06_14,
} from "../inputs";
class NameDefaultFieldOutput_2024_06_14 {
@IsBoolean()
isDefault = true;
@IsString()
slug!: "name";
@IsString()
type!: "name";
@IsBoolean()
required!: true;
}
class EmailDefaultFieldOutput_2024_06_14 {
@IsBoolean()
isDefault = true;
@IsString()
slug!: "email";
@IsString()
type!: "email";
@IsBoolean()
required!: true;
}
class LocationDefaultFieldOutput_2024_06_14 {
@IsBoolean()
isDefault = true;
@IsString()
slug!: "location";
@IsString()
type!: "radioInput";
@IsBoolean()
required!: false;
}
class RescheduleReasonDefaultFieldOutput_2024_06_14 {
@IsBoolean()
isDefault = true;
@IsString()
slug!: "rescheduleReason";
@IsString()
type!: "textarea";
@IsBoolean()
required!: false;
}
class TitleDefaultFieldOutput_2024_06_14 {
@IsBoolean()
isDefault = true;
@IsString()
slug!: "title";
@IsString()
type!: "text";
@IsBoolean()
required!: true;
}
class NotesDefaultFieldOutput_2024_06_14 {
@IsBoolean()
isDefault = true;
@IsString()
slug!: "notes";
@IsString()
type!: "textarea";
@IsBoolean()
required!: false;
}
class GuestsDefaultFieldOutput_2024_06_14 {
@IsBoolean()
isDefault = true;
@IsString()
slug!: "guests";
@IsString()
type!: "multiemail";
@IsBoolean()
required!: false;
}
class PhoneFieldOutput_2024_06_14 extends PhoneFieldInput_2024_06_14 {
@IsBoolean()
isDefault = false;
}
class AddressFieldOutput_2024_06_14 extends AddressFieldInput_2024_06_14 {
@IsBoolean()
isDefault = false;
}
class TextFieldOutput_2024_06_14 extends TextFieldInput_2024_06_14 {
@IsBoolean()
isDefault = false;
}
class NumberFieldOutput_2024_06_14 extends NumberFieldInput_2024_06_14 {
@IsBoolean()
isDefault = false;
}
class TextAreaFieldOutput_2024_06_14 extends TextAreaFieldInput_2024_06_14 {
@IsBoolean()
isDefault = false;
}
class SelectFieldOutput_2024_06_14 extends SelectFieldInput_2024_06_14 {
@IsBoolean()
isDefault = false;
}
class MultiSelectFieldOutput_2024_06_14 extends MultiSelectFieldInput_2024_06_14 {
@IsBoolean()
isDefault = false;
}
class MultiEmailFieldOutput_2024_06_14 extends MultiEmailFieldInput_2024_06_14 {
@IsBoolean()
isDefault = false;
}
class CheckboxGroupFieldOutput_2024_06_14 extends CheckboxGroupFieldInput_2024_06_14 {
@IsBoolean()
isDefault = false;
}
class RadioGroupFieldOutput_2024_06_14 extends RadioGroupFieldInput_2024_06_14 {
@IsBoolean()
isDefault = false;
}
class BooleanFieldOutput_2024_06_14 extends BooleanFieldInput_2024_06_14 {
@IsBoolean()
isDefault = false;
}
export type DefaultFieldOutput_2024_06_14 =
| NameDefaultFieldOutput_2024_06_14
| EmailDefaultFieldOutput_2024_06_14
| LocationDefaultFieldOutput_2024_06_14
| RescheduleReasonDefaultFieldOutput_2024_06_14
| TitleDefaultFieldOutput_2024_06_14
| NotesDefaultFieldOutput_2024_06_14
| GuestsDefaultFieldOutput_2024_06_14;
export type CustomFieldOutput_2024_06_14 =
| PhoneFieldOutput_2024_06_14
| AddressFieldOutput_2024_06_14
| TextFieldOutput_2024_06_14
| NumberFieldOutput_2024_06_14
| TextAreaFieldOutput_2024_06_14
| SelectFieldOutput_2024_06_14
| MultiSelectFieldOutput_2024_06_14
| MultiEmailFieldOutput_2024_06_14
| CheckboxGroupFieldOutput_2024_06_14
| RadioGroupFieldOutput_2024_06_14
| BooleanFieldOutput_2024_06_14;
export type OutputBookingField_2024_06_14 = DefaultFieldOutput_2024_06_14 | CustomFieldOutput_2024_06_14;
@ValidatorConstraint({ async: true })
class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface {
private defaultOutputNameMap: { [key: string]: new () => DefaultFieldOutput_2024_06_14 } = {
name: NameDefaultFieldOutput_2024_06_14,
email: EmailDefaultFieldOutput_2024_06_14,
location: LocationDefaultFieldOutput_2024_06_14,
rescheduleReason: RescheduleReasonDefaultFieldOutput_2024_06_14,
title: TitleDefaultFieldOutput_2024_06_14,
notes: NotesDefaultFieldOutput_2024_06_14,
guests: GuestsDefaultFieldOutput_2024_06_14,
};
private customOutputTypeMap: { [key: string]: new () => CustomFieldOutput_2024_06_14 } = {
phone: PhoneFieldOutput_2024_06_14,
address: AddressFieldOutput_2024_06_14,
text: TextFieldOutput_2024_06_14,
number: NumberFieldOutput_2024_06_14,
textarea: TextAreaFieldOutput_2024_06_14,
select: SelectFieldOutput_2024_06_14,
multiselect: MultiSelectFieldOutput_2024_06_14,
multiemail: MultiEmailFieldOutput_2024_06_14,
checkbox: CheckboxGroupFieldOutput_2024_06_14,
radio: RadioGroupFieldOutput_2024_06_14,
boolean: BooleanFieldOutput_2024_06_14,
};
async validate(bookingFields: OutputBookingField_2024_06_14[]) {
if (!Array.isArray(bookingFields)) {
throw new BadRequestException(`'bookingFields' must be an array.`);
}
if (!bookingFields.length) {
throw new BadRequestException(`'bookingFields' must contain at least 1 booking field.`);
}
const slugs: string[] = [];
for (const field of bookingFields) {
const { type, slug } = field;
if (!type) {
throw new BadRequestException(`Each booking field must have a 'type' property.`);
}
if (!slug) {
throw new BadRequestException(`Each booking field must have a 'slug' property.`);
}
if (slugs.includes(slug)) {
throw new BadRequestException(
`Duplicate bookingFields slug '${slug}' found. All bookingFields slugs must be unique.`
);
}
slugs.push(slug);
if (this.isDefaultField(field)) {
await this.validateDefaultField(field);
} else {
await this.validateCustomField(field);
}
}
return true;
}
isDefaultField(field: OutputBookingField_2024_06_14): field is DefaultFieldOutput_2024_06_14 {
return field.isDefault === true;
}
async validateDefaultField(field: DefaultFieldOutput_2024_06_14) {
const ClassType = this.defaultOutputNameMap[field.slug];
if (!ClassType) {
throw new BadRequestException(`Unsupported booking field slgu '${field.slug}'.`);
}
const instance = plainToInstance(ClassType, field);
const errors = await validate(instance);
if (errors.length > 0) {
const message = errors.flatMap((error) => Object.values(error.constraints || {})).join(", ");
throw new BadRequestException(`Validation failed for ${field.slug} booking field: ${message}`);
}
}
async validateCustomField(field: CustomFieldOutput_2024_06_14) {
const ClassType = this.customOutputTypeMap[field.type];
if (!ClassType) {
throw new BadRequestException(`Unsupported booking field type '${field.type}'.`);
}
const instance = plainToInstance(ClassType, field);
const errors = await validate(instance);
if (errors.length > 0) {
const message = errors.flatMap((error) => Object.values(error.constraints || {})).join(", ");
throw new BadRequestException(`Validation failed for ${field.type} booking field: ${message}`);
}
}
defaultMessage() {
return `Validation failed for one or more booking fields.`;
}
}
export function ValidateOutputBookingFields_2024_06_14(validationOptions?: ValidationOptions) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function (object: any, propertyName: string) {
registerDecorator({
name: "ValidateOutputBookingFields",
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: new OutputBookingFieldValidator_2024_06_14(),
});
};
}
@@ -11,12 +11,13 @@ import {
ValidateNested,
} from "class-validator";
import type { Location_2024_06_14, BookingField_2024_06_14, BookingWindow_2024_06_14 } from "../inputs";
import type { Location_2024_06_14, BookingWindow_2024_06_14 } from "../inputs";
import { Host as TeamEventTypeHostInput, BookingLimitsDuration_2024_06_14 } from "../inputs";
import { Recurrence_2024_06_14 } from "../inputs";
import { ValidateBookingFields_2024_06_14 } from "../inputs/booking-fields.input";
import type { BookingLimitsCount_2024_06_14 } from "../inputs/booking-limits-count.input";
import { ValidateLocations_2024_06_14 } from "../inputs/locations.input";
import type { OutputBookingField_2024_06_14 } from "./booking-fields.output";
import { ValidateOutputBookingFields_2024_06_14 } from "./booking-fields.output";
enum SchedulingTypeEnum {
ROUND_ROBIN = "ROUND_ROBIN",
@@ -75,8 +76,8 @@ export class EventTypeOutput_2024_06_14 {
@ValidateLocations_2024_06_14()
locations!: Location_2024_06_14[];
@ValidateBookingFields_2024_06_14()
bookingFields!: BookingField_2024_06_14[];
@ValidateOutputBookingFields_2024_06_14()
bookingFields!: OutputBookingField_2024_06_14[];
@IsBoolean()
disableGuests!: boolean;
@@ -185,8 +186,8 @@ export class TeamEventTypeOutput_2024_06_14 {
@ValidateLocations_2024_06_14()
locations!: Location_2024_06_14[];
@ValidateBookingFields_2024_06_14()
bookingFields!: BookingField_2024_06_14[];
@ValidateOutputBookingFields_2024_06_14()
bookingFields!: OutputBookingField_2024_06_14[];
@IsBoolean()
disableGuests!: boolean;
@@ -1 +1,2 @@
export * from "./event-type.output";
export * from "./booking-fields.output";
+5653 -74
View File
File diff suppressed because it is too large Load Diff