fix: disregarding already booked spots or blocked calendar times (#2029)
* fix: double booking * fix: update double-booking error response code * fix: update double-booking error response code * test: add test * fix: check availability before creating booking * Update apps/web/playwright/booking-pages.test.ts * Update yarn.lock * Restored missing fix Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
parent
322a845a17
commit
b143498393
4 changed files with 177 additions and 85 deletions
|
@ -540,7 +540,9 @@ const BookingPage = (props: BookingPageProps) => {
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
{mutation.isError && (
|
{mutation.isError && (
|
||||||
<div className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
<div
|
||||||
|
data-testid="booking-fail"
|
||||||
|
className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { getErrorFromUnknown } from "@lib/errors";
|
||||||
import { getEventName } from "@lib/event";
|
import { getEventName } from "@lib/event";
|
||||||
import EventManager, { EventResult, PartialReference } from "@lib/events/EventManager";
|
import EventManager, { EventResult, PartialReference } from "@lib/events/EventManager";
|
||||||
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
|
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
|
||||||
|
import { EventBusyDate } from "@lib/integrations/calendar/constants/types";
|
||||||
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
|
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||||
import { BufferedBusyTime } from "@lib/integrations/calendar/interfaces/Office365Calendar";
|
import { BufferedBusyTime } from "@lib/integrations/calendar/interfaces/Office365Calendar";
|
||||||
import logger from "@lib/logger";
|
import logger from "@lib/logger";
|
||||||
|
@ -408,27 +409,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type Booking = Prisma.PromiseReturnType<typeof createBooking>;
|
|
||||||
let booking: Booking | null = null;
|
|
||||||
try {
|
|
||||||
booking = await createBooking();
|
|
||||||
evt.uid = booking.uid;
|
|
||||||
} catch (_err) {
|
|
||||||
const err = getErrorFromUnknown(_err);
|
|
||||||
log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
|
|
||||||
if (err.code === "P2002") {
|
|
||||||
res.status(409).json({ message: "booking.conflict" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.status(500).end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let results: EventResult[] = [];
|
let results: EventResult[] = [];
|
||||||
let referencesToCreate: PartialReference[] = [];
|
let referencesToCreate: PartialReference[] = [];
|
||||||
let user: User | null = null;
|
let user: User | null = null;
|
||||||
|
|
||||||
/** Let's start cheking for availability */
|
/** Let's start checking for availability */
|
||||||
for (const currentUser of users) {
|
for (const currentUser of users) {
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
console.error(`currentUser not found`);
|
console.error(`currentUser not found`);
|
||||||
|
@ -443,68 +428,94 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
});
|
});
|
||||||
|
|
||||||
const credentials = currentUser.credentials;
|
const credentials = currentUser.credentials;
|
||||||
|
const calendarBusyTimes: EventBusyDate[] = await prisma.booking
|
||||||
|
.findMany({
|
||||||
|
where: {
|
||||||
|
userId: currentUser.id,
|
||||||
|
eventTypeId: eventTypeId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((bookings) => bookings.map((booking) => ({ end: booking.endTime, start: booking.startTime })));
|
||||||
|
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
const calendarBusyTimes = await getBusyCalendarTimes(
|
await getBusyCalendarTimes(credentials, reqBody.start, reqBody.end, selectedCalendars).then(
|
||||||
credentials,
|
(busyTimes) => calendarBusyTimes.push(...busyTimes)
|
||||||
reqBody.start,
|
|
||||||
reqBody.end,
|
|
||||||
selectedCalendars
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter(notEmpty);
|
const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter(notEmpty);
|
||||||
calendarBusyTimes.push(...videoBusyTimes);
|
calendarBusyTimes.push(...videoBusyTimes);
|
||||||
console.log("calendarBusyTimes==>>>", calendarBusyTimes);
|
|
||||||
|
|
||||||
const bufferedBusyTimes: BufferedBusyTimes = calendarBusyTimes.map((a) => ({
|
|
||||||
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
|
|
||||||
end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
let isAvailableToBeBooked = true;
|
|
||||||
try {
|
|
||||||
isAvailableToBeBooked = isAvailable(bufferedBusyTimes, reqBody.start, eventType.length);
|
|
||||||
} catch {
|
|
||||||
log.debug({
|
|
||||||
message: "Unable set isAvailableToBeBooked. Using true. ",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAvailableToBeBooked) {
|
|
||||||
const error = {
|
|
||||||
errorCode: "BookingUserUnAvailable",
|
|
||||||
message: `${currentUser.name} is unavailable at this time.`,
|
|
||||||
};
|
|
||||||
|
|
||||||
log.debug(`Booking ${currentUser.name} failed`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeOutOfBounds = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
timeOutOfBounds = isOutOfBounds(reqBody.start, {
|
|
||||||
periodType: eventType.periodType,
|
|
||||||
periodDays: eventType.periodDays,
|
|
||||||
periodEndDate: eventType.periodEndDate,
|
|
||||||
periodStartDate: eventType.periodStartDate,
|
|
||||||
periodCountCalendarDays: eventType.periodCountCalendarDays,
|
|
||||||
timeZone: currentUser.timeZone,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
log.debug({
|
|
||||||
message: "Unable set timeOutOfBounds. Using false. ",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeOutOfBounds) {
|
|
||||||
const error = {
|
|
||||||
errorCode: "BookingUserUnAvailable",
|
|
||||||
message: `${currentUser.name} is unavailable at this time.`,
|
|
||||||
};
|
|
||||||
|
|
||||||
log.debug(`Booking ${currentUser.name} failed`, error);
|
|
||||||
res.status(400).json(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("calendarBusyTimes==>>>", calendarBusyTimes);
|
||||||
|
|
||||||
|
const bufferedBusyTimes: BufferedBusyTimes = calendarBusyTimes.map((a) => ({
|
||||||
|
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
|
||||||
|
end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let isAvailableToBeBooked = true;
|
||||||
|
try {
|
||||||
|
isAvailableToBeBooked = isAvailable(bufferedBusyTimes, reqBody.start, eventType.length);
|
||||||
|
} catch {
|
||||||
|
log.debug({
|
||||||
|
message: "Unable set isAvailableToBeBooked. Using true. ",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAvailableToBeBooked) {
|
||||||
|
const error = {
|
||||||
|
errorCode: "BookingUserUnAvailable",
|
||||||
|
message: `${currentUser.name} is unavailable at this time.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
log.debug(`Booking ${currentUser.name} failed`, error);
|
||||||
|
res.status(409).json(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeOutOfBounds = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
timeOutOfBounds = isOutOfBounds(reqBody.start, {
|
||||||
|
periodType: eventType.periodType,
|
||||||
|
periodDays: eventType.periodDays,
|
||||||
|
periodEndDate: eventType.periodEndDate,
|
||||||
|
periodStartDate: eventType.periodStartDate,
|
||||||
|
periodCountCalendarDays: eventType.periodCountCalendarDays,
|
||||||
|
timeZone: currentUser.timeZone,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
log.debug({
|
||||||
|
message: "Unable set timeOutOfBounds. Using false. ",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeOutOfBounds) {
|
||||||
|
const error = {
|
||||||
|
errorCode: "BookingUserUnAvailable",
|
||||||
|
message: `${currentUser.name} is unavailable at this time.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
log.debug(`Booking ${currentUser.name} failed`, error);
|
||||||
|
res.status(400).json(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Booking = Prisma.PromiseReturnType<typeof createBooking>;
|
||||||
|
let booking: Booking | null = null;
|
||||||
|
try {
|
||||||
|
booking = await createBooking();
|
||||||
|
evt.uid = booking.uid;
|
||||||
|
} catch (_err) {
|
||||||
|
const err = getErrorFromUnknown(_err);
|
||||||
|
log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
|
||||||
|
if (err.code === "P2002") {
|
||||||
|
res.status(409).json({ message: "booking.conflict" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(500).end();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) throw Error("Can't continue, user not found.");
|
if (!user) throw Error("Can't continue, user not found.");
|
||||||
|
|
|
@ -1,16 +1,80 @@
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
import { todo } from "./lib/testUtils";
|
import { todo } from "./lib/testUtils";
|
||||||
|
|
||||||
|
const deleteBookingsByEmail = async (email: string) =>
|
||||||
|
prisma.booking.deleteMany({
|
||||||
|
where: {
|
||||||
|
user: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
test.describe("free user", () => {
|
test.describe("free user", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto("/free");
|
await page.goto("/free");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.afterEach(async () => {
|
||||||
|
// delete test bookings
|
||||||
|
await deleteBookingsByEmail("free@example.com");
|
||||||
|
});
|
||||||
|
|
||||||
test("only one visible event", async ({ page }) => {
|
test("only one visible event", async ({ page }) => {
|
||||||
await expect(page.locator(`[href="/free/30min"]`)).toBeVisible();
|
await expect(page.locator(`[href="/free/30min"]`)).toBeVisible();
|
||||||
await expect(page.locator(`[href="/free/60min"]`)).not.toBeVisible();
|
await expect(page.locator(`[href="/free/60min"]`)).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("cannot book same slot multiple times", async ({ page }) => {
|
||||||
|
// Click first event type
|
||||||
|
await page.click('[data-testid="event-type-link"]');
|
||||||
|
// Click [data-testid="incrementMonth"]
|
||||||
|
await page.click('[data-testid="incrementMonth"]');
|
||||||
|
// Click [data-testid="day"]
|
||||||
|
await page.click('[data-testid="day"][data-disabled="false"]');
|
||||||
|
// Click [data-testid="time"]
|
||||||
|
await page.click('[data-testid="time"]');
|
||||||
|
|
||||||
|
// Navigate to book page
|
||||||
|
await page.waitForNavigation({
|
||||||
|
url(url) {
|
||||||
|
return url.pathname.endsWith("/book");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// save booking url
|
||||||
|
const bookingUrl: string = page.url();
|
||||||
|
|
||||||
|
const bookTimeSlot = async () => {
|
||||||
|
// --- fill form
|
||||||
|
await page.fill('[name="name"]', "Test Testson");
|
||||||
|
await page.fill('[name="email"]', "test@example.com");
|
||||||
|
await page.press('[name="email"]', "Enter");
|
||||||
|
};
|
||||||
|
|
||||||
|
// book same time spot twice
|
||||||
|
await bookTimeSlot();
|
||||||
|
|
||||||
|
// Make sure we're navigated to the success page
|
||||||
|
await page.waitForNavigation({
|
||||||
|
url(url) {
|
||||||
|
return url.pathname.endsWith("/success");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// return to same time spot booking page
|
||||||
|
await page.goto(bookingUrl);
|
||||||
|
|
||||||
|
// book same time spot again
|
||||||
|
await bookTimeSlot();
|
||||||
|
|
||||||
|
// check for error message
|
||||||
|
await expect(page.locator("[data-testid=booking-fail]")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
todo("`/free/30min` is bookable");
|
todo("`/free/30min` is bookable");
|
||||||
|
|
||||||
todo("`/free/60min` is not bookable");
|
todo("`/free/60min` is not bookable");
|
||||||
|
@ -21,6 +85,11 @@ test.describe("pro user", () => {
|
||||||
await page.goto("/pro");
|
await page.goto("/pro");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.afterEach(async () => {
|
||||||
|
// delete test bookings
|
||||||
|
await deleteBookingsByEmail("pro@example.com");
|
||||||
|
});
|
||||||
|
|
||||||
test("pro user's page has at least 2 visible events", async ({ page }) => {
|
test("pro user's page has at least 2 visible events", async ({ page }) => {
|
||||||
const $eventTypes = await page.$$("[data-testid=event-types] > *");
|
const $eventTypes = await page.$$("[data-testid=event-types] > *");
|
||||||
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
|
expect($eventTypes.length).toBeGreaterThanOrEqual(2);
|
||||||
|
|
32
yarn.lock
32
yarn.lock
|
@ -1012,7 +1012,12 @@
|
||||||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.4.3.tgz#f77c6bb5cb4a614a5d730fb880cab502d48abf37"
|
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.4.3.tgz#f77c6bb5cb4a614a5d730fb880cab502d48abf37"
|
||||||
integrity sha512-n2IQkaaw0aAAlQS5MEXsM4uRK+w18CrM72EqnGRl/UBOQeQajad8oiKXR9Nk15jOzTFQjpxzrZMf1NxHidFBiw==
|
integrity sha512-n2IQkaaw0aAAlQS5MEXsM4uRK+w18CrM72EqnGRl/UBOQeQajad8oiKXR9Nk15jOzTFQjpxzrZMf1NxHidFBiw==
|
||||||
|
|
||||||
"@heroicons/react@^1.0.4", "@heroicons/react@^1.0.5":
|
"@heroicons/react@^1.0.4":
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.6.tgz#35dd26987228b39ef2316db3b1245c42eb19e324"
|
||||||
|
integrity sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==
|
||||||
|
|
||||||
|
"@heroicons/react@^1.0.5":
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.5.tgz#2fe4df9d33eb6ce6d5178a0f862e97b61c01e27d"
|
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.5.tgz#2fe4df9d33eb6ce6d5178a0f862e97b61c01e27d"
|
||||||
integrity sha512-UDMyLM2KavIu2vlWfMspapw9yii7aoLwzI2Hudx4fyoPwfKfxU8r3cL8dEBXOjcLG0/oOONZzbT14M1HoNtEcg==
|
integrity sha512-UDMyLM2KavIu2vlWfMspapw9yii7aoLwzI2Hudx4fyoPwfKfxU8r3cL8dEBXOjcLG0/oOONZzbT14M1HoNtEcg==
|
||||||
|
@ -2590,9 +2595,9 @@
|
||||||
integrity sha512-fm8TR8r4LwbXgBIYdPmeMjJJkxxFC66tvoliNnmXOpUgZSgQKoNPW3ON0ZphZIiif1oqWNhAaSrr7tOvGu+AFg==
|
integrity sha512-fm8TR8r4LwbXgBIYdPmeMjJJkxxFC66tvoliNnmXOpUgZSgQKoNPW3ON0ZphZIiif1oqWNhAaSrr7tOvGu+AFg==
|
||||||
|
|
||||||
"@stripe/stripe-js@^1.17.1":
|
"@stripe/stripe-js@^1.17.1":
|
||||||
version "1.23.0"
|
version "1.24.0"
|
||||||
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.23.0.tgz#62eed14e83c63c3e8c27f14f6b1e6feb8496c867"
|
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.24.0.tgz#d23977f364565981f8ab30b1b540e367f72abc5c"
|
||||||
integrity sha512-+7w4rVs71Fk8/8uzyzQB5GotHSH9mjOjxM3EYDq/3MR3I2ewELHtvWVMOqfS/9WSKCaKv7h7eFLsMZGpK5jApQ==
|
integrity sha512-8CEILOpzoRhGwvgcf6y+BlPyEq1ZqxAv3gsX7LvokFYvbcyH72GRcHQMGXuZS3s7HqfYQuTSFrvZNL/qdkgA9Q==
|
||||||
|
|
||||||
"@szmarczak/http-timer@^1.1.2":
|
"@szmarczak/http-timer@^1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
|
@ -5072,11 +5077,16 @@ dayjs-business-time@^1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
dayjs "^1.10.4"
|
dayjs "^1.10.4"
|
||||||
|
|
||||||
dayjs@^1.10.4, dayjs@^1.10.6, dayjs@^1.10.7:
|
dayjs@^1.10.4, dayjs@^1.10.6:
|
||||||
version "1.10.7"
|
version "1.10.7"
|
||||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
|
||||||
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==
|
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==
|
||||||
|
|
||||||
|
dayjs@^1.10.7:
|
||||||
|
version "1.10.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41"
|
||||||
|
integrity sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow==
|
||||||
|
|
||||||
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
|
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
@ -11520,9 +11530,9 @@ postcss@8.4.5:
|
||||||
source-map-js "^1.0.1"
|
source-map-js "^1.0.1"
|
||||||
|
|
||||||
postcss@^8.1.6, postcss@^8.3.5, postcss@^8.3.6:
|
postcss@^8.1.6, postcss@^8.3.5, postcss@^8.3.6:
|
||||||
version "8.4.7"
|
version "8.4.8"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.7.tgz#f99862069ec4541de386bf57f5660a6c7a0875a8"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.8.tgz#dad963a76e82c081a0657d3a2f3602ce10c2e032"
|
||||||
integrity sha512-L9Ye3r6hkkCeOETQX6iOaWZgjp3LL6Lpqm6EtgbKrgqGGteRMNb9vzBfRL96YOSu8o7x3MfIH9Mo5cPJFGrW6A==
|
integrity sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid "^3.3.1"
|
nanoid "^3.3.1"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
|
@ -15400,9 +15410,9 @@ zod@^3.8.2:
|
||||||
integrity sha512-daZ80A81I3/9lIydI44motWe6n59kRBfNzTuS2bfzVh1nAXi667TOTWWtatxyG+fwgNUiagSj/CWZwRRbevJIg==
|
integrity sha512-daZ80A81I3/9lIydI44motWe6n59kRBfNzTuS2bfzVh1nAXi667TOTWWtatxyG+fwgNUiagSj/CWZwRRbevJIg==
|
||||||
|
|
||||||
zod@^3.9.5:
|
zod@^3.9.5:
|
||||||
version "3.12.0"
|
version "3.13.4"
|
||||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.12.0.tgz#84ba9f6bdb7835e2483982d5f52cfffcb6a00346"
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.13.4.tgz#5d6fe03ef4824a637d7ef50b5441cf6ab3acede0"
|
||||||
integrity sha512-w+mmntgEL4hDDL5NLFdN6Fq2DSzxfmlSoJqiYE1/CApO8EkOCxvJvRYEVf8Vr/lRs3i6gqoiyFM6KRcWqqdBzQ==
|
integrity sha512-LZRucWt4j/ru5azOkJxCfpR87IyFDn8h2UODdqvXzZLb3K7bb9chUrUIGTy3BPsr8XnbQYfQ5Md5Hu2OYIo1mg==
|
||||||
|
|
||||||
zwitch@^1.0.0:
|
zwitch@^1.0.0:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
|
|
Loading…
Reference in a new issue