79a09362c9
* refactor: remove circular dependency between prisma and app-store packages - Replace EventTypeAppMetadataSchema with z.record(z.any()).optional() pattern - Remove appDataSchemas import from packages/prisma/zod-utils.ts - Add null checks in consuming packages for flexible validation - Fix test file that no longer needs @ts-expect-error directive This breaks the circular dependency while maintaining all functionality by moving strict validation to the business logic layer where operations actually happen, following existing patterns in the codebase. Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * refactor: remove EventTypeAppMetadataSchema exports from prisma package - Remove EventTypeAppMetadataSchema and eventTypeAppMetadataOptionalSchema exports from prisma/zod-utils.ts - Update all importing files to use local z.record(z.any()).optional() schemas - Replace type annotations with Record<string, any> where appropriate - Maintain validation functionality while breaking circular dependency - All TypeScript compilation now passes without errors Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * refactor: complete removal of EventTypeAppMetadataSchema from remaining files - Update handleSeats/createNewSeat.ts to use local schema - Update payment handlers to use local schemas - Update eventTypes update handler to use local schema - All files now define their own validation instead of importing from prisma - Circular dependency completely eliminated Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * fix: resolve TypeScript compilation errors - Remove duplicate z import from handleConfirmation.ts - Remove duplicate appDataSchemas import from update.handler.ts - Fix index signature errors in handlePayment.ts by using appData variable consistently - All type checks now pass successfully Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * fix: add type casting for appSlug in eventTypeService - Cast appSlug as keyof typeof apps to resolve index signature error - Maintains type safety while allowing dynamic property access Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * fix * remove * make zod-utils.ts in app-store * update imports * fix * fix * fix * revert unrelated change * update imports * fix * fix * revert * fix * fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: hbjORbj <sldisek783@gmail.com>
771 lines
24 KiB
TypeScript
771 lines
24 KiB
TypeScript
import type { UnitTypeLongPlural } from "dayjs";
|
|
import type { TFunction } from "i18next";
|
|
import z, { ZodNullable, ZodObject, ZodOptional } from "zod";
|
|
import type {
|
|
AnyZodObject,
|
|
objectInputType,
|
|
objectOutputType,
|
|
ZodNullableDef,
|
|
ZodOptionalDef,
|
|
ZodRawShape,
|
|
ZodTypeAny,
|
|
} from "zod";
|
|
|
|
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
|
import type { FieldType as FormBuilderFieldType } from "@calcom/features/form-builder/schema";
|
|
import { fieldsSchema as formBuilderFieldsSchema } from "@calcom/features/form-builder/schema";
|
|
import { emailSchema as emailRegexSchema, emailRegex } from "@calcom/lib/emailSchema";
|
|
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
|
|
import { zodAttributesQueryValue } from "@calcom/lib/raqb/zod";
|
|
import { slugify } from "@calcom/lib/slugify";
|
|
import { EventTypeCustomInputType } from "@calcom/prisma/enums";
|
|
|
|
import type { Prisma } from "./client";
|
|
|
|
// Let's not import 118kb just to get an enum
|
|
export enum Frequency {
|
|
YEARLY = 0,
|
|
MONTHLY = 1,
|
|
WEEKLY = 2,
|
|
DAILY = 3,
|
|
HOURLY = 4,
|
|
MINUTELY = 5,
|
|
SECONDLY = 6,
|
|
}
|
|
|
|
export enum BookerLayouts {
|
|
MONTH_VIEW = "month_view",
|
|
WEEK_VIEW = "week_view",
|
|
COLUMN_VIEW = "column_view",
|
|
}
|
|
|
|
export const bookerLayoutOptions = [
|
|
BookerLayouts.MONTH_VIEW,
|
|
BookerLayouts.WEEK_VIEW,
|
|
BookerLayouts.COLUMN_VIEW,
|
|
];
|
|
|
|
const layoutOptions = z.union([
|
|
z.literal(bookerLayoutOptions[0]),
|
|
z.literal(bookerLayoutOptions[1]),
|
|
z.literal(bookerLayoutOptions[2]),
|
|
]);
|
|
|
|
export const bookerLayouts = z
|
|
.object({
|
|
enabledLayouts: z.array(layoutOptions),
|
|
defaultLayout: layoutOptions,
|
|
})
|
|
.nullable();
|
|
|
|
export const orgOnboardingInvitedMembersSchema = z.array(
|
|
z.object({ email: z.string().email(), name: z.string().optional() })
|
|
);
|
|
|
|
export const orgOnboardingTeamsSchema = z.array(
|
|
z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
isBeingMigrated: z.boolean(),
|
|
// "slug" is null for new teams
|
|
slug: z.string().nullable(),
|
|
})
|
|
);
|
|
|
|
export const defaultBookerLayoutSettings = {
|
|
defaultLayout: BookerLayouts.MONTH_VIEW,
|
|
// if the user has no explicit layouts set (not in user profile and not in event settings), all layouts are enabled.
|
|
enabledLayouts: bookerLayoutOptions,
|
|
};
|
|
|
|
export type BookerLayoutSettings = z.infer<typeof bookerLayouts>;
|
|
|
|
export const RequiresConfirmationThresholdUnits: z.ZodType<UnitTypeLongPlural> = z.enum(["hours", "minutes"]);
|
|
|
|
const _eventTypeMetaDataSchemaWithoutApps = z.object({
|
|
smartContractAddress: z.string().optional(),
|
|
blockchainId: z.number().optional(),
|
|
multipleDuration: z.number().array().optional(),
|
|
giphyThankYouPage: z.string().optional(),
|
|
additionalNotesRequired: z.boolean().optional(),
|
|
disableSuccessPage: z.boolean().optional(),
|
|
disableStandardEmails: z
|
|
.object({
|
|
all: z
|
|
.object({
|
|
host: z.boolean().optional(),
|
|
attendee: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
confirmation: z
|
|
.object({
|
|
host: z.boolean().optional(),
|
|
attendee: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
managedEventConfig: z
|
|
.object({
|
|
unlockedFields: z.custom<{ [k in keyof Omit<Prisma.EventTypeSelect, "id">]: true }>().optional(),
|
|
})
|
|
.optional(),
|
|
requiresConfirmationThreshold: z
|
|
.object({
|
|
time: z.number(),
|
|
unit: RequiresConfirmationThresholdUnits,
|
|
})
|
|
.optional(),
|
|
config: z
|
|
.object({
|
|
useHostSchedulesForTeamEvent: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
bookerLayouts: bookerLayouts.optional(),
|
|
});
|
|
|
|
export const eventTypeMetaDataSchemaWithUntypedApps = _eventTypeMetaDataSchemaWithoutApps.merge(
|
|
z.object({
|
|
apps: z.record(z.string(), z.any()).optional(),
|
|
})
|
|
);
|
|
|
|
export const EventTypeMetaDataSchema = eventTypeMetaDataSchemaWithUntypedApps.nullable();
|
|
export const eventTypeMetaDataSchemaWithoutApps = _eventTypeMetaDataSchemaWithoutApps.nullable();
|
|
|
|
export type EventTypeMetadata = z.infer<typeof EventTypeMetaDataSchema>;
|
|
|
|
export const eventTypeBookingFields = formBuilderFieldsSchema;
|
|
export const BookingFieldTypeEnum = eventTypeBookingFields.element.shape.type.Enum;
|
|
export type BookingFieldType = FormBuilderFieldType;
|
|
|
|
// Validation of user added bookingFields' responses happen using `getBookingResponsesSchema` which requires `eventType`.
|
|
// So it is a dynamic validation and thus entire validation can't exist here
|
|
// Note that this validation runs to validate prefill params as well, so it should consider that partial values can be there. e.g. `name` might be empty string
|
|
export const bookingResponses = z
|
|
.object({
|
|
email: z.string(),
|
|
attendeePhoneNumber: z.string().optional(),
|
|
//TODO: Why don't we move name out of bookingResponses and let it be handled like user fields?
|
|
name: z.union([
|
|
z.string(),
|
|
z.object({
|
|
firstName: z.string(),
|
|
lastName: z.string().optional(),
|
|
}),
|
|
]),
|
|
guests: z.array(z.string()).optional(),
|
|
notes: z.string().optional(),
|
|
location: z
|
|
.object({
|
|
optionValue: z.string(),
|
|
value: z.string(),
|
|
})
|
|
.optional(),
|
|
smsReminderNumber: z.string().optional(),
|
|
rescheduleReason: z.string().optional(),
|
|
})
|
|
.nullable();
|
|
|
|
export type BookingResponses = z.infer<typeof bookingResponses>;
|
|
|
|
export const eventTypeLocations = z.array(
|
|
z.object({
|
|
// TODO: Couldn't find a way to make it a union of types from App Store locations
|
|
// Creating a dynamic union by iterating over the object doesn't seem to make TS happy
|
|
type: z.string(),
|
|
address: z.string().optional(),
|
|
link: z.string().url().optional(),
|
|
displayLocationPublicly: z.boolean().optional(),
|
|
hostPhoneNumber: z.string().optional(),
|
|
credentialId: z.number().optional(),
|
|
teamName: z.string().optional(),
|
|
customLabel: z.string().optional(),
|
|
})
|
|
);
|
|
|
|
// Matching RRule.Options: rrule/dist/esm/src/types.d.ts
|
|
export const recurringEventType = z
|
|
.object({
|
|
dtstart: z.date().optional(),
|
|
interval: z.number(),
|
|
count: z.number(),
|
|
freq: z.nativeEnum(Frequency),
|
|
until: z.date().optional(),
|
|
tzid: z.string().optional(),
|
|
})
|
|
.nullable();
|
|
|
|
// dayjs iso parsing is very buggy - cant use :( - turns ISO string into Date object
|
|
export const iso8601 = z.string().transform((val, ctx) => {
|
|
const time = Date.parse(val);
|
|
if (!time) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "Invalid ISO Date",
|
|
});
|
|
}
|
|
const d = new Date();
|
|
d.setTime(time);
|
|
return d;
|
|
});
|
|
|
|
export const eventTypeColor = z
|
|
.object({
|
|
lightEventTypeColor: z.string(),
|
|
darkEventTypeColor: z.string(),
|
|
})
|
|
.nullable();
|
|
|
|
export type IntervalLimitsType = IntervalLimit | null;
|
|
|
|
export { intervalLimitsType } from "@calcom/lib/intervalLimits/intervalLimitSchema";
|
|
|
|
export const eventTypeSlug = z
|
|
.string()
|
|
.trim()
|
|
.transform((val) => slugify(val))
|
|
.refine((val) => val.length >= 1, {
|
|
message: "Please enter at least one character",
|
|
});
|
|
|
|
export const stringToDate = z.string().transform((a) => new Date(a));
|
|
|
|
export const stringOrNumber = z.union([
|
|
z.string().transform((v, ctx) => {
|
|
const parsed = parseInt(v);
|
|
if (isNaN(parsed)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "Not a number",
|
|
});
|
|
}
|
|
return parsed;
|
|
}),
|
|
z.number().int(),
|
|
]);
|
|
|
|
export const requiredCustomInputSchema = z.union([
|
|
// string must be given & nonempty
|
|
z.string().trim().min(1),
|
|
// boolean must be true if set.
|
|
z.boolean().refine((v) => v === true),
|
|
]);
|
|
|
|
const PlatformClientParamsSchema = z.object({
|
|
platformClientId: z.string().optional(),
|
|
platformRescheduleUrl: z.string().nullable().optional(),
|
|
platformCancelUrl: z.string().nullable().optional(),
|
|
platformBookingUrl: z.string().nullable().optional(),
|
|
platformBookingLocation: z.string().optional(),
|
|
areCalendarEventsEnabled: z.boolean().optional(),
|
|
});
|
|
|
|
export type PlatformClientParams = z.infer<typeof PlatformClientParamsSchema>;
|
|
|
|
export const bookingConfirmPatchBodySchema = z.object({
|
|
bookingId: z.number(),
|
|
confirmed: z.boolean(),
|
|
recurringEventId: z.string().optional(),
|
|
reason: z.string().optional(),
|
|
emailsEnabled: z.boolean().default(true),
|
|
platformClientParams: PlatformClientParamsSchema.optional(),
|
|
});
|
|
|
|
export const bookingCancelSchema = z.object({
|
|
id: z.number().optional(),
|
|
uid: z.string().optional(),
|
|
// note(Lauris): allRemainingBookings will cancel all bookings that have start time greater than this moment.
|
|
allRemainingBookings: z.boolean().optional(),
|
|
// note(Lauris): cancelSubsequentBookings will cancel all bookings after one specified by id or uid.
|
|
cancelSubsequentBookings: z.boolean().optional(),
|
|
cancellationReason: z.string().optional(),
|
|
seatReferenceUid: z.string().optional(),
|
|
cancelledBy: z.string().email({ message: "Invalid email" }).optional(),
|
|
internalNote: z
|
|
.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
cancellationReason: z.string().optional().nullable(),
|
|
})
|
|
.optional()
|
|
.nullable(),
|
|
});
|
|
|
|
export const bookingCancelAttendeeSeatSchema = z.object({
|
|
seatReferenceUid: z.string(),
|
|
});
|
|
|
|
export const bookingCancelInput = bookingCancelSchema.refine(
|
|
(data) => !!data.id || !!data.uid,
|
|
"At least one of the following required: 'id', 'uid'."
|
|
);
|
|
|
|
export const bookingCancelWithCsrfSchema = bookingCancelSchema
|
|
.extend({
|
|
csrfToken: z.string().length(64, "Invalid CSRF token"),
|
|
})
|
|
.refine((data) => !!data.id || !!data.uid, "At least one of the following required: 'id', 'uid'.");
|
|
|
|
export const vitalSettingsUpdateSchema = z.object({
|
|
connected: z.boolean().optional(),
|
|
selectedParam: z.string().optional(),
|
|
sleepValue: z.number().optional(),
|
|
});
|
|
|
|
export const createdEventSchema = z
|
|
.object({
|
|
id: z.string(),
|
|
thirdPartyRecurringEventId: z.string(),
|
|
password: z.union([z.string(), z.undefined()]),
|
|
onlineMeetingUrl: z.string().nullable(),
|
|
iCalUID: z.string().optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
const schemaDefaultConferencingApp = z.object({
|
|
appSlug: z.string().default("daily-video").optional(),
|
|
appLink: z.string().optional(),
|
|
});
|
|
|
|
export const userMetadata = z
|
|
.object({
|
|
proPaidForByTeamId: z.number().optional(),
|
|
stripeCustomerId: z.string().optional(),
|
|
vitalSettings: vitalSettingsUpdateSchema.optional(),
|
|
isPremium: z.boolean().optional(),
|
|
sessionTimeout: z.number().optional(), // Minutes
|
|
defaultConferencingApp: schemaDefaultConferencingApp.optional(),
|
|
defaultBookerLayouts: bookerLayouts.optional(),
|
|
emailChangeWaitingForVerification: z
|
|
.string()
|
|
.transform((data) => data.toLowerCase())
|
|
.optional(),
|
|
migratedToOrgFrom: z
|
|
.object({
|
|
username: z.string().or(z.null()).optional(),
|
|
lastMigrationTime: z.string().optional(),
|
|
reverted: z.boolean().optional(),
|
|
revertTime: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.nullable();
|
|
|
|
export type DefaultConferencingApp = z.infer<typeof schemaDefaultConferencingApp>;
|
|
|
|
export const orgSettingsSchema = z
|
|
.object({
|
|
isOrganizationVerified: z.boolean().optional(),
|
|
isOrganizationConfigured: z.boolean().optional(),
|
|
isAdminReviewed: z.boolean().optional(),
|
|
orgAutoAcceptEmail: z.string().optional(),
|
|
isAdminAPIEnabled: z.boolean().optional(),
|
|
})
|
|
.nullable();
|
|
export type userMetadataType = z.infer<typeof userMetadata>;
|
|
|
|
export enum BillingPeriod {
|
|
MONTHLY = "MONTHLY",
|
|
ANNUALLY = "ANNUALLY",
|
|
}
|
|
|
|
const baseTeamMetadataSchema = z.object({
|
|
defaultConferencingApp: schemaDefaultConferencingApp.optional(),
|
|
requestedSlug: z.string().or(z.null()),
|
|
paymentId: z.string(),
|
|
subscriptionId: z.string().nullable(),
|
|
subscriptionItemId: z.string().nullable(),
|
|
orgSeats: z.number().nullable(),
|
|
orgPricePerSeat: z.number().nullable(),
|
|
migratedToOrgFrom: z
|
|
.object({
|
|
teamSlug: z.string().or(z.null()).optional(),
|
|
lastMigrationTime: z.string().optional(),
|
|
reverted: z.boolean().optional(),
|
|
lastRevertTime: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
billingPeriod: z.nativeEnum(BillingPeriod).optional(),
|
|
});
|
|
|
|
export const teamMetadataSchema = baseTeamMetadataSchema.partial().nullable();
|
|
|
|
export const teamMetadataStrictSchema = baseTeamMetadataSchema
|
|
.extend({
|
|
subscriptionId: z
|
|
.string()
|
|
.refine((val) => val.startsWith("sub_"), {
|
|
message: "subscriptionId must start with 'sub_'",
|
|
})
|
|
.nullable(),
|
|
subscriptionItemId: z
|
|
.string()
|
|
.refine((val) => val.startsWith("si_"), {
|
|
message: "subscriptionItemId must start with 'si_'",
|
|
})
|
|
.nullable(),
|
|
})
|
|
.partial()
|
|
.nullable();
|
|
|
|
export const bookingMetadataSchema = z
|
|
.object({
|
|
videoCallUrl: z.string().optional(),
|
|
})
|
|
.and(z.record(z.string()))
|
|
.nullable()
|
|
.describe("BookingMetadata");
|
|
|
|
export const customInputOptionSchema = z.array(
|
|
z.object({
|
|
label: z.string(),
|
|
type: z.string(),
|
|
})
|
|
);
|
|
|
|
export const customInputSchema = z.object({
|
|
id: z.number(),
|
|
eventTypeId: z.number(),
|
|
label: z.string(),
|
|
type: z.nativeEnum(EventTypeCustomInputType),
|
|
options: customInputOptionSchema.optional().nullable(),
|
|
required: z.boolean(),
|
|
placeholder: z.string(),
|
|
hasToBeCreated: z.boolean().optional(),
|
|
});
|
|
|
|
export type CustomInputSchema = z.infer<typeof customInputSchema>;
|
|
|
|
export const recordingItemSchema = z
|
|
.object({
|
|
id: z.string(),
|
|
room_name: z.string(),
|
|
start_ts: z.number(),
|
|
status: z.string(),
|
|
max_participants: z.number().optional(),
|
|
duration: z.number(),
|
|
share_token: z.string(),
|
|
})
|
|
.passthrough();
|
|
|
|
export const recordingItemsSchema = z.array(recordingItemSchema);
|
|
|
|
export type RecordingItemSchema = z.infer<typeof recordingItemSchema>;
|
|
|
|
export const getRecordingsResponseSchema = z.union([
|
|
z.object({
|
|
total_count: z.number(),
|
|
data: recordingItemsSchema,
|
|
}),
|
|
z.object({}),
|
|
]);
|
|
|
|
export type GetRecordingsResponseSchema = z.infer<typeof getRecordingsResponseSchema>;
|
|
|
|
/**
|
|
* Ensures that it is a valid HTTP URL
|
|
* It automatically avoids
|
|
* - XSS attempts through javascript:alert('hi')
|
|
* - mailto: links
|
|
*/
|
|
export const successRedirectUrl = z
|
|
.union([
|
|
z.literal(""),
|
|
z
|
|
.string()
|
|
.url()
|
|
.regex(/^http(s)?:\/\/.*/),
|
|
])
|
|
.optional();
|
|
|
|
export const RoutingFormSettings = z
|
|
.object({
|
|
// Applicable only for User Forms
|
|
emailOwnerOnSubmission: z.boolean(),
|
|
|
|
// Applicable only for Team Forms
|
|
sendUpdatesTo: z.array(z.number()).optional(),
|
|
sendToAll: z.boolean().optional(),
|
|
})
|
|
.nullable();
|
|
|
|
export const DeploymentTheme = z
|
|
.object({
|
|
brand: z.string().default("#292929"),
|
|
textBrand: z.string().default("#ffffff"),
|
|
darkBrand: z.string().default("#fafafa"),
|
|
textDarkBrand: z.string().default("#292929"),
|
|
bookingHighlight: z.string().default("#10B981"),
|
|
bookingLightest: z.string().default("#E1E1E1"),
|
|
bookingLighter: z.string().default("#ACACAC"),
|
|
bookingLight: z.string().default("#888888"),
|
|
bookingMedian: z.string().default("#494949"),
|
|
bookingDark: z.string().default("#313131"),
|
|
bookingDarker: z.string().default("#292929"),
|
|
fontName: z.string().default("Cal Sans"),
|
|
fontSrc: z.string().default("https://cal.com/cal.ttf"),
|
|
})
|
|
.optional();
|
|
|
|
export type ZodDenullish<T extends ZodTypeAny> = T extends ZodNullable<infer U> | ZodOptional<infer U>
|
|
? ZodDenullish<U>
|
|
: T;
|
|
|
|
export type ZodDenullishShape<T extends ZodRawShape> = {
|
|
[k in keyof T]: ZodDenullish<T[k]>;
|
|
};
|
|
|
|
export const denullish = <T extends ZodTypeAny>(schema: T): ZodDenullish<T> =>
|
|
(schema instanceof ZodNullable || schema instanceof ZodOptional
|
|
? denullish((schema._def as ZodNullableDef | ZodOptionalDef).innerType)
|
|
: schema) as ZodDenullish<T>;
|
|
|
|
type UnknownKeysParam = "passthrough" | "strict" | "strip";
|
|
|
|
/**
|
|
* @see https://github.com/3x071c/lsg-remix/blob/e2a9592ba3ec5103556f2cf307c32f08aeaee32d/app/lib/util/zod.ts
|
|
*/
|
|
export function denullishShape<
|
|
T extends ZodRawShape,
|
|
UnknownKeys extends UnknownKeysParam = "strip",
|
|
Catchall extends ZodTypeAny = ZodTypeAny,
|
|
Output = objectOutputType<T, Catchall>,
|
|
Input = objectInputType<T, Catchall>
|
|
>(
|
|
obj: ZodObject<T, UnknownKeys, Catchall, Output, Input>
|
|
): ZodObject<ZodDenullishShape<T>, UnknownKeys, Catchall> {
|
|
const a = entries(obj.shape).map(([field, schema]) => [field, denullish(schema)] as const) as {
|
|
[K in keyof T]: [K, ZodDenullish<T[K]>];
|
|
}[keyof T][];
|
|
return new ZodObject({
|
|
...obj._def,
|
|
shape: () => fromEntries(a) as unknown as ZodDenullishShape<T>, // TODO: Safely assert type
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Like Object.entries, but with actually useful typings
|
|
* @param obj The object to turn into a tuple array (`[key, value][]`)
|
|
* @returns The constructed tuple array from the given object
|
|
* @see https://github.com/3x071c/lsg-remix/blob/e2a9592ba3ec5103556f2cf307c32f08aeaee32d/app/lib/util/entries.ts
|
|
*/
|
|
export const entries = <O extends Record<string, unknown>>(
|
|
obj: O
|
|
): {
|
|
readonly [K in keyof O]: [K, O[K]];
|
|
}[keyof O][] => {
|
|
return Object.entries(obj) as {
|
|
[K in keyof O]: [K, O[K]];
|
|
}[keyof O][];
|
|
};
|
|
|
|
/**
|
|
* Returns a type with all readonly notations removed (traverses recursively on an object)
|
|
*/
|
|
type DeepWriteable<T> = T extends Readonly<{
|
|
-readonly [K in keyof T]: T[K];
|
|
}>
|
|
? {
|
|
-readonly [K in keyof T]: DeepWriteable<T[K]>;
|
|
}
|
|
: T; /* Make it work with readonly types (this is not strictly necessary) */
|
|
|
|
type FromEntries<T> = T extends [infer Keys, unknown][]
|
|
? { [K in Keys & PropertyKey]: Extract<T[number], [K, unknown]>[1] }
|
|
: never;
|
|
|
|
/**
|
|
* Like Object.fromEntries, but with actually useful typings
|
|
* @param arr The tuple array (`[key, value][]`) to turn into an object
|
|
* @returns Object constructed from the given entries
|
|
* @see https://github.com/3x071c/lsg-remix/blob/e2a9592ba3ec5103556f2cf307c32f08aeaee32d/app/lib/util/fromEntries.ts
|
|
*/
|
|
export const fromEntries = <
|
|
E extends [PropertyKey, unknown][] | ReadonlyArray<readonly [PropertyKey, unknown]>
|
|
>(
|
|
entries: E
|
|
): FromEntries<DeepWriteable<E>> => {
|
|
return Object.fromEntries(entries) as FromEntries<DeepWriteable<E>>;
|
|
};
|
|
|
|
export const getAccessLinkResponseSchema = z.object({
|
|
download_link: z.string().url(),
|
|
});
|
|
|
|
export type GetAccessLinkResponseSchema = z.infer<typeof getAccessLinkResponseSchema>;
|
|
|
|
/** Facilitates converting values from Select inputs to plain ones before submitting */
|
|
export const optionToValueSchema = <T extends z.ZodTypeAny>(valueSchema: T) =>
|
|
z
|
|
.object({
|
|
label: z.string(),
|
|
value: valueSchema,
|
|
})
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
.transform((foo) => (foo as any).value as z.infer<T>);
|
|
|
|
/**
|
|
* Allows parsing without losing original data inference.
|
|
* @url https://github.com/colinhacks/zod/discussions/1655#discussioncomment-4367368
|
|
*/
|
|
export const getParserWithGeneric =
|
|
<T extends AnyZodObject>(valueSchema: T) =>
|
|
<Data>(data: Data) => {
|
|
type Output = z.infer<T>;
|
|
type SimpleFormValues = string | number | null | undefined;
|
|
return valueSchema.parse(data) as {
|
|
// TODO: Invesitage why this broke on zod 3.22.2 upgrade
|
|
[key in keyof Data]: Data[key] extends SimpleFormValues ? Data[key] : Output[key];
|
|
};
|
|
};
|
|
export const sendDailyVideoRecordingEmailsSchema = z.object({
|
|
recordingId: z.string(),
|
|
bookingUID: z.string(),
|
|
});
|
|
|
|
export const downloadLinkSchema = z.object({
|
|
download_link: z.string(),
|
|
});
|
|
|
|
// All properties within event type that can and will be updated if needed
|
|
export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect, "id">]: true } = {
|
|
title: true,
|
|
description: true,
|
|
interfaceLanguage: true,
|
|
isInstantEvent: true,
|
|
instantMeetingParameters: true,
|
|
instantMeetingExpiryTimeOffsetInSeconds: true,
|
|
aiPhoneCallConfig: true,
|
|
currency: true,
|
|
periodDays: true,
|
|
position: true,
|
|
price: true,
|
|
slug: true,
|
|
length: true,
|
|
offsetStart: true,
|
|
locations: true,
|
|
hidden: true,
|
|
availability: true,
|
|
recurringEvent: true,
|
|
customInputs: true,
|
|
disableGuests: true,
|
|
disableCancelling: true,
|
|
disableRescheduling: true,
|
|
allowReschedulingCancelledBookings: true,
|
|
requiresConfirmation: true,
|
|
canSendCalVideoTranscriptionEmails: true,
|
|
requiresConfirmationForFreeEmail: true,
|
|
requiresConfirmationWillBlockSlot: true,
|
|
eventName: true,
|
|
metadata: true,
|
|
children: true,
|
|
hideCalendarNotes: true,
|
|
hideCalendarEventDetails: true,
|
|
minimumBookingNotice: true,
|
|
beforeEventBuffer: true,
|
|
afterEventBuffer: true,
|
|
successRedirectUrl: true,
|
|
seatsPerTimeSlot: true,
|
|
seatsShowAttendees: true,
|
|
seatsShowAvailabilityCount: true,
|
|
forwardParamsSuccessRedirect: true,
|
|
periodType: true,
|
|
hashedLink: true,
|
|
webhooks: true,
|
|
periodStartDate: true,
|
|
periodEndDate: true,
|
|
destinationCalendar: true,
|
|
periodCountCalendarDays: true,
|
|
bookingLimits: true,
|
|
onlyShowFirstAvailableSlot: true,
|
|
slotInterval: true,
|
|
scheduleId: true,
|
|
workflows: true,
|
|
bookingFields: true,
|
|
durationLimits: true,
|
|
maxActiveBookingsPerBooker: true,
|
|
maxActiveBookingPerBookerOfferReschedule: true,
|
|
lockTimeZoneToggleOnBookingPage: true,
|
|
lockedTimeZone: true,
|
|
requiresBookerEmailVerification: true,
|
|
assignAllTeamMembers: true,
|
|
isRRWeightsEnabled: true,
|
|
eventTypeColor: true,
|
|
allowReschedulingPastBookings: true,
|
|
hideOrganizerEmail: true,
|
|
rescheduleWithSameRoundRobinHost: true,
|
|
maxLeadThreshold: true,
|
|
customReplyToEmail: true,
|
|
bookingRequiresAuthentication: true,
|
|
};
|
|
|
|
// All properties that are defined as unlocked based on all managed props
|
|
// Eventually this is going to be just a default and the user can change the config through the UI
|
|
export const unlockedManagedEventTypeProps = {
|
|
locations: allManagedEventTypeProps.locations,
|
|
scheduleId: allManagedEventTypeProps.scheduleId,
|
|
destinationCalendar: allManagedEventTypeProps.destinationCalendar,
|
|
};
|
|
|
|
export const emailSchema = emailRegexSchema;
|
|
|
|
// The PR at https://github.com/colinhacks/zod/pull/2157 addresses this issue and improves email validation
|
|
// I introduced this refinement(to be used with z.email()) as a short term solution until we upgrade to a zod
|
|
// version that will include updates in the above PR.
|
|
export const emailSchemaRefinement = (value: string) => {
|
|
return emailRegex.test(value);
|
|
};
|
|
|
|
export const signupSchema = z.object({
|
|
// Username is marked optional here because it's requirement depends on if it's the Organization invite or a team invite which isn't easily done in zod
|
|
// It's better handled beyond zod in `validateAndGetCorrectedUsernameAndEmail`
|
|
username: z.string().optional(),
|
|
email: z.string().regex(emailRegex, { message: "Invalid email" }),
|
|
password: z.string().superRefine((data, ctx) => {
|
|
const isStrict = false;
|
|
const result = isPasswordValid(data, true, isStrict);
|
|
Object.keys(result).map((key: string) => {
|
|
if (!result[key as keyof typeof result]) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: [key],
|
|
message: key,
|
|
});
|
|
}
|
|
});
|
|
}),
|
|
language: z.string().optional(),
|
|
token: z.string().optional(),
|
|
});
|
|
|
|
export const ZVerifyCodeInputSchema = z.object({
|
|
email: emailSchema,
|
|
code: z.string(),
|
|
});
|
|
|
|
export type ZVerifyCodeInputSchema = z.infer<typeof ZVerifyCodeInputSchema>;
|
|
|
|
export const coerceToDate = z.coerce.date();
|
|
export const getStringAsNumberRequiredSchema = (t: TFunction) =>
|
|
z.string().min(1, t("error_required_field")).pipe(z.coerce.number());
|
|
|
|
export const bookingSeatDataSchema = z.object({
|
|
description: z.string().optional(),
|
|
responses: bookingResponses,
|
|
});
|
|
|
|
// Schema for decrypted service account key
|
|
export const serviceAccountKeySchema = z
|
|
.object({
|
|
private_key: z.string(),
|
|
client_email: z.string().optional(),
|
|
client_id: z.string(),
|
|
tenant_id: z.string().optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
export type TServiceAccountKeySchema = z.infer<typeof serviceAccountKeySchema>;
|
|
|
|
export const rrSegmentQueryValueSchema = zodAttributesQueryValue.nullish();
|