diff --git a/package.json b/package.json index adbf9577a6..51394df0de 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,10 @@ "db:rollback": "sequelize db:migrate:undo", "db:reset": "sequelize db:drop && sequelize db:create && sequelize db:migrate", "upgrade": "git fetch && git pull && yarn install && yarn heroku-postbuild", - "test": "jest --config=.jestconfig.json --forceExit", - "test:app": "jest --config=.jestconfig.json --selectProjects app", - "test:shared": "jest --config=.jestconfig.json --selectProjects shared-node shared-jsdom", - "test:server": "jest --config=.jestconfig.json --selectProjects server", + "test": "TZ=UTC jest --config=.jestconfig.json --forceExit", + "test:app": "TZ=UTC jest --config=.jestconfig.json --selectProjects app", + "test:shared": "TZ=UTC jest --config=.jestconfig.json --selectProjects shared-node shared-jsdom", + "test:server": "TZ=UTC jest --config=.jestconfig.json --selectProjects server", "vite:dev": "vite", "vite:build": "vite build", "vite:preview": "vite preview" diff --git a/server/commands/attachmentCreator.ts b/server/commands/attachmentCreator.ts index 76bb4dd5cc..c035ebc2fc 100644 --- a/server/commands/attachmentCreator.ts +++ b/server/commands/attachmentCreator.ts @@ -1,51 +1,98 @@ import { Transaction } from "sequelize"; import { v4 as uuidv4 } from "uuid"; +import { AttachmentPreset } from "@shared/types"; import { Attachment, Event, User } from "@server/models"; +import AttachmentHelper from "@server/models/helpers/AttachmentHelper"; import FileStorage from "@server/storage/files"; +type BaseProps = { + id?: string; + name: string; + user: User; + source?: "import"; + preset: AttachmentPreset; + ip?: string; + transaction?: Transaction; +}; + +type UrlProps = BaseProps & { + url: string; +}; + +type BufferProps = BaseProps & { + buffer: Buffer; + type: string; +}; + +type Props = UrlProps | BufferProps; + export default async function attachmentCreator({ id, name, - type, - buffer, user, source, + preset, ip, transaction, -}: { - id?: string; - name: string; - type: string; - buffer: Buffer; - user: User; - source?: "import"; - ip?: string; - transaction?: Transaction; -}) { - const key = `uploads/${user.id}/${uuidv4()}/${name}`; - const acl = process.env.AWS_S3_ACL || "private"; - const url = await FileStorage.upload({ - body: buffer, - contentType: type, - contentLength: buffer.length, - key, + ...rest +}: Props): Promise { + const acl = AttachmentHelper.presetToAcl(preset); + const key = AttachmentHelper.getKey({ acl, + id: uuidv4(), + name, + userId: user.id, }); - const attachment = await Attachment.create( - { - id, + + let attachment; + + if ("url" in rest) { + const { url } = rest; + const res = await FileStorage.uploadFromUrl(url, key, acl); + + if (!res) { + return; + } + attachment = await Attachment.create( + { + id, + key, + acl, + size: res.contentLength, + contentType: res.contentType, + teamId: user.teamId, + userId: user.id, + }, + { + transaction, + } + ); + } else { + const { buffer, type } = rest; + await FileStorage.upload({ + body: buffer, + contentType: type, + contentLength: buffer.length, key, acl, - url, - size: buffer.length, - contentType: type, - teamId: user.teamId, - userId: user.id, - }, - { - transaction, - } - ); + }); + + attachment = await Attachment.create( + { + id, + key, + acl, + size: buffer.length, + contentType: type, + teamId: user.teamId, + userId: user.id, + }, + { + transaction, + } + ); + } + await Event.create( { name: "attachments.create", @@ -62,5 +109,6 @@ export default async function attachmentCreator({ transaction, } ); + return attachment; } diff --git a/server/commands/documentCreator.ts b/server/commands/documentCreator.ts index 8430271637..52cbc9643f 100644 --- a/server/commands/documentCreator.ts +++ b/server/commands/documentCreator.ts @@ -84,7 +84,15 @@ export default async function documentCreator({ title: templateDocument ? DocumentHelper.replaceTemplateVariables(templateDocument.title, user) : title, - text: templateDocument ? templateDocument.text : text, + text: await DocumentHelper.replaceImagesWithAttachments( + DocumentHelper.replaceTemplateVariables( + templateDocument ? templateDocument.text : text, + user + ), + user, + ip, + transaction + ), state, }, { @@ -112,7 +120,11 @@ export default async function documentCreator({ ); if (publish) { - await document.publish(user.id, collectionId!, { transaction }); + if (!collectionId) { + throw new Error("Collection ID is required to publish"); + } + + await document.publish(user.id, collectionId, { transaction }); await Event.create( { name: "documents.publish", diff --git a/server/commands/documentImporter.ts b/server/commands/documentImporter.ts index 7be7ec27f2..a4dd457061 100644 --- a/server/commands/documentImporter.ts +++ b/server/commands/documentImporter.ts @@ -1,6 +1,5 @@ import path from "path"; import emojiRegex from "emoji-regex"; -import escapeRegExp from "lodash/escapeRegExp"; import truncate from "lodash/truncate"; import mammoth from "mammoth"; import quotedPrintable from "quoted-printable"; @@ -10,12 +9,10 @@ import parseTitle from "@shared/utils/parseTitle"; import { DocumentValidation } from "@shared/validations"; import { traceFunction } from "@server/logging/tracing"; import { User } from "@server/models"; +import DocumentHelper from "@server/models/helpers/DocumentHelper"; import ProsemirrorHelper from "@server/models/helpers/ProsemirrorHelper"; -import dataURItoBuffer from "@server/utils/dataURItoBuffer"; -import parseImages from "@server/utils/parseImages"; import turndownService from "@server/utils/turndown"; import { FileImportError, InvalidRequestError } from "../errors"; -import attachmentCreator from "./attachmentCreator"; interface ImportableFile { type: string; @@ -207,26 +204,12 @@ async function documentImporter({ // to match our hardbreak parser. text = text.replace(/
/gi, "\\n"); - // find data urls, convert to blobs, upload and write attachments - const images = parseImages(text); - const dataURIs = images.filter((href) => href.startsWith("data:")); - - for (const uri of dataURIs) { - const name = "imported"; - const { buffer, type } = dataURItoBuffer(uri); - const attachment = await attachmentCreator({ - name, - type, - buffer, - user, - ip, - transaction, - }); - text = text.replace( - new RegExp(escapeRegExp(uri), "g"), - attachment.redirectUrl - ); - } + text = await DocumentHelper.replaceImagesWithAttachments( + text, + user, + ip, + transaction + ); // It's better to truncate particularly long titles than fail the import title = truncate(title, { length: DocumentValidation.maxTitleLength }); diff --git a/server/models/helpers/DocumentHelper.test.ts b/server/models/helpers/DocumentHelper.test.ts index a920178c91..0c36260b87 100644 --- a/server/models/helpers/DocumentHelper.test.ts +++ b/server/models/helpers/DocumentHelper.test.ts @@ -1,8 +1,39 @@ import Revision from "@server/models/Revision"; -import { buildDocument } from "@server/test/factories"; +import { buildDocument, buildUser } from "@server/test/factories"; import DocumentHelper from "./DocumentHelper"; describe("DocumentHelper", () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(Date.parse("2021-01-01T00:00:00.000Z")); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + describe("replaceTemplateVariables", () => { + it("should replace {time} with current time", async () => { + const user = await buildUser(); + const result = DocumentHelper.replaceTemplateVariables( + "Hello {time}", + user + ); + + expect(result).toBe("Hello 12 00 AM"); + }); + + it("should replace {date} with current date", async () => { + const user = await buildUser(); + const result = DocumentHelper.replaceTemplateVariables( + "Hello {date}", + user + ); + + expect(result).toBe("Hello January 1 2021"); + }); + }); + describe("parseMentions", () => { it("should not parse normal links as mentions", async () => { const document = await buildDocument({ diff --git a/server/models/helpers/DocumentHelper.tsx b/server/models/helpers/DocumentHelper.tsx index 459195b98e..2a65e143ef 100644 --- a/server/models/helpers/DocumentHelper.tsx +++ b/server/models/helpers/DocumentHelper.tsx @@ -6,14 +6,17 @@ import { JSDOM } from "jsdom"; import escapeRegExp from "lodash/escapeRegExp"; import startCase from "lodash/startCase"; import { Node } from "prosemirror-model"; +import { Transaction } from "sequelize"; import * as Y from "yjs"; import textBetween from "@shared/editor/lib/textBetween"; +import { AttachmentPreset } from "@shared/types"; import { getCurrentDateAsString, getCurrentDateTimeAsString, getCurrentTimeAsString, unicodeCLDRtoBCP47, } from "@shared/utils/date"; +import attachmentCreator from "@server/commands/attachmentCreator"; import { parser, schema } from "@server/editor"; import { trace } from "@server/logging/tracing"; import type Document from "@server/models/Document"; @@ -22,6 +25,7 @@ import User from "@server/models/User"; import FileStorage from "@server/storage/files"; import diff from "@server/utils/diff"; import parseAttachmentIds from "@server/utils/parseAttachmentIds"; +import parseImages from "@server/utils/parseImages"; import Attachment from "../Attachment"; import ProsemirrorHelper from "./ProsemirrorHelper"; @@ -351,9 +355,51 @@ export default class DocumentHelper { : undefined; return text - .replace("{date}", startCase(getCurrentDateAsString(locales))) - .replace("{time}", startCase(getCurrentTimeAsString(locales))) - .replace("{datetime}", startCase(getCurrentDateTimeAsString(locales))); + .replace(/{date}/g, startCase(getCurrentDateAsString(locales))) + .replace(/{time}/g, startCase(getCurrentTimeAsString(locales))) + .replace(/{datetime}/g, startCase(getCurrentDateTimeAsString(locales))); + } + + /** + * Replaces remote and base64 encoded images in the given text with attachment + * urls and uploads the images to the storage provider. + * + * @param text The text to replace the images in + * @param user The user context + * @param ip The IP address of the user + * @param transaction The transaction to use for the database operations + * @returns The text with the images replaced + */ + static async replaceImagesWithAttachments( + text: string, + user: User, + ip?: string, + transaction?: Transaction + ) { + let output = text; + const images = parseImages(text); + + await Promise.all( + images.map(async (image) => { + const attachment = await attachmentCreator({ + name: image.alt ?? "image", + url: image.src, + preset: AttachmentPreset.DocumentAttachment, + user, + ip, + transaction, + }); + + if (attachment) { + output = output.replace( + new RegExp(escapeRegExp(image.src), "g"), + attachment.redirectUrl + ); + } + }) + ); + + return output; } /** diff --git a/server/queues/tasks/ImportTask.ts b/server/queues/tasks/ImportTask.ts index d39db21483..ab4fe75229 100644 --- a/server/queues/tasks/ImportTask.ts +++ b/server/queues/tasks/ImportTask.ts @@ -1,5 +1,6 @@ import truncate from "lodash/truncate"; import { + AttachmentPreset, CollectionPermission, CollectionSort, FileOperationState, @@ -243,6 +244,7 @@ export default abstract class ImportTask extends BaseTask { Logger.debug("task", `ImportTask persisting attachment ${item.id}`); const attachment = await attachmentCreator({ source: "import", + preset: AttachmentPreset.DocumentAttachment, id: item.id, name: item.name, type: item.mimeType, @@ -251,7 +253,9 @@ export default abstract class ImportTask extends BaseTask { ip, transaction, }); - attachments.set(item.id, attachment); + if (attachment) { + attachments.set(item.id, attachment); + } }) ); diff --git a/server/queues/tasks/UploadTeamAvatarTask.ts b/server/queues/tasks/UploadTeamAvatarTask.ts index 33098a350b..657c71e4a2 100644 --- a/server/queues/tasks/UploadTeamAvatarTask.ts +++ b/server/queues/tasks/UploadTeamAvatarTask.ts @@ -20,14 +20,14 @@ export default class UploadTeamAvatarTask extends BaseTask { rejectOnEmpty: true, }); - const avatarUrl = await FileStorage.uploadFromUrl( + const res = await FileStorage.uploadFromUrl( props.avatarUrl, `avatars/${team.id}/${uuidv4()}`, "public-read" ); - if (avatarUrl) { - await team.update({ avatarUrl }); + if (res?.url) { + await team.update({ avatarUrl: res?.url }); } } diff --git a/server/queues/tasks/UploadUserAvatarTask.ts b/server/queues/tasks/UploadUserAvatarTask.ts index f52ae53cd8..35744c2b37 100644 --- a/server/queues/tasks/UploadUserAvatarTask.ts +++ b/server/queues/tasks/UploadUserAvatarTask.ts @@ -20,14 +20,14 @@ export default class UploadUserAvatarTask extends BaseTask { rejectOnEmpty: true, }); - const avatarUrl = await FileStorage.uploadFromUrl( + const res = await FileStorage.uploadFromUrl( props.avatarUrl, `avatars/${user.id}/${uuidv4()}`, "public-read" ); - if (avatarUrl) { - await user.update({ avatarUrl }); + if (res?.url) { + await user.update({ avatarUrl: res?.url }); } } diff --git a/server/storage/files/BaseStorage.ts b/server/storage/files/BaseStorage.ts index af855d625b..5e7c852296 100644 --- a/server/storage/files/BaseStorage.ts +++ b/server/storage/files/BaseStorage.ts @@ -1,5 +1,6 @@ import { Readable } from "stream"; import { PresignedPost } from "aws-sdk/clients/s3"; +import env from "@server/env"; import Logger from "@server/logging/Logger"; import fetch from "@server/utils/fetch"; @@ -78,31 +79,87 @@ export default abstract class BaseStorage { }): Promise; /** - * Upload a file to the storage provider directly from a remote URL. + * Upload a file to the storage provider directly from a remote or base64 encoded URL. * * @param url The URL to upload from * @param key The path to store the file at * @param acl The ACL to use - * @returns The URL of the file + * @returns A promise that resolves when the file is uploaded */ - public async uploadFromUrl(url: string, key: string, acl: string) { + public async uploadFromUrl( + url: string, + key: string, + acl: string + ): Promise< + | { + url: string; + contentType: string; + contentLength: number; + } + | undefined + > { const endpoint = this.getPublicEndpoint(true); if (url.startsWith("/api") || url.startsWith(endpoint)) { return; } + let buffer, contentLength, contentType; + const match = url.match(/data:(.*);base64,(.*)/); + + if (match) { + contentType = match[1]; + buffer = Buffer.from(match[2], "base64"); + contentLength = buffer.byteLength; + } else { + try { + const res = await fetch(url, { + follow: 3, + redirect: "follow", + size: env.AWS_S3_UPLOAD_MAX_SIZE, + timeout: 10000, + }); + + if (!res.ok) { + throw new Error(`Error fetching URL to upload: ${res.status}`); + } + + buffer = await res.buffer(); + + contentType = + res.headers.get("content-type") ?? "application/octet-stream"; + contentLength = parseInt(res.headers.get("content-length") ?? "0", 10); + } catch (err) { + Logger.error("Error fetching URL to upload", err, { + url, + key, + acl, + }); + return; + } + } + + if (contentLength === 0) { + return; + } + try { - const res = await fetch(url); - const buffer = await res.buffer(); - return this.upload({ + const result = await this.upload({ body: buffer, - contentLength: res.headers["content-length"], - contentType: res.headers["content-type"], + contentLength, + contentType, key, acl, }); + + return result + ? { + url: result, + contentType, + contentLength, + } + : undefined; } catch (err) { - Logger.error("Error uploading to S3 from URL", err, { + Logger.error("Error uploading to file storage from URL", err, { url, key, acl, diff --git a/server/utils/dataURItoBuffer.test.ts b/server/utils/dataURItoBuffer.test.ts deleted file mode 100644 index 4c629b9a44..0000000000 --- a/server/utils/dataURItoBuffer.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import dataURItoBuffer from "./dataURItoBuffer"; - -it("should parse value data URI", () => { - const response = dataURItoBuffer( - `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0XkNTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8kkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwaljuZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklHpEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZFo0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzvvhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGdyKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWfwtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+uSi6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUALabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxheD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7s7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3a/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RHiTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZNJ3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSuQmCC` - ); - expect(response.buffer).toBeTruthy(); - expect(response.type).toBe("image/png"); -}); -it("should throw an error with junk input", () => { - let err; - - try { - dataURItoBuffer("what"); - } catch (error) { - err = error; - } - - expect(err).toBeTruthy(); -}); diff --git a/server/utils/dataURItoBuffer.ts b/server/utils/dataURItoBuffer.ts deleted file mode 100644 index 18fdae238f..0000000000 --- a/server/utils/dataURItoBuffer.ts +++ /dev/null @@ -1,16 +0,0 @@ -export default function dataURItoBuffer(dataURI: string) { - const split = dataURI.split(","); - - if (!dataURI.startsWith("data") || split.length <= 1) { - throw new Error("Not a dataURI"); - } - - // separate out the mime component - const type = split[0].split(":")[1].split(";")[0]; - // convert base64 to buffer - const buffer = Buffer.from(split[1], "base64"); - return { - buffer, - type, - }; -} diff --git a/server/utils/parseImages.test.ts b/server/utils/parseImages.test.ts index 90635abdf5..d4040a0a6a 100644 --- a/server/utils/parseImages.test.ts +++ b/server/utils/parseImages.test.ts @@ -10,7 +10,8 @@ it("should return an array of images", () => { ![internal](/attachments/image.png) `); expect(result.length).toBe(1); - expect(result[0]).toBe("/attachments/image.png"); + expect(result[0].alt).toBe("internal"); + expect(result[0].src).toBe("/attachments/image.png"); }); it("should return deeply nested images", () => { @@ -18,10 +19,11 @@ it("should return deeply nested images", () => { - one - two - - three ![internal](/attachments/image.png) + - three ![oh my](/attachments/image.png) `); expect(result.length).toBe(1); - expect(result[0]).toBe("/attachments/image.png"); + expect(result[0].alt).toBe("oh my"); + expect(result[0].src).toBe("/attachments/image.png"); }); it("should not return non document links", () => { diff --git a/server/utils/parseImages.ts b/server/utils/parseImages.ts index 69772c23e2..9f43219371 100644 --- a/server/utils/parseImages.ts +++ b/server/utils/parseImages.ts @@ -1,17 +1,29 @@ import { Node } from "prosemirror-model"; import { parser } from "@server/editor"; -export default function parseImages(text: string): string[] { +type ImageProps = { src: string; alt: string }; + +/** + * Parses a string of markdown and returns a list of images. + * + * @param text The markdown to parse + * @returns A unique list of images + */ +export default function parseImages(text: string): ImageProps[] { const doc = parser.parse(text); - const images: string[] = []; + const images = new Map(); + if (!doc) { - return images; + return []; } doc.descendants((node: Node) => { if (node.type.name === "image") { - if (!images.includes(node.attrs.src)) { - images.push(node.attrs.src); + if (!images.has(node.attrs.src)) { + images.set(node.attrs.src, { + src: node.attrs.src, + alt: node.attrs.alt, + }); } return false; @@ -24,5 +36,5 @@ export default function parseImages(text: string): string[] { return true; }); - return images; + return Array.from(images.values()); } diff --git a/yarn.lock b/yarn.lock index 7024fec116..8adc3f4308 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1879,14 +1879,7 @@ strip-ansi "^6.0.0" v8-to-istanbul "^9.0.1" -"@jest/schemas@^29.6.0": - version "29.6.0" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.0.tgz#0f4cb2c8e3dca80c135507ba5635a4fd755b0040" - integrity sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ== - dependencies: - "@sinclair/typebox" "^0.27.8" - -"@jest/schemas@^29.6.3": +"@jest/schemas@^29.6.0", "@jest/schemas@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== @@ -1943,19 +1936,7 @@ slash "^3.0.0" write-file-atomic "^4.0.2" -"@jest/types@^29.5.0", "@jest/types@^29.6.1": - version "29.6.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.1.tgz#ae79080278acff0a6af5eb49d063385aaa897bf2" - integrity sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw== - dependencies: - "@jest/schemas" "^29.6.0" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jest/types@^29.6.3": +"@jest/types@^29.5.0", "@jest/types@^29.6.1", "@jest/types@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== @@ -8386,26 +8367,7 @@ jest-get-type@^29.4.3: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg== -jest-haste-map@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.6.1.tgz#62655c7a1c1b349a3206441330fb2dbdb4b63803" - integrity sha512-0m7f9PZXxOCk1gRACiVgX85knUKPKLPg4oRCjLoqIm9brTHXaorMA0JpmtmVkQiT8nmXyIVoZd/nnH1cfC33ig== - dependencies: - "@jest/types" "^29.6.1" - "@types/graceful-fs" "^4.1.3" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.9" - jest-regex-util "^29.4.3" - jest-util "^29.6.1" - jest-worker "^29.6.1" - micromatch "^4.0.4" - walker "^1.0.8" - optionalDependencies: - fsevents "^2.3.2" - -jest-haste-map@^29.6.3: +jest-haste-map@^29.6.1, jest-haste-map@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.6.3.tgz#a53ac35a137fd32d932039aab29d02a9dab30689" integrity sha512-GecR5YavfjkhOytEFHAeI6aWWG3f/cOKNB1YJvj/B76xAmeVjy4zJUYobGF030cRmKaO1FBw3V8CZZ6KVh9ZSw== @@ -8471,12 +8433,7 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== -jest-regex-util@^29.4.3: - version "29.4.3" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8" - integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg== - -jest-regex-util@^29.6.3: +jest-regex-util@^29.4.3, jest-regex-util@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== @@ -8586,19 +8543,7 @@ jest-snapshot@^29.6.1: pretty-format "^29.6.1" semver "^7.5.3" -jest-util@^29.5.0, jest-util@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.1.tgz#c9e29a87a6edbf1e39e6dee2b4689b8a146679cb" - integrity sha512-NRFCcjc+/uO3ijUVyNOQJluf8PtGCe/W6cix36+M3cTFgiYqFOOW5MgN4JOOcvbUhcKTYVd1CvHz/LWi8d16Mg== - dependencies: - "@jest/types" "^29.6.1" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - -jest-util@^29.6.3: +jest-util@^29.5.0, jest-util@^29.6.1, jest-util@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.3.tgz#e15c3eac8716440d1ed076f09bc63ace1aebca63" integrity sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA== @@ -8645,17 +8590,7 @@ jest-worker@^26.2.1: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^29.6.1: - version "29.6.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.6.1.tgz#64b015f0e985ef3a8ad049b61fe92b3db74a5319" - integrity sha512-U+Wrbca7S8ZAxAe9L6nb6g8kPdia5hj32Puu5iOqBCMTMWFHXuK6dOV2IFrpedbTV8fjMFLdWNttQTBL6u2MRA== - dependencies: - "@types/node" "*" - jest-util "^29.6.1" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest-worker@^29.6.3: +jest-worker@^29.6.1, jest-worker@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.6.3.tgz#7b1a47bbb6559f3c0882d16595938590e63915d5" integrity sha512-wacANXecZ/GbQakpf2CClrqrlwsYYDSXFd4fIGdL+dXpM2GWoJ+6bhQ7vR3TKi3+gkSfBkjy1/khH/WrYS4Q6g==