aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc
diff options
context:
space:
mode:
Diffstat (limited to 'packages/trpc')
-rw-r--r--packages/trpc/lib/attachments.ts19
-rw-r--r--packages/trpc/routers/bookmarks.test.ts20
-rw-r--r--packages/trpc/routers/bookmarks.ts156
-rw-r--r--packages/trpc/testUtils.ts5
4 files changed, 146 insertions, 54 deletions
diff --git a/packages/trpc/lib/attachments.ts b/packages/trpc/lib/attachments.ts
index 6fe1ef40..175947f8 100644
--- a/packages/trpc/lib/attachments.ts
+++ b/packages/trpc/lib/attachments.ts
@@ -8,6 +8,8 @@ export function mapDBAssetTypeToUserType(assetType: AssetTypes): ZAssetType {
[AssetTypes.LINK_SCREENSHOT]: "screenshot",
[AssetTypes.LINK_FULL_PAGE_ARCHIVE]: "fullPageArchive",
[AssetTypes.LINK_BANNER_IMAGE]: "bannerImage",
+ [AssetTypes.BOOKMARK_ASSET]: "bookmarkAsset",
+ [AssetTypes.UNKNOWN]: "bannerImage",
};
return map[assetType];
}
@@ -19,6 +21,8 @@ export function mapSchemaAssetTypeToDB(
screenshot: AssetTypes.LINK_SCREENSHOT,
fullPageArchive: AssetTypes.LINK_FULL_PAGE_ARCHIVE,
bannerImage: AssetTypes.LINK_BANNER_IMAGE,
+ bookmarkAsset: AssetTypes.BOOKMARK_ASSET,
+ unknown: AssetTypes.UNKNOWN,
};
return map[assetType];
}
@@ -28,6 +32,8 @@ export function humanFriendlyNameForAssertType(type: ZAssetType) {
screenshot: "Screenshot",
fullPageArchive: "Full Page Archive",
bannerImage: "Banner Image",
+ bookmarkAsset: "Bookmark Asset",
+ unknown: "Unknown",
};
return map[type];
}
@@ -37,6 +43,19 @@ export function isAllowedToAttachAsset(type: ZAssetType) {
screenshot: true,
fullPageArchive: false,
bannerImage: true,
+ bookmarkAsset: false,
+ unknown: false,
+ };
+ return map[type];
+}
+
+export function isAllowedToDetachAsset(type: ZAssetType) {
+ const map: Record<ZAssetType, boolean> = {
+ screenshot: true,
+ fullPageArchive: true,
+ bannerImage: true,
+ bookmarkAsset: false,
+ unknown: false,
};
return map[type];
}
diff --git a/packages/trpc/routers/bookmarks.test.ts b/packages/trpc/routers/bookmarks.test.ts
index d6a7bc27..d2944c40 100644
--- a/packages/trpc/routers/bookmarks.test.ts
+++ b/packages/trpc/routers/bookmarks.test.ts
@@ -369,6 +369,24 @@ describe("Bookmark Routes", () => {
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 (
@@ -424,7 +442,7 @@ describe("Bookmark Routes", () => {
await api.replaceAsset({
bookmarkId: bookmark.id,
oldAssetId: "asset3",
- newAssetId: "asset4",
+ newAssetId: "asset6",
}),
).rejects.toThrow(/You can't attach this type of asset/);
await expect(
diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts
index b1491a61..f272433a 100644
--- a/packages/trpc/routers/bookmarks.ts
+++ b/packages/trpc/routers/bookmarks.ts
@@ -44,6 +44,7 @@ import type { AuthedContext, Context } from "../index";
import { authedProcedure, router } from "../index";
import {
isAllowedToAttachAsset,
+ isAllowedToDetachAsset,
mapDBAssetTypeToUserType,
mapSchemaAssetTypeToDB,
} from "../lib/attachments";
@@ -80,23 +81,35 @@ export const ensureBookmarkOwnership = experimental_trpcMiddleware<{
return opts.next();
});
-interface Asset {
- id: string;
- assetType: AssetTypes;
-}
-
-function mapAssetsToBookmarkFields(assets: Asset | Asset[] = []) {
- const ASSET_TYE_MAPPING: Record<AssetTypes, string> = {
- [AssetTypes.LINK_SCREENSHOT]: "screenshotAssetId",
- [AssetTypes.LINK_FULL_PAGE_ARCHIVE]: "fullPageArchiveAssetId",
- [AssetTypes.LINK_BANNER_IMAGE]: "imageAssetId",
- };
- const assetsArray = Array.isArray(assets) ? assets : [assets];
- return assetsArray.reduce((result: Record<string, string>, asset: Asset) => {
- result[ASSET_TYE_MAPPING[asset.assetType]] = asset.id;
- return result;
- }, {});
-}
+export const ensureAssetOwnership = async (opts: {
+ ctx: Context;
+ assetId: string;
+}) => {
+ const asset = await opts.ctx.db.query.assets.findFirst({
+ where: eq(bookmarks.id, opts.assetId),
+ columns: {
+ userId: true,
+ },
+ });
+ 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",
+ });
+ }
+};
async function getBookmark(ctx: AuthedContext, bookmarkId: string) {
const bookmark = await ctx.db.query.bookmarks.findFirst({
@@ -189,7 +202,15 @@ function toZodSchema(bookmark: BookmarkQueryReturnType): ZBookmark {
case BookmarkTypes.LINK:
content = {
type: bookmark.type,
- ...mapAssetsToBookmarkFields(assets),
+ screenshotAssetId: assets.find(
+ (a) => a.assetType == AssetTypes.LINK_SCREENSHOT,
+ )?.id,
+ fullPageArchiveAssetId: assets.find(
+ (a) => a.assetType == AssetTypes.LINK_FULL_PAGE_ARCHIVE,
+ )?.id,
+ imageAssetId: assets.find(
+ (a) => a.assetType == AssetTypes.LINK_BANNER_IMAGE,
+ )?.id,
...link,
};
break;
@@ -307,6 +328,19 @@ export const bookmarksAppRouter = router({
sourceUrl: null,
})
.returning();
+ await ensureAssetOwnership({ ctx, assetId: input.assetId });
+ await tx
+ .update(assets)
+ .set({
+ bookmarkId: bookmark.id,
+ assetType: AssetTypes.BOOKMARK_ASSET,
+ })
+ .where(
+ and(
+ eq(assets.id, input.assetId),
+ eq(assets.userId, ctx.user.id),
+ ),
+ );
content = {
type: BookmarkTypes.ASSET,
assetType: asset.assetType,
@@ -647,10 +681,20 @@ export const bookmarksAppRouter = router({
row.assets &&
!acc[bookmarkId].assets.some((a) => a.id == row.assets!.id)
) {
- acc[bookmarkId].content = {
- ...acc[bookmarkId].content,
- ...mapAssetsToBookmarkFields(row.assets),
- };
+ if (acc[bookmarkId].content.type == BookmarkTypes.LINK) {
+ const content = acc[bookmarkId].content;
+ invariant(content.type == BookmarkTypes.LINK);
+ if (row.assets.assetType == AssetTypes.LINK_SCREENSHOT) {
+ content.screenshotAssetId = row.assets.id;
+ }
+ if (row.assets.assetType == AssetTypes.LINK_FULL_PAGE_ARCHIVE) {
+ content.fullPageArchiveAssetId = row.assets.id;
+ }
+ if (row.assets.assetType == AssetTypes.LINK_BANNER_IMAGE) {
+ content.imageAssetId = row.assets.id;
+ }
+ acc[bookmarkId].content = content;
+ }
acc[bookmarkId].assets.push({
id: row.assets.id,
assetType: mapDBAssetTypeToUserType(row.assets.assetType),
@@ -841,6 +885,7 @@ export const bookmarksAppRouter = router({
.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",
@@ -848,14 +893,14 @@ export const bookmarksAppRouter = router({
});
}
await ctx.db
- .insert(assets)
- .values({
- id: input.asset.id,
+ .update(assets)
+ .set({
assetType: mapSchemaAssetTypeToDB(input.asset.assetType),
bookmarkId: input.bookmarkId,
- userId: ctx.user.id,
})
- .returning();
+ .where(
+ and(eq(assets.id, input.asset.id), eq(assets.userId, ctx.user.id)),
+ );
return input.asset;
}),
replaceAsset: authedProcedure
@@ -869,21 +914,19 @@ export const bookmarksAppRouter = router({
.output(z.void())
.use(ensureBookmarkOwnership)
.mutation(async ({ input, ctx }) => {
- const oldAsset = await ctx.db
+ 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.bookmarkId, input.bookmarkId),
- ),
+ and(eq(assets.id, input.oldAssetId), eq(assets.userId, ctx.user.id)),
)
.limit(1);
- if (!oldAsset.length) {
- throw new TRPCError({ code: "NOT_FOUND" });
- }
if (
- !isAllowedToAttachAsset(mapDBAssetTypeToUserType(oldAsset[0].assetType))
+ !isAllowedToAttachAsset(mapDBAssetTypeToUserType(oldAsset.assetType))
) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -891,21 +934,17 @@ export const bookmarksAppRouter = router({
});
}
- const result = await ctx.db
- .update(assets)
- .set({
- id: input.newAssetId,
- bookmarkId: input.bookmarkId,
- })
- .where(
- and(
- eq(assets.id, input.oldAssetId),
- eq(assets.bookmarkId, input.bookmarkId),
- ),
- );
- if (result.changes == 0) {
- throw new TRPCError({ code: "NOT_FOUND" });
- }
+ 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,
@@ -921,6 +960,21 @@ export const bookmarksAppRouter = router({
.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(
diff --git a/packages/trpc/testUtils.ts b/packages/trpc/testUtils.ts
index 67fbddcc..04e6b0a3 100644
--- a/packages/trpc/testUtils.ts
+++ b/packages/trpc/testUtils.ts
@@ -26,12 +26,13 @@ export async function seedUsers(db: TestDB) {
.returning();
}
-export function getApiCaller(db: TestDB, userId?: string) {
+export function getApiCaller(db: TestDB, userId?: string, email?: string) {
const createCaller = createCallerFactory(appRouter);
return createCaller({
user: userId
? {
id: userId,
+ email,
role: "user",
}
: null,
@@ -55,7 +56,7 @@ export async function buildTestContext(
if (seedDB) {
users = await seedUsers(db);
}
- const callers = users.map((u) => getApiCaller(db, u.id));
+ const callers = users.map((u) => getApiCaller(db, u.id, u.email));
return {
apiCallers: callers,