2022-12-01 21:53:52 +00:00
|
|
|
import { EventTypeCustomInputType } from "@prisma/client";
|
2023-03-02 18:15:28 +00:00
|
|
|
import type { UnitTypeLongPlural } from "dayjs";
|
2022-10-07 20:48:41 +00:00
|
|
|
import z, { ZodNullable, ZodObject, ZodOptional } from "zod";
|
|
|
|
|
|
|
|
/* eslint-disable no-underscore-dangle */
|
|
|
|
import type {
|
|
|
|
objectInputType,
|
|
|
|
objectOutputType,
|
|
|
|
ZodNullableDef,
|
|
|
|
ZodOptionalDef,
|
|
|
|
ZodRawShape,
|
|
|
|
ZodTypeAny,
|
|
|
|
} from "zod";
|
2022-01-21 21:35:31 +00:00
|
|
|
|
2022-10-14 18:35:30 +00:00
|
|
|
import { appDataSchemas } from "@calcom/app-store/apps.schemas.generated";
|
2022-06-28 20:40:58 +00:00
|
|
|
import dayjs from "@calcom/dayjs";
|
2023-03-02 18:15:28 +00:00
|
|
|
import { fieldsSchema as formBuilderFieldsSchema } from "@calcom/features/form-builder/FormBuilderFieldsSchema";
|
2022-02-21 18:45:35 +00:00
|
|
|
import { slugify } from "@calcom/lib/slugify";
|
2022-01-21 21:35:31 +00:00
|
|
|
|
2022-06-10 00:32:34 +00:00
|
|
|
// 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,
|
|
|
|
}
|
|
|
|
|
2022-12-05 12:12:14 +00:00
|
|
|
export const RequiresConfirmationThresholdUnits: z.ZodType<UnitTypeLongPlural> = z.enum(["hours", "minutes"]);
|
|
|
|
|
2022-10-14 16:24:43 +00:00
|
|
|
export const EventTypeMetaDataSchema = z
|
|
|
|
.object({
|
|
|
|
smartContractAddress: z.string().optional(),
|
|
|
|
blockchainId: z.number().optional(),
|
2022-11-28 18:14:01 +00:00
|
|
|
multipleDuration: z.number().array().optional(),
|
2022-10-14 16:24:43 +00:00
|
|
|
giphyThankYouPage: z.string().optional(),
|
|
|
|
apps: z.object(appDataSchemas).partial().optional(),
|
2022-10-17 09:43:16 +00:00
|
|
|
additionalNotesRequired: z.boolean().optional(),
|
2023-01-02 08:41:39 +00:00
|
|
|
disableSuccessPage: z.boolean().optional(),
|
2022-12-05 12:12:14 +00:00
|
|
|
requiresConfirmationThreshold: z
|
|
|
|
.object({
|
|
|
|
time: z.number(),
|
|
|
|
unit: RequiresConfirmationThresholdUnits,
|
|
|
|
})
|
|
|
|
.optional(),
|
2022-11-03 14:24:07 +00:00
|
|
|
config: z
|
|
|
|
.object({
|
|
|
|
useHostSchedulesForTeamEvent: z.boolean().optional(),
|
|
|
|
})
|
|
|
|
.optional(),
|
2022-10-14 16:24:43 +00:00
|
|
|
})
|
|
|
|
.nullable();
|
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
export const eventTypeBookingFields = formBuilderFieldsSchema;
|
|
|
|
export const BookingFieldType = eventTypeBookingFields.element.shape.type.Enum;
|
|
|
|
export type BookingFieldType = typeof BookingFieldType extends z.Values<infer T> ? T[number] : never;
|
|
|
|
|
|
|
|
// 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
|
|
|
|
export const bookingResponses = z
|
|
|
|
.object({
|
|
|
|
email: z.string(),
|
|
|
|
name: z.string(),
|
|
|
|
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();
|
|
|
|
|
2022-01-21 21:35:31 +00:00
|
|
|
export const eventTypeLocations = z.array(
|
2022-03-13 15:56:56 +00:00
|
|
|
z.object({
|
2022-08-26 00:48:50 +00:00
|
|
|
// 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(),
|
2022-03-13 15:56:56 +00:00
|
|
|
address: z.string().optional(),
|
|
|
|
link: z.string().url().optional(),
|
2022-05-25 20:34:08 +00:00
|
|
|
displayLocationPublicly: z.boolean().optional(),
|
2022-05-16 15:50:12 +00:00
|
|
|
hostPhoneNumber: z.string().optional(),
|
2022-03-13 15:56:56 +00:00
|
|
|
})
|
2022-01-21 21:35:31 +00:00
|
|
|
);
|
|
|
|
|
2022-05-05 21:16:25 +00:00
|
|
|
// Matching RRule.Options: rrule/dist/esm/src/types.d.ts
|
2022-06-10 00:32:34 +00:00
|
|
|
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();
|
2022-05-05 21:16:25 +00:00
|
|
|
|
2022-10-17 09:07:49 +00:00
|
|
|
// 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;
|
|
|
|
});
|
|
|
|
|
2022-10-12 05:29:04 +00:00
|
|
|
export const bookingLimitsType = z
|
|
|
|
.object({
|
|
|
|
PER_DAY: z.number().optional(),
|
|
|
|
PER_WEEK: z.number().optional(),
|
|
|
|
PER_MONTH: z.number().optional(),
|
|
|
|
PER_YEAR: z.number().optional(),
|
|
|
|
})
|
|
|
|
.nullable();
|
|
|
|
|
2022-02-21 16:53:16 +00:00
|
|
|
export const eventTypeSlug = z.string().transform((val) => slugify(val.trim()));
|
2022-06-10 00:32:34 +00:00
|
|
|
|
2022-01-21 21:35:31 +00:00
|
|
|
export const stringToDate = z.string().transform((a) => new Date(a));
|
2022-06-10 00:32:34 +00:00
|
|
|
|
|
|
|
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 stringToDayjs = z.string().transform((val) => dayjs(val));
|
2022-06-10 18:38:46 +00:00
|
|
|
|
|
|
|
export const bookingCreateBodySchema = z.object({
|
|
|
|
end: z.string(),
|
|
|
|
eventTypeId: z.number(),
|
2022-06-11 21:30:52 +00:00
|
|
|
eventTypeSlug: z.string().optional(),
|
2022-06-10 18:38:46 +00:00
|
|
|
rescheduleUid: z.string().optional(),
|
|
|
|
recurringEventId: z.string().optional(),
|
|
|
|
start: z.string(),
|
|
|
|
timeZone: z.string(),
|
|
|
|
user: z.union([z.string(), z.array(z.string())]).optional(),
|
|
|
|
language: z.string(),
|
|
|
|
bookingUid: z.string().optional(),
|
|
|
|
metadata: z.record(z.string()),
|
2022-06-11 21:30:52 +00:00
|
|
|
hasHashedBookingLink: z.boolean().optional(),
|
2022-06-10 18:38:46 +00:00
|
|
|
hashedLink: z.string().nullish(),
|
2022-09-05 21:10:58 +00:00
|
|
|
ethSignature: z.string().optional(),
|
2022-06-10 18:38:46 +00:00
|
|
|
});
|
|
|
|
|
2022-08-29 13:04:22 +00:00
|
|
|
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),
|
|
|
|
]);
|
|
|
|
|
2022-06-11 21:30:52 +00:00
|
|
|
export type BookingCreateBody = z.input<typeof bookingCreateBodySchema>;
|
|
|
|
|
2022-08-26 21:58:08 +00:00
|
|
|
export const bookingConfirmPatchBodySchema = z.object({
|
|
|
|
bookingId: z.number(),
|
|
|
|
confirmed: z.boolean(),
|
|
|
|
recurringEventId: z.string().optional(),
|
|
|
|
reason: z.string().optional(),
|
|
|
|
});
|
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
// `responses` is merged with it during handleNewBooking call because `responses` schema is dynamic and depends on eventType
|
2022-06-10 18:38:46 +00:00
|
|
|
export const extendedBookingCreateBody = bookingCreateBodySchema.merge(
|
|
|
|
z.object({
|
|
|
|
noEmail: z.boolean().optional(),
|
|
|
|
recurringCount: z.number().optional(),
|
2022-11-08 20:59:44 +00:00
|
|
|
allRecurringDates: z.string().array().optional(),
|
|
|
|
currentRecurringIndex: z.number().optional(),
|
2022-10-19 16:11:50 +00:00
|
|
|
appsStatus: z
|
|
|
|
.array(
|
|
|
|
z.object({
|
|
|
|
appName: z.string(),
|
|
|
|
success: z.number(),
|
|
|
|
failures: z.number(),
|
|
|
|
type: z.string(),
|
2022-11-22 20:44:08 +00:00
|
|
|
errors: z.string().array(),
|
|
|
|
warnings: z.string().array().optional(),
|
2022-10-19 16:11:50 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
.optional(),
|
2022-06-10 18:38:46 +00:00
|
|
|
})
|
|
|
|
);
|
2022-06-14 20:07:54 +00:00
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
// It has only the legacy props that are part of `responses` now. The API can still hit old props
|
|
|
|
export const bookingCreateSchemaLegacyPropsForApi = z.object({
|
|
|
|
email: z.string(),
|
|
|
|
name: z.string(),
|
|
|
|
guests: z.array(z.string()).optional(),
|
|
|
|
notes: z.string().optional(),
|
|
|
|
location: z.string(),
|
|
|
|
smsReminderNumber: z.string().optional().nullable(),
|
|
|
|
rescheduleReason: z.string().optional(),
|
|
|
|
customInputs: z.array(z.object({ label: z.string(), value: z.union([z.string(), z.boolean()]) })),
|
|
|
|
});
|
|
|
|
|
|
|
|
// This is the schema that is used for the API. It has all the legacy props that are part of `responses` now.
|
|
|
|
export const bookingCreateBodySchemaForApi = extendedBookingCreateBody.merge(
|
|
|
|
bookingCreateSchemaLegacyPropsForApi
|
|
|
|
);
|
|
|
|
|
2022-10-20 23:28:02 +00:00
|
|
|
export const schemaBookingCancelParams = z.object({
|
|
|
|
id: z.number().optional(),
|
|
|
|
uid: z.string().optional(),
|
|
|
|
allRemainingBookings: z.boolean().optional(),
|
|
|
|
cancellationReason: z.string().optional(),
|
|
|
|
});
|
|
|
|
|
2022-06-14 20:07:54 +00:00
|
|
|
export const vitalSettingsUpdateSchema = z.object({
|
|
|
|
connected: z.boolean().optional(),
|
|
|
|
selectedParam: z.string().optional(),
|
|
|
|
sleepValue: z.number().optional(),
|
|
|
|
});
|
|
|
|
|
2022-09-05 19:13:49 +00:00
|
|
|
export const createdEventSchema = z
|
|
|
|
.object({
|
|
|
|
id: z.string(),
|
|
|
|
password: z.union([z.string(), z.undefined()]),
|
|
|
|
onlineMeetingUrl: z.string().nullable(),
|
|
|
|
})
|
|
|
|
.passthrough();
|
|
|
|
|
2022-06-14 20:07:54 +00:00
|
|
|
export const userMetadata = z
|
|
|
|
.object({
|
2022-06-16 19:33:23 +00:00
|
|
|
proPaidForByTeamId: z.number().optional(),
|
2022-06-14 20:07:54 +00:00
|
|
|
stripeCustomerId: z.string().optional(),
|
|
|
|
vitalSettings: vitalSettingsUpdateSchema.optional(),
|
2022-07-06 19:31:07 +00:00
|
|
|
isPremium: z.boolean().optional(),
|
2023-01-31 20:44:14 +00:00
|
|
|
sessionTimeout: z.number().optional(), // Minutes
|
2023-02-13 22:55:32 +00:00
|
|
|
defaultConferencingApp: z
|
|
|
|
.object({
|
|
|
|
appSlug: z.string().default("daily-video").optional(),
|
|
|
|
appLink: z.string().optional(),
|
|
|
|
})
|
|
|
|
.optional(),
|
2022-06-14 20:07:54 +00:00
|
|
|
})
|
|
|
|
.nullable();
|
2022-08-01 21:44:08 +00:00
|
|
|
|
2022-11-10 20:23:56 +00:00
|
|
|
export const teamMetadataSchema = z
|
|
|
|
.object({
|
|
|
|
requestedSlug: z.string(),
|
|
|
|
paymentId: z.string(),
|
|
|
|
subscriptionId: z.string().nullable(),
|
|
|
|
subscriptionItemId: z.string().nullable(),
|
|
|
|
})
|
|
|
|
.partial()
|
|
|
|
.nullable();
|
|
|
|
|
2022-12-15 21:43:07 +00:00
|
|
|
export const bookingMetadataSchema = z
|
|
|
|
.object({
|
|
|
|
videoCallUrl: z.string().optional(),
|
|
|
|
})
|
2023-02-20 02:00:23 +00:00
|
|
|
.and(z.record(z.string()))
|
2022-12-15 21:43:07 +00:00
|
|
|
.nullable();
|
|
|
|
|
2022-12-01 21:53:52 +00:00
|
|
|
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(),
|
2022-12-26 10:55:58 +00:00
|
|
|
hasToBeCreated: z.boolean().optional(),
|
2022-12-01 21:53:52 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
export type CustomInputSchema = z.infer<typeof customInputSchema>;
|
|
|
|
|
2022-12-27 21:03:39 +00:00
|
|
|
export const recordingItemSchema = z.object({
|
|
|
|
id: z.string(),
|
|
|
|
room_name: z.string(),
|
|
|
|
start_ts: z.number(),
|
|
|
|
status: z.string(),
|
|
|
|
max_participants: z.number(),
|
|
|
|
duration: z.number(),
|
|
|
|
share_token: z.string(),
|
|
|
|
});
|
|
|
|
|
|
|
|
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>;
|
|
|
|
|
2022-08-01 21:44:08 +00:00
|
|
|
/**
|
|
|
|
* Ensures that it is a valid HTTP URL
|
|
|
|
* It automatically avoids
|
|
|
|
* - XSS attempts through javascript:alert('hi')
|
|
|
|
* - mailto: links
|
|
|
|
*/
|
|
|
|
export const successRedirectUrl = z
|
2022-08-02 00:45:47 +00:00
|
|
|
.union([
|
|
|
|
z.literal(""),
|
|
|
|
z
|
|
|
|
.string()
|
|
|
|
.url()
|
|
|
|
.regex(/^http(s)?:\/\/.*/),
|
|
|
|
])
|
|
|
|
.optional();
|
2022-10-07 20:48:41 +00:00
|
|
|
|
2022-11-03 14:40:03 +00:00
|
|
|
export const RoutingFormSettings = z
|
|
|
|
.object({
|
|
|
|
emailOwnerOnSubmission: z.boolean(),
|
|
|
|
})
|
|
|
|
.nullable();
|
|
|
|
|
2023-01-26 20:36:15 +00:00
|
|
|
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();
|
|
|
|
|
2022-10-07 20:48:41 +00:00
|
|
|
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
|
|
|
|
*/
|
2023-01-27 01:50:56 +00:00
|
|
|
export const entries = <O extends Record<string, unknown>>(
|
2022-10-07 20:48:41 +00:00
|
|
|
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>>;
|
|
|
|
};
|