fix: v2 event types booking fields & enable booker atom google-meet location (#17321)

* fix: v2 phone system booking field

* feat: atom support google-meet

* feat: unknown booking field return type

* tests

* bump platform libraries

* driveby: improve create-booking-input
This commit is contained in:
Lauris Skraucis
2024-10-25 15:55:29 +03:00
committed by GitHub
parent 9157e1faf3
commit 69ca57e45a
16 changed files with 496 additions and 1470 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.52",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.53",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -915,7 +915,7 @@ describe("Event types Endpoints", () => {
});
});
describe("Handle legacy event-types booking fields", () => {
describe("Handle event-types booking fields", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
@@ -940,6 +940,7 @@ describe("Event types Endpoints", () => {
{ isDefault: true, required: false, slug: "notes", type: "textarea" },
{ isDefault: true, required: false, slug: "guests", type: "multiemail" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false },
];
beforeAll(async () => {
@@ -1092,6 +1093,21 @@ describe("Event types Endpoints", () => {
defaultLabel: "reason_for_reschedule",
defaultPlaceholder: "reschedule_placeholder",
},
{
name: "attendeePhoneNumber",
type: "phone",
hidden: true,
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: false,
defaultLabel: "phone_number",
},
],
};
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
@@ -1207,6 +1223,21 @@ describe("Event types Endpoints", () => {
defaultLabel: "reason_for_reschedule",
defaultPlaceholder: "reschedule_placeholder",
},
{
name: "attendeePhoneNumber",
type: "phone",
hidden: true,
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: false,
defaultLabel: "phone_number",
},
],
};
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
@@ -1234,6 +1265,54 @@ describe("Event types Endpoints", () => {
});
});
it("should return event type with unknown bookingField", async () => {
const unknownSystemField = {
name: "unknown-whatever",
type: "unknown-whatever",
label: "your team",
sources: [
{
id: "user",
type: "user",
label: "User",
fieldRequired: true,
},
],
editable: "user",
required: true,
placeholder: "FC Barcelona",
};
const eventTypeInput = {
title: "unknown field event type two",
description: "unknown field event type description two",
length: 40,
hidden: false,
slug: "unknown-field-type-two",
locations: [],
schedulingType: SchedulingType.ROUND_ROBIN,
bookingFields: [unknownSystemField],
};
const eventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
return request(app.getHttpServer())
.get(`/api/v2/event-types/${eventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([
{
type: "unknown",
slug: "unknown",
bookingField: JSON.stringify(unknownSystemField),
},
]);
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -8,7 +8,6 @@ import {
transformBookingFieldsInternalToApi,
parseRecurringEvent,
InternalLocationSchema,
BookingFieldsSchema,
SystemField,
CustomField,
parseBookingLimit,
@@ -21,6 +20,7 @@ import {
parseEventTypeColor,
transformSeatsInternalToApi,
InternalLocation,
BookingFieldSchema,
} from "@calcom/platform-libraries";
import {
TransformFutureBookingsLimitSchema_2024_06_14,
@@ -28,6 +28,7 @@ import {
NoticeThresholdTransformedSchema,
EventTypeOutput_2024_06_14,
OutputUnknownLocation_2024_06_14,
OutputUnknownBookingField_2024_06_14,
} from "@calcom/platform-types";
type EventTypeRelations = {
@@ -120,7 +121,7 @@ export class OutputEventTypesService_2024_06_14 {
const locations = this.transformLocations(databaseEventType.locations);
const customName = databaseEventType?.eventName ?? undefined;
const bookingFields = databaseEventType.bookingFields
? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields))
? this.transformBookingFields(databaseEventType.bookingFields)
: [];
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
@@ -196,14 +197,14 @@ export class OutputEventTypesService_2024_06_14 {
const knownLocations: InternalLocation[] = [];
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
const parsedLocations = locations.map((location: any) => {
for (const location of locations) {
const result = InternalLocationSchema.safeParse(location);
if (result.success) {
return knownLocations.push(result.data);
knownLocations.push(result.data);
} else {
return unknownLocations.push({ type: "unknown", location: JSON.stringify(location) });
unknownLocations.push({ type: "unknown", location: JSON.stringify(location) });
}
});
}
return [...transformLocationsInternalToApi(knownLocations), ...unknownLocations];
}
@@ -216,10 +217,26 @@ export class OutputEventTypesService_2024_06_14 {
};
}
transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) {
transformBookingFields(bookingFields: any) {
if (!bookingFields) return [];
return transformBookingFieldsInternalToApi(bookingFields);
const knownBookingFields: (SystemField | CustomField)[] = [];
const unknownBookingFields: OutputUnknownBookingField_2024_06_14[] = [];
for (const bookingField of bookingFields) {
const result = BookingFieldSchema.safeParse(bookingField);
if (result.success) {
knownBookingFields.push(result.data);
} else {
unknownBookingFields.push({
type: "unknown",
slug: "unknown",
bookingField: JSON.stringify(bookingField),
});
}
}
return [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
}
transformRecurringEvent(recurringEvent: any) {
+4 -3
View File
@@ -5908,7 +5908,8 @@
"type": "string",
"example": "cal-video",
"enum": [
"cal-video"
"cal-video",
"google-meet"
]
}
},
@@ -7067,7 +7068,7 @@
"link": {
"type": "string"
},
"credentialdId": {
"credentialId": {
"type": "number"
}
},
@@ -12740,7 +12741,7 @@
},
"bookingFieldsResponses": {
"type": "object",
"description": "Booking field responses.",
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
"example": {
"customField": "customValue"
}
@@ -11,8 +11,12 @@ import type {
OrganizerPhoneLocation,
} from "../internal/locations";
const apiToInternalintegrationsMapping: Record<Integration_2024_06_14, "integrations:daily"> = {
const apiToInternalintegrationsMapping: Record<
Integration_2024_06_14,
"integrations:daily" | "integrations:google:meet"
> = {
"cal-video": "integrations:daily",
"google-meet": "integrations:google:meet",
};
export function transformLocationsApiToInternal(inputLocations: InputLocation_2024_06_14[] | undefined) {
@@ -5,6 +5,7 @@ import type {
DefaultFieldOutput_2024_06_14,
CustomFieldOutput_2024_06_14,
} from "@calcom/platform-types";
import type { OutputUnknownBookingField_2024_06_14 } from "@calcom/platform-types/event-types/event-types_2024_06_14/outputs/booking-fields.output";
export function transformBookingFieldsInternalToApi(
databaseBookingFields: (SystemField | CustomField)[]
@@ -17,186 +18,201 @@ export function transformBookingFieldsInternalToApi(
(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,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "email":
return {
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
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 responseDefaultFields: (DefaultFieldOutput_2024_06_14 | OutputUnknownBookingField_2024_06_14)[] =
defaultFields.map((field) => {
switch (field.name) {
case "name":
return {
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "email":
return {
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
label: field.label,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
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,
};
case "attendeePhoneNumber":
return {
isDefault: true,
type: field.type,
slug: field.name,
required: field.required,
};
default:
return {
type: "unknown",
slug: "unknown",
bookingField: JSON.stringify(field),
};
}
});
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,
disableOnPrefill: field.disableOnPrefill,
};
case "address":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "text":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "number":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "textarea":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "multiemail":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "boolean":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
disableOnPrefill: field.disableOnPrefill,
};
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) : [],
disableOnPrefill: field.disableOnPrefill,
};
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) : [],
disableOnPrefill: field.disableOnPrefill,
};
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) : [],
disableOnPrefill: field.disableOnPrefill,
};
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) : [],
disableOnPrefill: field.disableOnPrefill,
};
default:
throw new Error(`Unsupported booking field type '${field.type}'.`);
}
});
const responseCustomFields: (CustomFieldOutput_2024_06_14 | OutputUnknownBookingField_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,
disableOnPrefill: field.disableOnPrefill,
};
case "address":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "text":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "number":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "textarea":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "multiemail":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
placeholder: field.placeholder,
disableOnPrefill: field.disableOnPrefill,
};
case "boolean":
return {
isDefault: false,
type: field.type,
slug: field.name,
label: field.label,
required: field.required,
disableOnPrefill: field.disableOnPrefill,
};
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) : [],
disableOnPrefill: field.disableOnPrefill,
};
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) : [],
disableOnPrefill: field.disableOnPrefill,
};
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) : [],
disableOnPrefill: field.disableOnPrefill,
};
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) : [],
disableOnPrefill: field.disableOnPrefill,
};
default:
return {
type: "unknown",
slug: "unknown",
bookingField: JSON.stringify(field),
};
}
});
return [...responseDefaultFields, ...responseCustomFields];
}
@@ -316,6 +332,12 @@ const NameSystemFieldSchema = SystemFieldSchema.extend({
.optional(),
});
const PhoneSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("attendeePhoneNumber"),
type: z.literal("phone"),
required: z.boolean(),
});
const EmailSystemFieldSchema = SystemFieldSchema.extend({
name: z.literal("email"),
type: z.literal("email"),
@@ -359,10 +381,12 @@ type LocationReasonSystemField = z.infer<typeof LocationReasonSystemFieldSchema>
type TitleSystemField = z.infer<typeof TitleSystemFieldSchema>;
type NotesSystemField = z.infer<typeof NotesSystemFieldSchema>;
type GuestsSystemField = z.infer<typeof GuestsSystemFieldSchema>;
type PhoneSystemField = z.infer<typeof PhoneSystemFieldSchema>;
export type SystemField =
| NameSystemField
| EmailSystemField
| PhoneSystemField
| RescheduleReasonSystemField
| LocationReasonSystemField
| TitleSystemField
@@ -374,6 +398,7 @@ export type CustomField = z.infer<typeof CustomFieldsSchema>;
const SystemFieldsSchema = z.union([
NameSystemFieldSchema,
EmailSystemFieldSchema,
PhoneSystemFieldSchema,
LocationReasonSystemFieldSchema,
RescheduleReasonSystemFieldSchema,
TitleSystemFieldSchema,
@@ -381,7 +406,8 @@ const SystemFieldsSchema = z.union([
GuestsSystemFieldSchema,
]);
export const BookingFieldsSchema = z.array(z.union([CustomFieldsSchema, SystemFieldsSchema]));
export const BookingFieldSchema = z.union([CustomFieldsSchema, SystemFieldsSchema]);
export const BookingFieldsSchema = z.array(BookingFieldSchema);
export const systemBeforeFieldName: NameSystemField = {
type: "name",
@@ -776,6 +776,73 @@ describe("transformBookingFieldsInternalToApi", () => {
expect(result).toEqual(expectedOutput);
});
it("should transform system phone field", () => {
const transformedField: SystemField[] = [
{
name: "attendeePhoneNumber",
type: "phone",
hidden: true,
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: false,
defaultLabel: "phone_number",
},
];
const expectedOutput = [
{
isDefault: true,
type: "phone",
slug: "attendeePhoneNumber",
required: false,
},
];
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
it("should transform unknown field", () => {
const transformedField = [
{
name: "blabla",
type: "blabla",
hidden: true,
sources: [
{
id: "default",
type: "default",
label: "Default",
},
],
editable: "system-but-optional",
required: false,
defaultLabel: "phone_number",
},
];
const expectedOutput = [
{
type: "unknown",
slug: "unknown",
bookingField: JSON.stringify(transformedField[0]),
},
];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const result = transformBookingFieldsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
});
describe("transformIntervalLimitsInternalToApi", () => {
@@ -2,11 +2,11 @@ import { z } from "zod";
const integrationsApiAvailable = {
"cal-video": z.literal("integrations:daily"),
"google-meet": z.literal("integrations:google:meet"),
};
// note(Lauris): these are read only aka they exist in database and are to be returned by a READ operation
const integrationsApiUnavailable = {
"google-meet": z.literal("integrations:google:meet"),
zoom: z.literal("integrations:zoom"),
"whereby-video": z.literal("integrations:whereby_video"),
"whatsapp-video": z.literal("integrations:whatsapp_video"),
@@ -230,6 +230,8 @@ function getLocations(locations: EventTypeOutput_2024_06_14["locations"]) {
function isAtomSupportedLocation(
location: EventTypeOutput_2024_06_14["locations"][number]
): location is InputLocation_2024_06_14 {
const supportedIntegrations = ["cal-video", "google-meet"];
return (
location.type === "address" ||
location.type === "attendeeAddress" ||
@@ -237,7 +239,7 @@ function isAtomSupportedLocation(
location.type === "phone" ||
location.type === "attendeePhone" ||
location.type === "attendeeDefined" ||
(location.type === "integration" && location.integration === "cal-video")
(location.type === "integration" && supportedIntegrations.includes(location.integration))
);
}
@@ -291,7 +293,7 @@ function getBookingFields(
function isCustomField(
field: EventTypeOutput_2024_06_14["bookingFields"][number]
): field is CustomFieldOutput_2024_06_14 {
return !field.isDefault;
return field.type !== "unknown" && !field.isDefault;
}
function isDefaultEditableField(
+1
View File
@@ -129,6 +129,7 @@ export {
InternalLocationsSchema,
InternalLocationSchema,
BookingFieldsSchema,
BookingFieldSchema,
// note(Lauris): constants
systemBeforeFieldName,
systemBeforeFieldEmail,
@@ -102,7 +102,7 @@ export class CreateBookingInputPipe implements PipeTransform {
private isRecurringBookingInput(
value: CreateBookingInput
): value is CreateRecurringBookingInput_2024_08_13 {
return value.hasOwnProperty("eventTypeId");
return value.hasOwnProperty("recurrenceCount");
}
private isInstantBookingInput(value: CreateBookingInput): value is CreateInstantBookingInput_2024_08_13 {
@@ -145,85 +145,7 @@ export class CreateInstantBookingInput_2024_08_13 extends CreateBookingInput_202
instant!: boolean;
}
export class CreateRecurringBookingInput_2024_08_13 {
@ApiProperty({
type: String,
description: "The start time of the booking in ISO 8601 format in UTC timezone.",
example: "2024-08-13T09:00:00Z",
})
@IsDateString()
start!: string;
@ApiProperty({
type: Number,
description: "The ID of the event type that is booked.",
example: 123,
})
@IsInt()
eventTypeId!: number;
@ApiProperty({
type: Attendee,
description: "The attendee's details.",
})
@ValidateNested()
@Type(() => Attendee)
attendee!: Attendee;
@ApiProperty({
type: [String],
description: "An optional list of guest emails attending the event.",
example: ["guest1@example.com", "guest2@example.com"],
required: false,
})
@IsArray()
@IsString({ each: true })
@IsOptional()
guests?: string[];
@ApiProperty({
type: String,
description:
"Deprecated - use 'location' instead. Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.",
example: "https://example.com/meeting",
required: false,
deprecated: true,
})
@IsUrl()
@IsOptional()
meetingUrl?: string;
@ApiProperty({
type: String,
description: "Location for this booking. Displayed in email and calendar event.",
example: "https://example.com/meeting",
required: false,
})
@IsUrl()
@IsOptional()
location?: string;
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
// @ApiProperty({
// type: Object,
// description: "Optional metadata for the booking.",
// example: { key: "value" },
// required: false,
// })
// @IsObject()
// @IsOptional()
// metadata!: Record<string, unknown>;
@ApiProperty({
type: Object,
description: "Booking field responses.",
example: { customField: "customValue" },
required: false,
})
@IsObject()
@IsOptional()
bookingFieldsResponses?: Record<string, unknown>;
export class CreateRecurringBookingInput_2024_08_13 extends CreateBookingInput_2024_08_13 {
@ApiPropertyOptional({
type: Number,
description: `The number of recurrences. If not provided then event type recurrence count will be used. Can't be more than
@@ -521,7 +521,7 @@ class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterf
boolean: BooleanFieldInput_2024_06_14,
};
private reservedSystemSlugs = ["location", "rescheduleReason"];
private reservedSystemSlugs = ["location", "rescheduleReason", "attendeePhoneNumber"];
async validate(bookingFields: { type: string; slug: string }[]) {
if (!Array.isArray(bookingFields)) {
@@ -43,16 +43,16 @@ export class InputLinkLocation_2024_06_14 {
public!: boolean;
}
const integrations = ["cal-video"] as const;
export type Integration_2024_06_14 = (typeof integrations)[number];
export const supportedIntegrations = ["cal-video", "google-meet"] as const;
export type Integration_2024_06_14 = (typeof supportedIntegrations)[number];
export class InputIntegrationLocation_2024_06_14 {
@IsIn(inputLocations)
@DocsProperty({ example: "integration", description: "only allowed value for type is `integration`" })
type!: "integration";
@IsIn(integrations)
@DocsProperty({ example: integrations[0], enum: integrations })
@IsIn(supportedIntegrations)
@DocsProperty({ example: supportedIntegrations[0], enum: supportedIntegrations })
integration!: Integration_2024_06_14;
}
@@ -205,6 +205,32 @@ export class GuestsDefaultFieldOutput_2024_06_14 {
required!: false;
}
export class PhoneDefaultFieldOutput_2024_06_14 {
@IsBoolean()
@DocsProperty({
description: "This property is always true because it's a default field",
example: true,
default: true,
})
isDefault = true;
@IsString()
@DocsProperty({
default: "attendeePhoneNumber",
})
slug!: "attendeePhoneNumber";
@IsString()
@DocsProperty({
default: "phone",
})
type!: "phone";
@IsBoolean()
@DocsProperty()
required!: boolean;
}
export class PhoneFieldOutput_2024_06_14 extends PhoneFieldInput_2024_06_14 {
@IsBoolean()
@DocsProperty({
@@ -315,6 +341,17 @@ export class BooleanFieldOutput_2024_06_14 extends BooleanFieldInput_2024_06_14
isDefault = false;
}
export class OutputUnknownBookingField_2024_06_14 {
@DocsProperty({ example: "unknown", description: "only allowed value for type is `unknown`" })
type!: "unknown";
@DocsProperty({ example: "unknown", description: "only allowed value for type is `unknown`" })
slug!: "unknown";
@IsString()
bookingField!: string;
}
export type DefaultFieldOutput_2024_06_14 =
| NameDefaultFieldOutput_2024_06_14
| EmailDefaultFieldOutput_2024_06_14
@@ -322,7 +359,8 @@ export type DefaultFieldOutput_2024_06_14 =
| RescheduleReasonDefaultFieldOutput_2024_06_14
| TitleDefaultFieldOutput_2024_06_14
| NotesDefaultFieldOutput_2024_06_14
| GuestsDefaultFieldOutput_2024_06_14;
| GuestsDefaultFieldOutput_2024_06_14
| PhoneDefaultFieldOutput_2024_06_14;
export type CustomFieldOutput_2024_06_14 =
| PhoneFieldOutput_2024_06_14
@@ -337,7 +375,10 @@ export type CustomFieldOutput_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;
export type OutputBookingField_2024_06_14 =
| DefaultFieldOutput_2024_06_14
| CustomFieldOutput_2024_06_14
| OutputUnknownBookingField_2024_06_14;
@ValidatorConstraint({ async: true })
class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface {
@@ -349,6 +390,7 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
title: TitleDefaultFieldOutput_2024_06_14,
notes: NotesDefaultFieldOutput_2024_06_14,
guests: GuestsDefaultFieldOutput_2024_06_14,
attendeePhoneNumber: PhoneDefaultFieldOutput_2024_06_14,
};
private customOutputTypeMap: { [key: string]: new () => CustomFieldOutput_2024_06_14 } = {
@@ -390,9 +432,13 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
`Duplicate bookingFields slug '${slug}' found. All bookingFields slugs must be unique.`
);
}
slugs.push(slug);
if (slug !== "unknown") {
slugs.push(slug);
}
if (this.isDefaultField(field)) {
if (this.isUnknownField(field)) {
await this.validateUnknownField(field);
} else if (this.isDefaultField(field)) {
await this.validateDefaultField(field);
} else {
await this.validateCustomField(field);
@@ -403,13 +449,26 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
}
isDefaultField(field: OutputBookingField_2024_06_14): field is DefaultFieldOutput_2024_06_14 {
return field.isDefault === true;
return field.type !== "unknown" && "isDefault" in field && field.isDefault === true;
}
isUnknownField(field: OutputBookingField_2024_06_14): field is OutputUnknownBookingField_2024_06_14 {
return field.type === "unknown";
}
async validateUnknownField(field: OutputUnknownBookingField_2024_06_14) {
const instance = plainToInstance(OutputUnknownBookingField_2024_06_14, 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 validateDefaultField(field: DefaultFieldOutput_2024_06_14) {
const ClassType = this.defaultOutputNameMap[field.slug];
if (!ClassType) {
throw new BadRequestException(`Unsupported booking field slgu '${field.slug}'.`);
throw new BadRequestException(`Unsupported default booking field slug '${field.slug}'.`);
}
const instance = plainToInstance(ClassType, field);
+30 -1182
View File
File diff suppressed because it is too large Load Diff