progressed file uploads feature (almost done!)

This commit is contained in:
daniel31x13 2024-04-01 02:56:54 -04:00
parent c659711181
commit e67fef1d04
14 changed files with 292 additions and 176 deletions

View File

@ -19,6 +19,7 @@ import { generateLinkHref } from "@/lib/client/generateLinkHref";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import LinkTypeBadge from "./LinkComponents/LinkTypeBadge";
type Props = { type Props = {
link: LinkIncludingShortenedCollectionAndTags; link: LinkIncludingShortenedCollectionAndTags;
@ -53,7 +54,9 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
let shortendURL; let shortendURL;
try { try {
shortendURL = new URL(link.url || "").host.toLowerCase(); if (link.url) {
shortendURL = new URL(link.url).host.toLowerCase();
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
@ -109,7 +112,6 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
editMode && editMode &&
(permissions === true || permissions?.canCreate || permissions?.canDelete); (permissions === true || permissions?.canCreate || permissions?.canDelete);
// window.open ('www.yourdomain.com', '_ blank');
return ( return (
<div <div
ref={ref} ref={ref}
@ -162,18 +164,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
{unescapeString(link.name || link.description) || link.url} {unescapeString(link.name || link.description) || link.url}
</p> </p>
<Link <LinkTypeBadge link={link} />
href={link.url || ""}
target="_blank"
title={link.url || ""}
onClick={(e) => {
e.stopPropagation();
}}
className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100"
>
<i className="bi-link-45deg text-lg mt-[0.10rem] leading-none"></i>
<p className="text-sm truncate">{shortendURL}</p>
</Link>
</div> </div>
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" /> <hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />

View File

@ -122,6 +122,7 @@ export default function LinkActions({
</div> </div>
</li> </li>
) : undefined} ) : undefined}
{link.type === "url" && (
<li> <li>
<div <div
role="button" role="button"
@ -134,6 +135,7 @@ export default function LinkActions({
Preserved Formats Preserved Formats
</div> </div>
</li> </li>
)}
{permissions === true || permissions?.canDelete ? ( {permissions === true || permissions?.canDelete ? (
<li> <li>
<div <div

View File

@ -6,9 +6,11 @@ import React from "react";
export default function LinkIcon({ export default function LinkIcon({
link, link,
width, width,
className,
}: { }: {
link: LinkIncludingShortenedCollectionAndTags; link: LinkIncludingShortenedCollectionAndTags;
width?: string; width?: string;
className?: string;
}) { }) {
const url = const url =
isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined; isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined;
@ -16,13 +18,16 @@ export default function LinkIcon({
const iconClasses: string = const iconClasses: string =
"bg-white shadow rounded-md border-[2px] flex item-center justify-center border-white select-none z-10" + "bg-white shadow rounded-md border-[2px] flex item-center justify-center border-white select-none z-10" +
" " + " " +
(width || "w-12"); (width || "w-12") +
" " +
(className || "");
const [showFavicon, setShowFavicon] = React.useState<boolean>(true); const [showFavicon, setShowFavicon] = React.useState<boolean>(true);
return ( return (
<> <>
{link.url && url && showFavicon ? ( {link.type === "url" && url ? (
showFavicon ? (
<Image <Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`} src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
width={64} width={64}
@ -34,15 +39,34 @@ export default function LinkIcon({
setShowFavicon(false); setShowFavicon(false);
}} }}
/> />
) : showFavicon === false ? ( ) : (
<div className={iconClasses}> <LinkPlaceholderIcon iconClasses={iconClasses} icon="bi-link-45deg" />
<i className="bi-link-45deg text-4xl text-black"></i> )
</div>
) : link.type === "pdf" ? ( ) : link.type === "pdf" ? (
<i className={`bi-file-earmark-pdf ${iconClasses}`}></i> <LinkPlaceholderIcon
iconClasses={iconClasses}
icon="bi-file-earmark-pdf"
/>
) : link.type === "image" ? ( ) : link.type === "image" ? (
<i className={`bi-file-earmark-image ${iconClasses}`}></i> <LinkPlaceholderIcon
iconClasses={iconClasses}
icon="bi-file-earmark-image"
/>
) : undefined} ) : undefined}
</> </>
); );
} }
const LinkPlaceholderIcon = ({
iconClasses,
icon,
}: {
iconClasses: string;
icon: string;
}) => {
return (
<div className={`text-4xl text-black aspect-square ${iconClasses}`}>
<i className={`${icon} m-auto`}></i>
</div>
);
};

View File

@ -0,0 +1,38 @@
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import Link from "next/link";
import React from "react";
export default function LinkTypeBadge({
link,
}: {
link: LinkIncludingShortenedCollectionAndTags;
}) {
let shortendURL;
if (link.type === "url" && link.url) {
try {
shortendURL = new URL(link.url).host.toLowerCase();
} catch (error) {
console.log(error);
}
}
return link.url && shortendURL ? (
<Link
href={link.url || ""}
target="_blank"
title={link.url || ""}
onClick={(e) => {
e.stopPropagation();
}}
className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100"
>
<i className="bi-link-45deg text-lg mt-[0.1rem] leading-none"></i>
<p className="text-sm truncate">{shortendURL}</p>
</Link>
) : (
<div className="badge badge-primary badge-sm my-1 select-none">
{link.type}
</div>
);
}

View File

@ -16,6 +16,7 @@ import { generateLinkHref } from "@/lib/client/generateLinkHref";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import LinkTypeBadge from "./LinkComponents/LinkTypeBadge";
type Props = { type Props = {
link: LinkIncludingShortenedCollectionAndTags; link: LinkIncludingShortenedCollectionAndTags;
@ -56,14 +57,6 @@ export default function LinkCardCompact({
} }
}; };
let shortendURL;
try {
shortendURL = new URL(link.url || "").host.toLowerCase();
} catch (error) {
console.log(error);
}
const [collection, setCollection] = const [collection, setCollection] =
useState<CollectionIncludingMembersAndLinkCount>( useState<CollectionIncludingMembersAndLinkCount>(
collections.find( collections.find(
@ -130,7 +123,11 @@ export default function LinkCardCompact({
} }
> >
<div className="shrink-0"> <div className="shrink-0">
<LinkIcon link={link} width="sm:w-12 w-8 mt-1 sm:mt-0" /> <LinkIcon
link={link}
width="sm:w-12 w-8"
className="mt-1 sm:mt-0"
/>
</div> </div>
<div className="w-[calc(100%-56px)] ml-2"> <div className="w-[calc(100%-56px)] ml-2">
@ -143,24 +140,7 @@ export default function LinkCardCompact({
{collection ? ( {collection ? (
<LinkCollection link={link} collection={collection} /> <LinkCollection link={link} collection={collection} />
) : undefined} ) : undefined}
{link.url ? ( <LinkTypeBadge link={link} />
<Link
href={link.url || ""}
target="_blank"
title={link.url || ""}
onClick={(e) => {
e.stopPropagation();
}}
className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100"
>
<i className="bi-link-45deg text-lg mt-[0.1rem] leading-none"></i>
<p className="text-sm truncate">{shortendURL}</p>
</Link>
) : (
<div className="badge badge-primary badge-sm my-1 select-none">
{link.type}
</div>
)}
<LinkDate link={link} /> <LinkDate link={link} />
</div> </div>
</div> </div>

View File

@ -43,7 +43,7 @@ export default function UploadFileModal({ onClose }: Props) {
const [file, setFile] = useState<File>(); const [file, setFile] = useState<File>();
const { addLink } = useLinkStore(); const { uploadFile } = useLinkStore();
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
const [optionsExpanded, setOptionsExpanded] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false);
@ -100,48 +100,15 @@ export default function UploadFileModal({ onClose }: Props) {
const submit = async () => { const submit = async () => {
if (!submitLoader && file) { if (!submitLoader && file) {
let fileType: ArchivedFormat | null = null;
let linkType: "url" | "image" | "pdf" | null = null;
if (file?.type === "image/jpg" || file.type === "image/jpeg") {
fileType = ArchivedFormat.jpeg;
linkType = "image";
} else if (file.type === "image/png") {
fileType = ArchivedFormat.png;
linkType = "image";
} else if (file.type === "application/pdf") {
fileType = ArchivedFormat.pdf;
linkType = "pdf";
}
if (fileType !== null && linkType !== null) {
setSubmitLoader(true); setSubmitLoader(true);
let response;
const load = toast.loading("Creating..."); const load = toast.loading("Creating...");
response = await addLink({ const response = await uploadFile(link, file);
...link,
type: linkType,
name: link.name ? link.name : file.name.replace(/\.[^/.]+$/, ""),
});
toast.dismiss(load); toast.dismiss(load);
if (response.ok) { if (response.ok) {
const formBody = new FormData();
file && formBody.append("file", file);
await fetch(
`/api/v1/archives/${
(response.data as LinkIncludingShortenedCollectionAndTags).id
}?format=${fileType}`,
{
body: formBody,
method: "POST",
}
);
toast.success(`Created!`); toast.success(`Created!`);
onClose(); onClose();
} else toast.error(response.data as string); } else toast.error(response.data as string);
@ -150,7 +117,6 @@ export default function UploadFileModal({ onClose }: Props) {
return response; return response;
} }
}
}; };
return ( return (
@ -238,7 +204,7 @@ export default function UploadFileModal({ onClose }: Props) {
className="btn btn-accent dark:border-violet-400 text-white" className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit} onClick={submit}
> >
Create Link Upload File
</button> </button>
</div> </div>
</Modal> </Modal>

View File

@ -10,6 +10,7 @@ import validateUrlSize from "./validateUrlSize";
import removeFile from "./storage/removeFile"; import removeFile from "./storage/removeFile";
import Jimp from "jimp"; import Jimp from "jimp";
import createFolder from "./storage/createFolder"; import createFolder from "./storage/createFolder";
import generatePreview from "./generatePreview";
type LinksAndCollectionAndOwner = Link & { type LinksAndCollectionAndOwner = Link & {
collection: Collection & { collection: Collection & {
@ -175,35 +176,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
// Check if imageResponse is not null // Check if imageResponse is not null
if (imageResponse && !link.preview?.startsWith("archive")) { if (imageResponse && !link.preview?.startsWith("archive")) {
const buffer = await imageResponse.body(); const buffer = await imageResponse.body();
await generatePreview(buffer, link.collectionId, link.id);
// Check if buffer is not null
if (buffer) {
// Load the image using Jimp
Jimp.read(buffer, async (err, image) => {
if (image && !err) {
image?.resize(1280, Jimp.AUTO).quality(20);
const processedBuffer = await image?.getBufferAsync(
Jimp.MIME_JPEG
);
createFile({
data: processedBuffer,
filePath: `archives/preview/${link.collectionId}/${link.id}.jpeg`,
}).then(() => {
return prisma.link.update({
where: { id: link.id },
data: {
preview: `archives/preview/${link.collectionId}/${link.id}.jpeg`,
},
});
});
}
}).catch((err) => {
console.error("Error processing the image:", err);
});
} else {
console.log("No image data found.");
}
} }
await page.goBack(); await page.goBack();

View File

@ -174,7 +174,7 @@ export default async function postLink(
const newLink = await prisma.link.create({ const newLink = await prisma.link.create({
data: { data: {
url: link.url?.trim(), url: link.url?.trim() || null,
name: link.name, name: link.name,
description, description,
type: linkType, type: linkType,

View File

@ -0,0 +1,35 @@
import Jimp from "jimp";
import { prisma } from "./db";
import createFile from "./storage/createFile";
const generatePreview = async (
buffer: Buffer,
collectionId: number,
linkId: number
) => {
if (buffer && collectionId && linkId) {
// Load the image using Jimp
await Jimp.read(buffer, async (err, image) => {
if (image && !err) {
image?.resize(1280, Jimp.AUTO).quality(20);
const processedBuffer = await image?.getBufferAsync(Jimp.MIME_JPEG);
createFile({
data: processedBuffer,
filePath: `archives/preview/${collectionId}/${linkId}.jpeg`,
}).then(() => {
return prisma.link.update({
where: { id: linkId },
data: {
preview: `archives/preview/${collectionId}/${linkId}.jpeg`,
},
});
});
}
}).catch((err) => {
console.error("Error processing the image:", err);
});
}
};
export default generatePreview;

View File

@ -16,24 +16,30 @@ export const generateLinkHref = (
): string => { ): string => {
// Return the links href based on the account's preference // Return the links href based on the account's preference
// If the user's preference is not available, return the original link // If the user's preference is not available, return the original link
switch (account.linksRouteTo) { if (account.linksRouteTo === LinksRouteTo.ORIGINAL && link.type === "url") {
case LinksRouteTo.ORIGINAL:
return link.url || ""; return link.url || "";
case LinksRouteTo.PDF: } else if (account.linksRouteTo === LinksRouteTo.PDF || link.type === "pdf") {
if (!pdfAvailable(link)) return link.url || ""; if (!pdfAvailable(link)) return link.url || "";
return `/preserved/${link?.id}?format=${ArchivedFormat.pdf}`; return `/preserved/${link?.id}?format=${ArchivedFormat.pdf}`;
case LinksRouteTo.READABLE: } else if (
account.linksRouteTo === LinksRouteTo.READABLE &&
link.type === "url"
) {
if (!readabilityAvailable(link)) return link.url || ""; if (!readabilityAvailable(link)) return link.url || "";
return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`; return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`;
case LinksRouteTo.SCREENSHOT: } else if (
account.linksRouteTo === LinksRouteTo.SCREENSHOT ||
link.type === "image"
) {
console.log(link);
if (!screenshotAvailable(link)) return link.url || ""; if (!screenshotAvailable(link)) return link.url || "";
return `/preserved/${link?.id}?format=${ return `/preserved/${link?.id}?format=${
link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg
}`; }`;
default: } else {
return link.url || ""; return link.url || "";
} }
}; };

View File

@ -79,7 +79,7 @@
"nodemon": "^3.0.2", "nodemon": "^3.0.2",
"postcss": "^8.4.26", "postcss": "^8.4.26",
"prettier": "3.1.1", "prettier": "3.1.1",
"prisma": "^5.1.0", "prisma": "^4.16.2",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "4.9.4" "typescript": "4.9.4"

View File

@ -9,6 +9,8 @@ import formidable from "formidable";
import createFile from "@/lib/api/storage/createFile"; import createFile from "@/lib/api/storage/createFile";
import fs from "fs"; import fs from "fs";
import verifyToken from "@/lib/api/verifyToken"; import verifyToken from "@/lib/api/verifyToken";
import Jimp from "jimp";
import generatePreview from "@/lib/api/generatePreview";
export const config = { export const config = {
api: { api: {
@ -124,6 +126,14 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
where: { id: linkId }, where: { id: linkId },
}); });
if (linkStillExists && files.file[0].mimetype?.includes("image")) {
generatePreview(
fileBuffer,
collectionPermissions?.id as number,
linkId
);
}
if (linkStillExists) { if (linkStillExists) {
await createFile({ await createFile({
filePath: `archives/${collectionPermissions?.id}/${ filePath: `archives/${collectionPermissions?.id}/${
@ -135,7 +145,15 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
await prisma.link.update({ await prisma.link.update({
where: { id: linkId }, where: { id: linkId },
data: { data: {
image: `archives/${collectionPermissions?.id}/${linkId + suffix}`, preview: files.file[0].mimetype?.includes("pdf")
? "unavailable"
: undefined,
image: files.file[0].mimetype?.includes("image")
? `archives/${collectionPermissions?.id}/${linkId + suffix}`
: null,
pdf: files.file[0].mimetype?.includes("pdf")
? `archives/${collectionPermissions?.id}/${linkId + suffix}`
: null,
lastPreserved: new Date().toISOString(), lastPreserved: new Date().toISOString(),
}, },
}); });

View File

@ -1,5 +1,8 @@
import { create } from "zustand"; import { create } from "zustand";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import useTagStore from "./tags"; import useTagStore from "./tags";
import useCollectionStore from "./collections"; import useCollectionStore from "./collections";
@ -19,6 +22,10 @@ type LinkStore = {
addLink: ( addLink: (
body: LinkIncludingShortenedCollectionAndTags body: LinkIncludingShortenedCollectionAndTags
) => Promise<ResponseObject>; ) => Promise<ResponseObject>;
uploadFile: (
link: LinkIncludingShortenedCollectionAndTags,
file: File
) => Promise<ResponseObject>;
getLink: (linkId: number, publicRoute?: boolean) => Promise<ResponseObject>; getLink: (linkId: number, publicRoute?: boolean) => Promise<ResponseObject>;
updateLink: ( updateLink: (
link: LinkIncludingShortenedCollectionAndTags link: LinkIncludingShortenedCollectionAndTags
@ -79,6 +86,82 @@ const useLinkStore = create<LinkStore>()((set) => ({
return { ok: response.ok, data: data.response }; return { ok: response.ok, data: data.response };
}, },
uploadFile: async (link, file) => {
let fileType: ArchivedFormat | null = null;
let linkType: "url" | "image" | "pdf" | null = null;
if (file?.type === "image/jpg" || file.type === "image/jpeg") {
fileType = ArchivedFormat.jpeg;
linkType = "image";
} else if (file.type === "image/png") {
fileType = ArchivedFormat.png;
linkType = "image";
} else if (file.type === "application/pdf") {
fileType = ArchivedFormat.pdf;
linkType = "pdf";
} else {
return { ok: false, data: "Invalid file type." };
}
const response = await fetch("/api/v1/links", {
body: JSON.stringify({
...link,
type: linkType,
name: link.name ? link.name : file.name,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
const data = await response.json();
const createdLink: LinkIncludingShortenedCollectionAndTags = data.response;
console.log(data);
if (response.ok) {
const formBody = new FormData();
file && formBody.append("file", file);
await fetch(
`/api/v1/archives/${(data as any).response.id}?format=${fileType}`,
{
body: formBody,
method: "POST",
}
);
// get file extension
const extension = file.name.split(".").pop() || "";
set((state) => ({
links: [
{
...createdLink,
image:
linkType === "image"
? `archives/${createdLink.collectionId}/${
createdLink.id + extension
}`
: null,
pdf:
linkType === "pdf"
? `archives/${createdLink.collectionId}/${
createdLink.id + ".pdf"
}`
: null,
},
...state.links,
],
}));
useTagStore.getState().setTags();
useCollectionStore.getState().setCollections();
}
return { ok: response.ok, data: data.response };
},
getLink: async (linkId, publicRoute) => { getLink: async (linkId, publicRoute) => {
const path = publicRoute const path = publicRoute
? `/api/v1/public/links/${linkId}` ? `/api/v1/public/links/${linkId}`

View File

@ -1301,10 +1301,10 @@
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz#d3b5dcf95b6d220e258cbf6ae19b06d30a7e9f14" resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz#d3b5dcf95b6d220e258cbf6ae19b06d30a7e9f14"
integrity sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg== integrity sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==
"@prisma/engines@5.1.0": "@prisma/engines@4.16.2":
version "5.1.0" version "4.16.2"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.1.0.tgz#4ccf7f344eaeee08ca1e4a1bb2dc14e36ff1d5ec" resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.16.2.tgz#5ec8dd672c2173d597e469194916ad4826ce2e5f"
integrity sha512-HqaFsnPmZOdMWkPq6tT2eTVTQyaAXEDdKszcZ4yc7DGMBIYRP6j/zAJTtZUG9SsMV8FaucdL5vRyxY/p5Ni28g== integrity sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==
"@radix-ui/primitive@1.0.1": "@radix-ui/primitive@1.0.1":
version "1.0.1" version "1.0.1"
@ -5038,12 +5038,12 @@ pretty-format@^3.8.0:
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew== integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==
prisma@^5.1.0: prisma@^4.16.2:
version "5.1.0" version "4.16.2"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.1.0.tgz#29e316b54844f5694a83017a9781a6d6f7cb99ea" resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.16.2.tgz#469e0a0991c6ae5bcde289401726bb012253339e"
integrity sha512-wkXvh+6wxk03G8qwpZMOed4Y3j+EQ+bMTlvbDZHeal6k1E8QuGKzRO7DRXlE1NV0WNgOAas8kwZqcLETQ2+BiQ== integrity sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==
dependencies: dependencies:
"@prisma/engines" "5.1.0" "@prisma/engines" "4.16.2"
process@^0.11.10: process@^0.11.10:
version "0.11.10" version "0.11.10"