feat: add project schema

This commit is contained in:
Khairul Hidayat 2024-02-22 20:59:20 +00:00
parent b232410d83
commit d579c1b7c5
27 changed files with 338 additions and 147 deletions

View File

@ -1,6 +1,8 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { BASE_URL } from "./consts";
import type { ProjectSchema } from "~/server/db/schema/project";
import type { FileSchema } from "~/server/db/schema/file";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@ -13,3 +15,13 @@ export function getFileExt(filename: string) {
export function getUrl(...path: string[]) {
return BASE_URL + ("/" + path.join("/")).replace(/\/\/+/g, "/");
}
export function getPreviewUrl(
project: Pick<ProjectSchema, "slug">,
file: string | Pick<FileSchema, "path">
) {
return getUrl(
`api/preview/${project.slug}`,
typeof file === "string" ? file : file.path
);
}

View File

@ -6,7 +6,8 @@
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx watch --ignore *.mjs server/index.ts",
"dev": "tsx server/index.ts",
"dev:watch": "tsx watch --ignore *.mjs server/index.ts",
"start": "NODE_ENV=production tsx server/index.ts",
"build": "vite build",
"generate": "drizzle-kit generate:sqlite",
@ -42,6 +43,7 @@
"@codemirror/lang-json": "^6.0.1",
"@emmetio/codemirror6-plugin": "^0.3.0",
"@hookform/resolvers": "^3.3.4",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-slot": "^1.0.2",

View File

@ -1,17 +0,0 @@
import { useData } from "~/renderer/hooks";
import { Data } from "./+data";
import Link from "~/renderer/link";
const ViewPostPage = () => {
const { post } = useData<Data>();
return (
<div>
<Link href="/">Go Back</Link>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
};
export default ViewPostPage;

View File

@ -1,12 +0,0 @@
import { PageContext } from "vike/types";
export const data = async (ctx: PageContext) => {
const id = ctx.routeParams?.id;
const post = await fetch(
"https://jsonplaceholder.typicode.com/posts/" + id
).then((response) => response.json());
return { post, title: post?.title };
};
export type Data = Awaited<ReturnType<typeof data>>;

View File

@ -3,19 +3,15 @@ import { useData } from "~/renderer/hooks";
import Link from "~/renderer/link";
const HomePage = () => {
const { posts } = useData<Data>();
if (!posts?.length) {
return <p>No posts.</p>;
}
const { projects } = useData<Data>();
return (
<div>
<h1>Posts</h1>
{posts.map((post: any) => (
<Link key={post.id} href={`/${post.id}`}>
{post.title}
{projects.map((project: any) => (
<Link key={project.id} href={`/${project.slug}`}>
{project.title}
</Link>
))}
</div>

View File

@ -1,9 +1,12 @@
export const data = async () => {
const posts = await fetch(
"https://jsonplaceholder.typicode.com/posts?_limit=20"
).then((response) => response.json());
import { PageContext } from "vike/types";
import trpcServer from "~/server/api/trpc/trpc";
return { posts };
export const data = async (ctx: PageContext) => {
const trpc = await trpcServer(ctx);
const projects = await trpc.project.getAll();
return { projects };
};
export type Data = Awaited<ReturnType<typeof data>>;

View File

@ -6,22 +6,21 @@ import {
import WebPreview from "./components/web-preview";
import Editor from "./components/editor";
import ProjectContext from "./context/project";
import { cn } from "~/lib/utils";
import { useParams, useSearchParams } from "~/renderer/hooks";
import { BASE_URL } from "~/lib/consts";
import { cn, getPreviewUrl } from "~/lib/utils";
import { useData, useSearchParams } from "~/renderer/hooks";
import { withClientOnly } from "~/renderer/client-only";
import Spinner from "~/components/ui/spinner";
import { Data } from "./+data";
const ViewProjectPage = () => {
const { project } = useData<Data>();
const searchParams = useSearchParams();
const params = useParams();
const isCompact =
searchParams.get("compact") === "1" || searchParams.get("embed") === "1";
const slug = params["slug"];
const previewUrl = BASE_URL + `/api/preview/${slug}/index.html`;
const previewUrl = getPreviewUrl(project, "index.html");
return (
<ProjectContext.Provider value={{ slug, isCompact }}>
<ProjectContext.Provider value={{ project, isCompact }}>
<ResizablePanelGroup
autoSaveId="main-panel"
direction={{ sm: "vertical", md: "horizontal" }}

View File

@ -1,12 +1,25 @@
import { PageContext } from "vike/types";
import { render } from "vike/abort";
import trpcServer from "~/server/api/trpc/trpc";
export const data = async (ctx: PageContext) => {
const trpc = await trpcServer(ctx);
const pinnedFiles = await trpc.file.getAll({ isPinned: true });
const files = await trpc.file.getAll();
return { files, pinnedFiles };
const project = await trpc.project.getById(ctx.routeParams?.slug!);
if (!project) {
throw render(404, "Project not found!");
}
const files = await trpc.file.getAll({ projectId: project.id });
const pinnedFiles = files.filter((i) => i.isPinned);
return {
title: project.title,
description: `Check ${project.title} on CodeShare!`,
project,
files,
pinnedFiles,
};
};
export type Data = Awaited<ReturnType<typeof data>>;

View File

@ -0,0 +1 @@
export default "/@slug";

View File

@ -23,9 +23,9 @@ import { Data } from "../+data";
import { useBreakpoint } from "~/hooks/useBreakpoint";
const Editor = () => {
const { pinnedFiles } = useData<Data>();
const { project, pinnedFiles } = useData<Data>();
const trpcUtils = trpc.useUtils();
const project = useProjectContext();
const projectCtx = useProjectContext();
const sidebarPanel = useRef<ImperativePanelHandle>(null);
const [breakpoint] = useBreakpoint();
@ -36,7 +36,7 @@ const Editor = () => {
);
const openedFilesData = trpc.file.getAll.useQuery(
{ id: curOpenFiles },
{ projectId: project.id, id: curOpenFiles },
{ enabled: curOpenFiles.length > 0, initialData: pinnedFiles }
);
const [openedFiles, setOpenedFiles] = useState<any[]>(pinnedFiles);
@ -156,7 +156,7 @@ const Editor = () => {
}) satisfies Tab[];
}, [curOpenFiles, openedFiles, refreshPreview]);
const PanelComponent = !project.isCompact ? Panel : "div";
const PanelComponent = !projectCtx.isCompact ? Panel : "div";
return (
<EditorContext.Provider

View File

@ -1,6 +1,4 @@
"use client";
import React, { Fragment, useMemo, useState } from "react";
import { Fragment, useMemo, useState } from "react";
import { UseDiscloseReturn, useDisclose } from "~/hooks/useDisclose";
import {
FiChevronRight,
@ -21,27 +19,28 @@ import {
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "~/components/ui/dropdown-menu";
import { cn, getUrl } from "~/lib/utils";
import { cn, getPreviewUrl } from "~/lib/utils";
import FileIcon from "~/components/ui/file-icon";
import copy from "copy-to-clipboard";
import { useData, useParams } from "~/renderer/hooks";
import { useData } from "~/renderer/hooks";
import Spinner from "~/components/ui/spinner";
import { Data } from "../+data";
const FileListing = () => {
const pageData = useData<Data>();
const { project, files: initialFiles } = useData<Data>();
const { onOpenFile, onFileChanged } = useEditorContext();
const createFileDlg = useDisclose<CreateFileSchema>();
const files = trpc.file.getAll.useQuery(undefined, {
initialData: pageData.files,
});
const files = trpc.file.getAll.useQuery(
{ projectId: project.id },
{ initialData: initialFiles }
);
const fileList = useMemo(() => groupFiles(files.data, null), [files.data]);
return (
<Fragment>
<div className="h-10 flex items-center pl-4 pr-1">
<p className="text-xs uppercase truncate flex-1">My Project</p>
<p className="text-xs uppercase truncate flex-1">{project.title}</p>
<ActionButton
icon={FiFilePlus}
onClick={() => createFileDlg.onOpen()}
@ -104,7 +103,7 @@ type FileItemProps = {
};
const FileItem = ({ file, createFileDlg }: FileItemProps) => {
const { slug } = useParams();
const { project } = useData<Data>();
const { onOpenFile, onDeleteFile } = useEditorContext();
const [isCollapsed, setCollapsed] = useState(false);
const trpcUtils = trpc.useUtils();
@ -186,16 +185,13 @@ const FileItem = ({ file, createFileDlg }: FileItemProps) => {
Copy Path
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => copy(getUrl(`api/preview/${slug}`, file.path))}
onClick={() => copy(getPreviewUrl(project, file))}
>
Copy URL
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
window.open(
getUrl(`api/preview/${slug}`, file.path),
"_blank"
)
window.open(getPreviewUrl(project, file), "_blank")
}
>
Open in new tab

View File

@ -1,5 +1,3 @@
"use client";
import { getFileExt } from "~/lib/utils";
import CodeEditor from "../../../../components/ui/code-editor";
import trpc from "~/lib/trpc";

View File

@ -1,4 +1,3 @@
import React from "react";
import FileListing from "./file-listing";
import { FaUserCircle } from "react-icons/fa";
import { Button } from "~/components/ui/button";
@ -7,6 +6,7 @@ const Sidebar = () => {
return (
<aside className="flex flex-col items-stretch h-full">
<FileListing />
<div className="h-12 bg-[#1a1b26] pl-12">
<Button
variant="ghost"

View File

@ -1,7 +1,8 @@
import { createContext, useContext } from "react";
import type { ProjectSchema } from "~/server/db/schema/project";
type TProjectContext = {
slug: string;
project: ProjectSchema;
isCompact?: boolean;
};

14
pnpm-lock.yaml generated
View File

@ -23,6 +23,9 @@ dependencies:
'@hookform/resolvers':
specifier: ^3.3.4
version: 3.3.4(react-hook-form@7.50.1)
'@paralleldrive/cuid2':
specifier: ^2.2.2
version: 2.2.2
'@radix-ui/react-dialog':
specifier: ^1.0.5
version: 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.57)(react-dom@18.2.0)(react@18.2.0)
@ -1324,6 +1327,11 @@ packages:
os-filter-obj: 2.0.0
dev: true
/@noble/hashes@1.3.3:
resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==}
engines: {node: '>= 16'}
dev: false
/@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -1342,6 +1350,12 @@ packages:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
/@paralleldrive/cuid2@2.2.2:
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
dependencies:
'@noble/hashes': 1.3.3
dev: false
/@pkgjs/parseargs@0.11.0:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}

View File

@ -2,10 +2,12 @@ import type { PageContext } from "vike/types";
export function getPageMetadata(pageContext: PageContext) {
let title = pageContext.data?.title || pageContext.config.title;
title = title ? `${title} - Vike` : "Welcome to Vike";
title = title ? `${title} - CodeShare` : "Welcome to CodeShare";
const description =
pageContext.data?.description || pageContext.config.description || "";
pageContext.data?.description ||
pageContext.config.description ||
"Share your frontend result with everyone";
return { title, description };
}

View File

@ -7,15 +7,26 @@ import { serveHtml } from "./serve-html";
import { serveJs } from "./serve-js";
import { getMimeType } from "~/server/lib/mime";
import { postcss } from "./postcss";
import { project } from "~/server/db/schema/project";
const get = async (req: Request, res: Response) => {
const { slug, ...pathParams } = req.params as any;
const path = pathParams[0];
const fileData = await db.query.file.findFirst({
where: and(eq(file.path, path), isNull(file.deletedAt)),
const projectData = await db.query.project.findFirst({
where: and(eq(project.slug, slug), isNull(project.deletedAt)),
});
if (!projectData) {
return res.status(404).send("Project not found!");
}
const fileData = await db.query.file.findFirst({
where: and(
eq(file.projectId, projectData.id),
eq(file.path, path),
isNull(file.deletedAt)
),
});
if (!fileData) {
return res.status(404).send("File not found!");
}

View File

@ -1,12 +1,12 @@
import db from "~/server/db";
import { FileSchema, file } from "~/server/db/schema/file";
import { and, eq, isNull } from "drizzle-orm";
const preventHtmlDirectAccess = `<script>if (window === window.parent) {window.location.href = '/';}</script>`;
import { IS_DEV } from "~/server/lib/consts";
export const serveHtml = async (fileData: FileSchema) => {
const layout = await db.query.file.findFirst({
where: and(
eq(file.projectId, fileData.projectId),
eq(file.filename, "_layout.html"),
fileData.parentId
? eq(file.parentId, fileData.parentId)
@ -22,10 +22,14 @@ export const serveHtml = async (fileData: FileSchema) => {
const bodyOpeningTagIdx = content.indexOf("<body");
const firstScriptTagIdx = content.indexOf("<script", bodyOpeningTagIdx);
const injectScripts = [
'<script src="/js/hook-console.js"></script>',
preventHtmlDirectAccess,
];
const injectScripts = ['<script src="/js/hook-console.js"></script>'];
if (!IS_DEV) {
// prevent direct access
injectScripts.push(
`<script>if (window === window.parent) {window.location.href = '/';}</script>`
);
}
const importMaps = [
{ name: "react", url: "https://esm.sh/react@18.2.0" },

View File

@ -1,2 +1,3 @@
export { user } from "./user";
export { file } from "./file";
export { project, projectRelations } from "./project";
export { file, fileRelations } from "./file";

View File

@ -1,23 +1,24 @@
import { sql } from "drizzle-orm";
import { relations, sql } from "drizzle-orm";
import {
integer,
sqliteTable,
text,
foreignKey,
index,
} from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";
import { user } from "./user";
import { project } from "./project";
export const file = sqliteTable(
"files",
{
id: integer("id").primaryKey({ autoIncrement: true }),
parentId: integer("parent_id"),
userId: integer("user_id")
projectId: integer("project_id")
.notNull()
.references(() => user.id),
path: text("path").notNull().unique(),
.references(() => project.id),
parentId: integer("parent_id"),
path: text("path").notNull(),
filename: text("filename").notNull(),
isDirectory: integer("is_directory", { mode: "boolean" })
.notNull()
@ -38,9 +39,18 @@ export const file = sqliteTable(
foreignColumns: [table.id],
name: "parent_id_fk",
}),
pathIdx: index("file_path_idx").on(table.path),
filenameIdx: index("file_name_idx").on(table.filename),
})
);
export const fileRelations = relations(file, ({ one }) => ({
project: one(project, {
fields: [file.projectId],
references: [project.id],
}),
}));
export const insertFileSchema = createInsertSchema(file);
export const selectFileSchema = createSelectSchema(file);

View File

@ -0,0 +1,32 @@
import { relations, sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";
import { user } from "./user";
import { file } from "./file";
export const project = sqliteTable("projects", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: integer("user_id")
.notNull()
.references(() => user.id),
slug: text("slug").notNull().unique(),
title: text("title").notNull(),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
deletedAt: text("deleted_at"),
});
export const projectRelations = relations(project, ({ one, many }) => ({
files: many(file),
user: one(user, {
fields: [project.userId],
references: [user.id],
}),
}));
export const insertProjectSchema = createInsertSchema(project);
export const selectProjectSchema = createSelectSchema(project);
export type ProjectSchema = z.infer<typeof selectProjectSchema>;

View File

@ -5,6 +5,7 @@ import { z } from "zod";
export const user = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull().unique(),
password: text("password").notNull(),
createdAt: text("created_at")

View File

@ -1,68 +1,83 @@
import db from ".";
import { hashPassword } from "../lib/crypto";
import { uid } from "../lib/utils";
import { file } from "./schema/file";
import { project } from "./schema/project";
import { user } from "./schema/user";
const main = async () => {
const [adminUser] = await db
.insert(user)
.values({
name: "Admin",
email: "admin@mail.com",
password: await hashPassword("123456"),
})
.returning();
// await db
// .insert(file)
// .values([
// {
// userId: adminUser.id,
// path: "index.html",
// filename: "index.html",
// content: '<p class="text-lg text-red-500">Hello world!</p>',
// },
// {
// userId: adminUser.id,
// path: "styles.css",
// filename: "styles.css",
// content: "body { padding: 16px; }",
// },
// {
// userId: adminUser.id,
// path: "script.js",
// filename: "script.js",
// content: "console.log('hello world!');",
// },
// {
// userId: adminUser.id,
// path: "_layout.html",
// filename: "_layout.html",
// content: `<!DOCTYPE html>
// <html lang="en">
// <head>
// <meta charset="UTF-8">
// <meta name="viewport" content="width=device-width, initial-scale=1.0">
// <title>Document</title>
const [vanillaProject] = await db
.insert(project)
.values({ userId: adminUser.id, slug: uid(), title: "Vanilla Project" })
.returning();
// <link rel="stylesheet" href="styles.css">
// vanilla html css js template
await db
.insert(file)
.values([
{
projectId: vanillaProject.id,
path: "index.html",
filename: "index.html",
content: "<p>Hello world!</p>",
isPinned: true,
},
{
projectId: vanillaProject.id,
path: "styles.css",
filename: "styles.css",
content: "body { padding: 16px; }",
isPinned: true,
},
{
projectId: vanillaProject.id,
path: "scripts.js",
filename: "scripts.js",
content: "console.log('hello world!');",
isPinned: true,
},
{
projectId: vanillaProject.id,
path: "_layout.html",
filename: "_layout.html",
content: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
// <script src="https://cdn.tailwindcss.com"></script>
// </head>
// <body>
// {CONTENT}
// <script src="script.js" type="module" defer></script>
// </body>
// </html>`,
// },
// ])
// .execute();
<link rel="stylesheet" href="styles.css">
</head>
<body>
{CONTENT}
<script src="scripts.js"></script>
</body>
</html>`,
},
])
.execute();
const [reactProject] = await db
.insert(project)
.values({ userId: adminUser.id, slug: uid(), title: "React Project" })
.returning();
// react template
await db
.insert(file)
.values([
{
userId: adminUser.id,
projectId: reactProject.id,
path: "index.html",
filename: "index.html",
content: `<!doctype html>
@ -84,7 +99,7 @@ const main = async () => {
`,
},
{
userId: adminUser.id,
projectId: reactProject.id,
path: "globals.css",
filename: "globals.css",
content: `@tailwind base;
@ -97,7 +112,7 @@ body {
`,
},
{
userId: adminUser.id,
projectId: reactProject.id,
path: "index.jsx",
filename: "index.jsx",
content: `import React from "react";
@ -109,7 +124,7 @@ root.render(<App />);
`,
},
{
userId: adminUser.id,
projectId: reactProject.id,
path: "App.jsx",
filename: "App.jsx",
isPinned: true,

View File

@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import cuid2 from "@paralleldrive/cuid2";
export const fileExists = (path: string) => {
try {
@ -13,3 +14,7 @@ export const fileExists = (path: string) => {
export const getProjectDir = () => {
return path.resolve(process.cwd(), "storage/tmp/project1");
};
export const uid = cuid2.init({
length: 8,
});

View File

@ -1,7 +1,9 @@
import { router } from "../api/trpc";
import project from "./project";
import file from "./file";
export const appRouter = router({
project,
file,
});

View File

@ -8,17 +8,19 @@ import { TRPCError } from "@trpc/server";
const fileRouter = router({
getAll: procedure
.input(
z
.object({ id: z.number().array().min(1), isPinned: z.boolean() })
.partial()
.optional()
z.object({
projectId: z.number(),
id: z.number().array().min(1).optional(),
isPinned: z.boolean().optional(),
})
)
.query(async ({ input: opt }) => {
const files = await db.query.file.findMany({
where: and(
eq(file.projectId, opt.projectId),
isNull(file.deletedAt),
opt?.id && opt.id.length > 0 ? inArray(file.id, opt.id) : undefined,
opt?.isPinned ? eq(file.isPinned, true) : undefined
opt.id && opt.id.length > 0 ? inArray(file.id, opt.id) : undefined,
opt.isPinned ? eq(file.isPinned, true) : undefined
),
orderBy: [desc(file.isDirectory), asc(file.filename)],
columns: !file.isPinned ? { content: true } : undefined,
@ -55,7 +57,7 @@ const fileRouter = router({
}
const data: z.infer<typeof insertFileSchema> = {
userId: 1,
projectId: 1,
parentId: input.parentId,
path: basePath + input.filename,
filename: input.filename,

100
server/routers/project.ts Normal file
View File

@ -0,0 +1,100 @@
import { and, desc, eq, isNull, sql } from "drizzle-orm";
import db from "../db";
import { procedure, router } from "../api/trpc";
import {
project,
insertProjectSchema,
selectProjectSchema,
} from "../db/schema/project";
import { z } from "zod";
import { TRPCError } from "@trpc/server";
import { uid } from "../lib/utils";
const projectRouter = router({
getAll: procedure.query(async () => {
const where = and(isNull(project.deletedAt));
const projects = await db.query.project.findMany({
where,
with: {
user: { columns: { password: false } },
},
orderBy: [desc(project.id)],
});
return projects;
}),
getById: procedure
.input(z.number().or(z.string()))
.query(async ({ input }) => {
const where = and(
typeof input === "string"
? eq(project.slug, input)
: eq(project.id, input),
isNull(project.deletedAt)
);
return db.query.project.findFirst({
where,
with: {
user: { columns: { password: false } },
},
});
}),
create: procedure
.input(
insertProjectSchema.pick({
title: true,
})
)
.mutation(async ({ input }) => {
const data: z.infer<typeof insertProjectSchema> = {
userId: 1,
title: input.title,
slug: uid(),
};
const [result] = await db.insert(project).values(data).returning();
return result;
}),
update: procedure
.input(selectProjectSchema.partial().required({ id: true }))
.mutation(async ({ input }) => {
const projectData = await db.query.project.findFirst({
where: and(eq(project.id, input.id), isNull(project.deletedAt)),
});
if (!projectData) {
throw new TRPCError({ code: "NOT_FOUND" });
}
const [result] = await db
.update(project)
.set(input)
.where(and(eq(project.id, input.id), isNull(project.deletedAt)))
.returning();
return result;
}),
delete: procedure.input(z.number()).mutation(async ({ input }) => {
const projectData = await db.query.project.findFirst({
where: and(eq(project.id, input), isNull(project.deletedAt)),
});
if (!projectData) {
throw new TRPCError({ code: "NOT_FOUND" });
}
const [result] = await db
.update(project)
.set({ deletedAt: sql`CURRENT_TIMESTAMP` })
.where(eq(project.id, projectData.id))
.returning();
return result;
}),
});
export default projectRouter;