aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/shared-react/hooks/assets.ts49
-rw-r--r--packages/shared-react/hooks/bookmarks.ts45
-rw-r--r--packages/trpc/lib/attachments.ts2
-rw-r--r--packages/trpc/routers/_app.ts2
-rw-r--r--packages/trpc/routers/assets.test.ts128
-rw-r--r--packages/trpc/routers/assets.ts213
-rw-r--r--packages/trpc/routers/bookmarks.test.ts119
-rw-r--r--packages/trpc/routers/bookmarks.ts153
8 files changed, 397 insertions, 314 deletions
diff --git a/packages/shared-react/hooks/assets.ts b/packages/shared-react/hooks/assets.ts
new file mode 100644
index 00000000..b9aeed26
--- /dev/null
+++ b/packages/shared-react/hooks/assets.ts
@@ -0,0 +1,49 @@
+import { api } from "../trpc";
+
+export function useAttachBookmarkAsset(
+ ...opts: Parameters<typeof api.assets.attachAsset.useMutation>
+) {
+ const apiUtils = api.useUtils();
+ return api.assets.attachAsset.useMutation({
+ ...opts[0],
+ onSuccess: (res, req, meta) => {
+ apiUtils.bookmarks.getBookmarks.invalidate();
+ apiUtils.bookmarks.searchBookmarks.invalidate();
+ apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
+ apiUtils.assets.list.invalidate();
+ return opts[0]?.onSuccess?.(res, req, meta);
+ },
+ });
+}
+
+export function useReplaceBookmarkAsset(
+ ...opts: Parameters<typeof api.assets.replaceAsset.useMutation>
+) {
+ const apiUtils = api.useUtils();
+ return api.assets.replaceAsset.useMutation({
+ ...opts[0],
+ onSuccess: (res, req, meta) => {
+ apiUtils.bookmarks.getBookmarks.invalidate();
+ apiUtils.bookmarks.searchBookmarks.invalidate();
+ apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
+ apiUtils.assets.list.invalidate();
+ return opts[0]?.onSuccess?.(res, req, meta);
+ },
+ });
+}
+
+export function useDetachBookmarkAsset(
+ ...opts: Parameters<typeof api.assets.detachAsset.useMutation>
+) {
+ const apiUtils = api.useUtils();
+ return api.assets.detachAsset.useMutation({
+ ...opts[0],
+ onSuccess: (res, req, meta) => {
+ apiUtils.bookmarks.getBookmarks.invalidate();
+ apiUtils.bookmarks.searchBookmarks.invalidate();
+ apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
+ apiUtils.assets.list.invalidate();
+ return opts[0]?.onSuccess?.(res, req, meta);
+ },
+ });
+}
diff --git a/packages/shared-react/hooks/bookmarks.ts b/packages/shared-react/hooks/bookmarks.ts
index f4dd203c..89715e4f 100644
--- a/packages/shared-react/hooks/bookmarks.ts
+++ b/packages/shared-react/hooks/bookmarks.ts
@@ -190,48 +190,3 @@ export function useBookmarkPostCreationHook() {
return Promise.all(promises);
};
}
-
-export function useAttachBookmarkAsset(
- ...opts: Parameters<typeof api.bookmarks.attachAsset.useMutation>
-) {
- const apiUtils = api.useUtils();
- return api.bookmarks.attachAsset.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta) => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.searchBookmarks.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
- return opts[0]?.onSuccess?.(res, req, meta);
- },
- });
-}
-
-export function useReplaceBookmarkAsset(
- ...opts: Parameters<typeof api.bookmarks.replaceAsset.useMutation>
-) {
- const apiUtils = api.useUtils();
- return api.bookmarks.replaceAsset.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta) => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.searchBookmarks.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
- return opts[0]?.onSuccess?.(res, req, meta);
- },
- });
-}
-
-export function useDetachBookmarkAsset(
- ...opts: Parameters<typeof api.bookmarks.detachAsset.useMutation>
-) {
- const apiUtils = api.useUtils();
- return api.bookmarks.detachAsset.useMutation({
- ...opts[0],
- onSuccess: (res, req, meta) => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.searchBookmarks.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId });
- return opts[0]?.onSuccess?.(res, req, meta);
- },
- });
-}
diff --git a/packages/trpc/lib/attachments.ts b/packages/trpc/lib/attachments.ts
index 3ad79a5a..29c99172 100644
--- a/packages/trpc/lib/attachments.ts
+++ b/packages/trpc/lib/attachments.ts
@@ -66,7 +66,7 @@ export function isAllowedToDetachAsset(type: ZAssetType) {
screenshot: true,
assetScreenshot: true,
fullPageArchive: true,
- precrawledArchive: false,
+ precrawledArchive: true,
bannerImage: true,
video: true,
bookmarkAsset: false,
diff --git a/packages/trpc/routers/_app.ts b/packages/trpc/routers/_app.ts
index 0d555a65..7af19884 100644
--- a/packages/trpc/routers/_app.ts
+++ b/packages/trpc/routers/_app.ts
@@ -1,6 +1,7 @@
import { router } from "../index";
import { adminAppRouter } from "./admin";
import { apiKeysAppRouter } from "./apiKeys";
+import { assetsAppRouter } from "./assets";
import { bookmarksAppRouter } from "./bookmarks";
import { feedsAppRouter } from "./feeds";
import { highlightsAppRouter } from "./highlights";
@@ -21,6 +22,7 @@ export const appRouter = router({
feeds: feedsAppRouter,
highlights: highlightsAppRouter,
webhooks: webhooksAppRouter,
+ assets: assetsAppRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;
diff --git a/packages/trpc/routers/assets.test.ts b/packages/trpc/routers/assets.test.ts
new file mode 100644
index 00000000..d7db35be
--- /dev/null
+++ b/packages/trpc/routers/assets.test.ts
@@ -0,0 +1,128 @@
+import { beforeEach, describe, expect, test } from "vitest";
+
+import { assets, AssetTypes } from "@hoarder/db/schema";
+import { BookmarkTypes, ZAssetType } from "@hoarder/shared/types/bookmarks";
+
+import type { CustomTestContext } from "../testUtils";
+import { defaultBeforeEach } from "../testUtils";
+
+beforeEach<CustomTestContext>(defaultBeforeEach(true));
+
+describe("Asset Routes", () => {
+ test<CustomTestContext>("mutate assets", async ({ apiCallers, db }) => {
+ const api = apiCallers[0].assets;
+ const userId = await apiCallers[0].users.whoami().then((u) => u.id);
+
+ const bookmark = await apiCallers[0].bookmarks.createBookmark({
+ url: "https://google.com",
+ type: BookmarkTypes.LINK,
+ });
+ await Promise.all([
+ db.insert(assets).values({
+ id: "asset1",
+ assetType: AssetTypes.LINK_SCREENSHOT,
+ bookmarkId: bookmark.id,
+ userId,
+ }),
+ db.insert(assets).values({
+ id: "asset2",
+ assetType: AssetTypes.LINK_BANNER_IMAGE,
+ bookmarkId: bookmark.id,
+ userId,
+ }),
+ db.insert(assets).values({
+ id: "asset3",
+ assetType: AssetTypes.LINK_FULL_PAGE_ARCHIVE,
+ bookmarkId: bookmark.id,
+ userId,
+ }),
+ db.insert(assets).values({
+ id: "asset4",
+ assetType: AssetTypes.UNKNOWN,
+ bookmarkId: null,
+ userId,
+ }),
+ db.insert(assets).values({
+ id: "asset5",
+ assetType: AssetTypes.UNKNOWN,
+ bookmarkId: null,
+ userId,
+ }),
+ db.insert(assets).values({
+ id: "asset6",
+ assetType: AssetTypes.UNKNOWN,
+ bookmarkId: null,
+ userId,
+ }),
+ ]);
+
+ const validateAssets = async (
+ expected: { id: string; assetType: ZAssetType }[],
+ ) => {
+ const b = await apiCallers[0].bookmarks.getBookmark({
+ bookmarkId: bookmark.id,
+ });
+ b.assets.sort((a, b) => a.id.localeCompare(b.id));
+ expect(b.assets).toEqual(expected);
+ };
+
+ await api.attachAsset({
+ bookmarkId: bookmark.id,
+ asset: {
+ id: "asset4",
+ assetType: "screenshot",
+ },
+ });
+
+ await validateAssets([
+ { id: "asset1", assetType: "screenshot" },
+ { id: "asset2", assetType: "bannerImage" },
+ { id: "asset3", assetType: "fullPageArchive" },
+ { id: "asset4", assetType: "screenshot" },
+ ]);
+
+ await api.replaceAsset({
+ bookmarkId: bookmark.id,
+ oldAssetId: "asset1",
+ newAssetId: "asset5",
+ });
+
+ await validateAssets([
+ { id: "asset2", assetType: "bannerImage" },
+ { id: "asset3", assetType: "fullPageArchive" },
+ { id: "asset4", assetType: "screenshot" },
+ { id: "asset5", assetType: "screenshot" },
+ ]);
+
+ await api.detachAsset({
+ bookmarkId: bookmark.id,
+ assetId: "asset4",
+ });
+
+ await validateAssets([
+ { id: "asset2", assetType: "bannerImage" },
+ { id: "asset3", assetType: "fullPageArchive" },
+ { id: "asset5", assetType: "screenshot" },
+ ]);
+
+ // You're not allowed to attach/replace a fullPageArchive
+ await expect(
+ async () =>
+ await api.replaceAsset({
+ bookmarkId: bookmark.id,
+ oldAssetId: "asset3",
+ newAssetId: "asset6",
+ }),
+ ).rejects.toThrow(/You can't attach this type of asset/);
+ await expect(
+ async () =>
+ await api.attachAsset({
+ bookmarkId: bookmark.id,
+ asset: {
+ id: "asset6",
+ assetType: "fullPageArchive",
+ },
+ }),
+ ).rejects.toThrow(/You can't attach this type of asset/);
+ });
+});
diff --git a/packages/trpc/routers/assets.ts b/packages/trpc/routers/assets.ts
new file mode 100644
index 00000000..45eac068
--- /dev/null
+++ b/packages/trpc/routers/assets.ts
@@ -0,0 +1,213 @@
+import { TRPCError } from "@trpc/server";
+import { and, desc, eq, sql } from "drizzle-orm";
+import { z } from "zod";
+
+import { assets, bookmarks } from "@hoarder/db/schema";
+import { deleteAsset } from "@hoarder/shared/assetdb";
+import {
+ zAssetSchema,
+ zAssetTypesSchema,
+} from "@hoarder/shared/types/bookmarks";
+
+import { authedProcedure, Context, router } from "../index";
+import {
+ isAllowedToAttachAsset,
+ isAllowedToDetachAsset,
+ mapDBAssetTypeToUserType,
+ mapSchemaAssetTypeToDB,
+} from "../lib/attachments";
+import { ensureBookmarkOwnership } from "./bookmarks";
+
+export const ensureAssetOwnership = async (opts: {
+ ctx: Context;
+ assetId: string;
+}) => {
+ const asset = await opts.ctx.db.query.assets.findFirst({
+ where: eq(bookmarks.id, opts.assetId),
+ });
+ if (!opts.ctx.user) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "User is not authorized",
+ });
+ }
+ if (!asset) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Asset not found",
+ });
+ }
+ if (asset.userId != opts.ctx.user.id) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "User is not allowed to access resource",
+ });
+ }
+ return asset;
+};
+
+export const assetsAppRouter = router({
+ list: authedProcedure
+ .input(
+ z.object({
+ limit: z.number().min(1).max(100).default(20),
+ cursor: z.number().nullish(), // page number
+ }),
+ )
+ .output(
+ z.object({
+ assets: z.array(
+ z.object({
+ id: z.string(),
+ assetType: zAssetTypesSchema,
+ size: z.number(),
+ contentType: z.string().nullable(),
+ fileName: z.string().nullable(),
+ bookmarkId: z.string().nullable(),
+ }),
+ ),
+ nextCursor: z.number().nullish(),
+ totalCount: z.number(),
+ }),
+ )
+ .query(async ({ input, ctx }) => {
+ const page = input.cursor ?? 1;
+ const [results, totalCount] = await Promise.all([
+ ctx.db
+ .select()
+ .from(assets)
+ .where(eq(assets.userId, ctx.user.id))
+ .orderBy(desc(assets.size))
+ .limit(input.limit)
+ .offset((page - 1) * input.limit),
+ ctx.db
+ .select({ count: sql<number>`count(*)` })
+ .from(assets)
+ .where(eq(assets.userId, ctx.user.id)),
+ ]);
+
+ return {
+ assets: results.map((a) => ({
+ ...a,
+ assetType: mapDBAssetTypeToUserType(a.assetType),
+ })),
+ nextCursor: page * input.limit < totalCount[0].count ? page + 1 : null,
+ totalCount: totalCount[0].count,
+ };
+ }),
+ attachAsset: authedProcedure
+ .input(
+ z.object({
+ bookmarkId: z.string(),
+ asset: zAssetSchema,
+ }),
+ )
+ .output(zAssetSchema)
+ .use(ensureBookmarkOwnership)
+ .mutation(async ({ input, ctx }) => {
+ await ensureAssetOwnership({ ctx, assetId: input.asset.id });
+ if (!isAllowedToAttachAsset(input.asset.assetType)) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "You can't attach this type of asset",
+ });
+ }
+ await ctx.db
+ .update(assets)
+ .set({
+ assetType: mapSchemaAssetTypeToDB(input.asset.assetType),
+ bookmarkId: input.bookmarkId,
+ })
+ .where(
+ and(eq(assets.id, input.asset.id), eq(assets.userId, ctx.user.id)),
+ );
+ return input.asset;
+ }),
+ replaceAsset: authedProcedure
+ .input(
+ z.object({
+ bookmarkId: z.string(),
+ oldAssetId: z.string(),
+ newAssetId: z.string(),
+ }),
+ )
+ .output(z.void())
+ .use(ensureBookmarkOwnership)
+ .mutation(async ({ input, ctx }) => {
+ await Promise.all([
+ ensureAssetOwnership({ ctx, assetId: input.oldAssetId }),
+ ensureAssetOwnership({ ctx, assetId: input.newAssetId }),
+ ]);
+ const [oldAsset] = await ctx.db
+ .select()
+ .from(assets)
+ .where(
+ and(eq(assets.id, input.oldAssetId), eq(assets.userId, ctx.user.id)),
+ )
+ .limit(1);
+ if (
+ !isAllowedToAttachAsset(mapDBAssetTypeToUserType(oldAsset.assetType))
+ ) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "You can't attach this type of asset",
+ });
+ }
+
+ await ctx.db.transaction(async (tx) => {
+ await tx.delete(assets).where(eq(assets.id, input.oldAssetId));
+ await tx
+ .update(assets)
+ .set({
+ bookmarkId: input.bookmarkId,
+ assetType: oldAsset.assetType,
+ })
+ .where(eq(assets.id, input.newAssetId));
+ });
+
+ await deleteAsset({
+ userId: ctx.user.id,
+ assetId: input.oldAssetId,
+ }).catch(() => ({}));
+ }),
+ detachAsset: authedProcedure
+ .input(
+ z.object({
+ bookmarkId: z.string(),
+ assetId: z.string(),
+ }),
+ )
+ .output(z.void())
+ .use(ensureBookmarkOwnership)
+ .mutation(async ({ input, ctx }) => {
+ await ensureAssetOwnership({ ctx, assetId: input.assetId });
+ const [oldAsset] = await ctx.db
+ .select()
+ .from(assets)
+ .where(
+ and(eq(assets.id, input.assetId), eq(assets.userId, ctx.user.id)),
+ );
+ if (
+ !isAllowedToDetachAsset(mapDBAssetTypeToUserType(oldAsset.assetType))
+ ) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "You can't deattach this type of asset",
+ });
+ }
+ const result = await ctx.db
+ .delete(assets)
+ .where(
+ and(
+ eq(assets.id, input.assetId),
+ eq(assets.bookmarkId, input.bookmarkId),
+ ),
+ );
+ if (result.changes == 0) {
+ throw new TRPCError({ code: "NOT_FOUND" });
+ }
+ await deleteAsset({ userId: ctx.user.id, assetId: input.assetId }).catch(
+ () => ({}),
+ );
+ }),
+});
diff --git a/packages/trpc/routers/bookmarks.test.ts b/packages/trpc/routers/bookmarks.test.ts
index d2944c40..d89f80fd 100644
--- a/packages/trpc/routers/bookmarks.test.ts
+++ b/packages/trpc/routers/bookmarks.test.ts
@@ -1,7 +1,7 @@
import { assert, beforeEach, describe, expect, test } from "vitest";
-import { assets, AssetTypes, bookmarks } from "@hoarder/db/schema";
-import { BookmarkTypes, ZAssetType } from "@hoarder/shared/types/bookmarks";
+import { bookmarks } from "@hoarder/db/schema";
+import { BookmarkTypes } from "@hoarder/shared/types/bookmarks";
import type { CustomTestContext } from "../testUtils";
import { defaultBeforeEach } from "../testUtils";
@@ -341,119 +341,4 @@ describe("Bookmark Routes", () => {
await validateWithLimit(10);
await validateWithLimit(100);
});
-
- test<CustomTestContext>("mutate assets", async ({ apiCallers, db }) => {
- const api = apiCallers[0].bookmarks;
- const userId = await apiCallers[0].users.whoami().then((u) => u.id);
-
- const bookmark = await api.createBookmark({
- url: "https://google.com",
- type: BookmarkTypes.LINK,
- });
- await Promise.all([
- db.insert(assets).values({
- id: "asset1",
- assetType: AssetTypes.LINK_SCREENSHOT,
- bookmarkId: bookmark.id,
- userId,
- }),
- db.insert(assets).values({
- id: "asset2",
- assetType: AssetTypes.LINK_BANNER_IMAGE,
- bookmarkId: bookmark.id,
- userId,
- }),
- db.insert(assets).values({
- id: "asset3",
- assetType: AssetTypes.LINK_FULL_PAGE_ARCHIVE,
- bookmarkId: bookmark.id,
- userId,
- }),
- db.insert(assets).values({
- id: "asset4",
- assetType: AssetTypes.UNKNOWN,
- bookmarkId: null,
- userId,
- }),
- db.insert(assets).values({
- id: "asset5",
- assetType: AssetTypes.UNKNOWN,
- bookmarkId: null,
- userId,
- }),
- db.insert(assets).values({
- id: "asset6",
- assetType: AssetTypes.UNKNOWN,
- bookmarkId: null,
- userId,
- }),
- ]);
-
- const validateAssets = async (
- expected: { id: string; assetType: ZAssetType }[],
- ) => {
- const b = await api.getBookmark({ bookmarkId: bookmark.id });
- b.assets.sort((a, b) => a.id.localeCompare(b.id));
- expect(b.assets).toEqual(expected);
- };
-
- await api.attachAsset({
- bookmarkId: bookmark.id,
- asset: {
- id: "asset4",
- assetType: "screenshot",
- },
- });
-
- await validateAssets([
- { id: "asset1", assetType: "screenshot" },
- { id: "asset2", assetType: "bannerImage" },
- { id: "asset3", assetType: "fullPageArchive" },
- { id: "asset4", assetType: "screenshot" },
- ]);
-
- await api.replaceAsset({
- bookmarkId: bookmark.id,
- oldAssetId: "asset1",
- newAssetId: "asset5",
- });
-
- await validateAssets([
- { id: "asset2", assetType: "bannerImage" },
- { id: "asset3", assetType: "fullPageArchive" },
- { id: "asset4", assetType: "screenshot" },
- { id: "asset5", assetType: "screenshot" },
- ]);
-
- await api.detachAsset({
- bookmarkId: bookmark.id,
- assetId: "asset4",
- });
-
- await validateAssets([
- { id: "asset2", assetType: "bannerImage" },
- { id: "asset3", assetType: "fullPageArchive" },
- { id: "asset5", assetType: "screenshot" },
- ]);
-
- // You're not allowed to attach/replace a fullPageArchive
- await expect(
- async () =>
- await api.replaceAsset({
- bookmarkId: bookmark.id,
- oldAssetId: "asset3",
- newAssetId: "asset6",
- }),
- ).rejects.toThrow(/You can't attach this type of asset/);
- await expect(
- async () =>
- await api.attachAsset({
- bookmarkId: bookmark.id,
- asset: {
- id: "asset6",
- assetType: "fullPageArchive",
- },
- }),
- ).rejects.toThrow(/You can't attach this type of asset/);
- });
});
diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts
index 6ab863fb..3b2d23ce 100644
--- a/packages/trpc/routers/bookmarks.ts
+++ b/packages/trpc/routers/bookmarks.ts
@@ -55,7 +55,6 @@ import { parseSearchQuery } from "@hoarder/shared/searchQueryParser";
import {
BookmarkTypes,
DEFAULT_NUM_BOOKMARKS_PER_PAGE,
- zAssetSchema,
zBareBookmarkSchema,
zBookmarkSchema,
zGetBookmarksRequestSchema,
@@ -69,13 +68,9 @@ import {
import type { AuthedContext, Context } from "../index";
import { authedProcedure, router } from "../index";
-import {
- isAllowedToAttachAsset,
- isAllowedToDetachAsset,
- mapDBAssetTypeToUserType,
- mapSchemaAssetTypeToDB,
-} from "../lib/attachments";
+import { mapDBAssetTypeToUserType } from "../lib/attachments";
import { getBookmarkIdsFromMatcher } from "../lib/search";
+import { ensureAssetOwnership } from "./assets";
export const ensureBookmarkOwnership = experimental_trpcMiddleware<{
ctx: Context;
@@ -109,34 +104,6 @@ export const ensureBookmarkOwnership = experimental_trpcMiddleware<{
return opts.next();
});
-export const ensureAssetOwnership = async (opts: {
- ctx: Context;
- assetId: string;
-}) => {
- const asset = await opts.ctx.db.query.assets.findFirst({
- where: eq(bookmarks.id, opts.assetId),
- });
- if (!opts.ctx.user) {
- throw new TRPCError({
- code: "UNAUTHORIZED",
- message: "User is not authorized",
- });
- }
- if (!asset) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: "Asset not found",
- });
- }
- if (asset.userId != opts.ctx.user.id) {
- throw new TRPCError({
- code: "FORBIDDEN",
- message: "User is not allowed to access resource",
- });
- }
- return asset;
-};
-
async function getBookmark(ctx: AuthedContext, bookmarkId: string) {
const bookmark = await ctx.db.query.bookmarks.findFirst({
where: and(eq(bookmarks.userId, ctx.user.id), eq(bookmarks.id, bookmarkId)),
@@ -1060,122 +1027,6 @@ export const bookmarksAppRouter = router({
};
});
}),
-
- attachAsset: authedProcedure
- .input(
- z.object({
- bookmarkId: z.string(),
- asset: zAssetSchema,
- }),
- )
- .output(zAssetSchema)
- .use(ensureBookmarkOwnership)
- .mutation(async ({ input, ctx }) => {
- await ensureAssetOwnership({ ctx, assetId: input.asset.id });
- if (!isAllowedToAttachAsset(input.asset.assetType)) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "You can't attach this type of asset",
- });
- }
- await ctx.db
- .update(assets)
- .set({
- assetType: mapSchemaAssetTypeToDB(input.asset.assetType),
- bookmarkId: input.bookmarkId,
- })
- .where(
- and(eq(assets.id, input.asset.id), eq(assets.userId, ctx.user.id)),
- );
- return input.asset;
- }),
- replaceAsset: authedProcedure
- .input(
- z.object({
- bookmarkId: z.string(),
- oldAssetId: z.string(),
- newAssetId: z.string(),
- }),
- )
- .output(z.void())
- .use(ensureBookmarkOwnership)
- .mutation(async ({ input, ctx }) => {
- await Promise.all([
- ensureAssetOwnership({ ctx, assetId: input.oldAssetId }),
- ensureAssetOwnership({ ctx, assetId: input.newAssetId }),
- ]);
- const [oldAsset] = await ctx.db
- .select()
- .from(assets)
- .where(
- and(eq(assets.id, input.oldAssetId), eq(assets.userId, ctx.user.id)),
- )
- .limit(1);
- if (
- !isAllowedToAttachAsset(mapDBAssetTypeToUserType(oldAsset.assetType))
- ) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "You can't attach this type of asset",
- });
- }
-
- await ctx.db.transaction(async (tx) => {
- await tx.delete(assets).where(eq(assets.id, input.oldAssetId));
- await tx
- .update(assets)
- .set({
- bookmarkId: input.bookmarkId,
- assetType: oldAsset.assetType,
- })
- .where(eq(assets.id, input.newAssetId));
- });
-
- await deleteAsset({
- userId: ctx.user.id,
- assetId: input.oldAssetId,
- }).catch(() => ({}));
- }),
- detachAsset: authedProcedure
- .input(
- z.object({
- bookmarkId: z.string(),
- assetId: z.string(),
- }),
- )
- .output(z.void())
- .use(ensureBookmarkOwnership)
- .mutation(async ({ input, ctx }) => {
- await ensureAssetOwnership({ ctx, assetId: input.assetId });
- const [oldAsset] = await ctx.db
- .select()
- .from(assets)
- .where(
- and(eq(assets.id, input.assetId), eq(assets.userId, ctx.user.id)),
- );
- if (
- !isAllowedToDetachAsset(mapDBAssetTypeToUserType(oldAsset.assetType))
- ) {
- throw new TRPCError({
- code: "BAD_REQUEST",
- message: "You can't deattach this type of asset",
- });
- }
- const result = await ctx.db
- .delete(assets)
- .where(
- and(
- eq(assets.id, input.assetId),
- eq(assets.bookmarkId, input.bookmarkId),
- ),
- );
- if (result.changes == 0) {
- throw new TRPCError({ code: "NOT_FOUND" });
- }
- await deleteAsset({ userId: ctx.user.id, assetId: input.assetId }).catch(
- () => ({}),
- );
- }),
getBrokenLinks: authedProcedure
.output(
z.object({