diff --git a/apps/website/src/lib/notification/maker/comment.ts b/apps/website/src/lib/notification/maker/comment.ts new file mode 100644 index 0000000000..6f2129ecf6 --- /dev/null +++ b/apps/website/src/lib/notification/maker/comment.ts @@ -0,0 +1,66 @@ +import { and, eq } from 'drizzle-orm'; +import { database, PostComments, PostRevisions, Posts, Profiles, Spaces } from '$lib/server/database'; +import { createNotification, useFirstRow } from '$lib/server/utils'; +import type { NotificationMaker } from './type'; + +export const commentNotificationMaker: NotificationMaker = async (commentId) => { + const comment = await database + .select({ + id: PostComments.id, + content: PostComments.content, + commenterId: PostComments.userId, + profileId: PostComments.profileId, + profileName: Profiles.name, + spaceId: Posts.spaceId, + spaceSlug: Spaces.slug, + postPermalink: Posts.permalink, + postWriterId: Posts.userId, + postTitle: PostRevisions.title, + parentId: PostComments.parentId, + }) + .from(PostComments) + .innerJoin(Posts, eq(PostComments.postId, Posts.id)) + .innerJoin(Spaces, eq(Posts.spaceId, Spaces.id)) + .innerJoin(PostRevisions, eq(Posts.publishedRevisionId, PostRevisions.id)) + .innerJoin(Profiles, eq(PostComments.profileId, Profiles.id)) + .where(and(eq(PostComments.id, commentId), eq(PostComments.state, 'ACTIVE'))) + .then(useFirstRow); + + if (!comment || !comment.spaceId) { + return; + } + + let notifiedUserId = comment.postWriterId; + + if (comment.parentId) { + const parentComment = await database + .select({ + userId: PostComments.userId, + }) + .from(PostComments) + .where(and(eq(PostComments.id, comment.parentId), eq(PostComments.state, 'ACTIVE'))) + .then(useFirstRow); + + if (parentComment) { + if (parentComment.userId === comment.commenterId) { + return; + } + + notifiedUserId = parentComment.userId; + } + } else if (notifiedUserId === comment.commenterId) { + return; + } + + await createNotification({ + userId: notifiedUserId, + category: 'COMMENT', + actorId: comment.profileId, + data: { + commentId: comment.id, + }, + pushTitle: comment.postTitle ?? '(제목 없음)', + pushBody: `${comment.profileName}님이 "${comment.content}" 댓글을 남겼어요.`, + pushPath: `/${comment.spaceSlug}/${comment.postPermalink}`, + }); +}; diff --git a/apps/website/src/lib/notification/maker/emoji-reaction.ts b/apps/website/src/lib/notification/maker/emoji-reaction.ts new file mode 100644 index 0000000000..68e68d7cb5 --- /dev/null +++ b/apps/website/src/lib/notification/maker/emoji-reaction.ts @@ -0,0 +1,43 @@ +import { eq } from 'drizzle-orm'; +import { database, PostReactions, PostRevisions, Posts, Spaces } from '$lib/server/database'; +import { createNotification, getSpaceProfile, useFirstRow } from '$lib/server/utils'; +import type { NotificationMaker } from './type'; + +export const emojiReactionNotificationMaker: NotificationMaker = async (reactionId) => { + const reaction = await database + .select({ + emoji: PostReactions.emoji, + postId: Posts.id, + postPermalink: Posts.permalink, + title: PostRevisions.title, + postWriterId: Posts.userId, + spaceId: Posts.spaceId, + spaceSlug: Spaces.slug, + emojigazerId: PostReactions.userId, + }) + .from(PostReactions) + .innerJoin(Posts, eq(PostReactions.postId, Posts.id)) + .innerJoin(PostRevisions, eq(Posts.publishedRevisionId, PostRevisions.id)) + .innerJoin(Spaces, eq(Posts.spaceId, Spaces.id)) + .where(eq(PostReactions.id, reactionId)) + .then(useFirstRow); + + if (!reaction || !reaction.spaceId || reaction.postWriterId === reaction.emojigazerId) { + return; + } + + const profile = await getSpaceProfile({ spaceId: reaction.spaceId, userId: reaction.emojigazerId }); + + await createNotification({ + userId: reaction.postWriterId, + category: 'EMOJI_REACTION', + actorId: profile.id, + data: { + postId: reaction.postId, + emoji: reaction.emoji, + }, + pushTitle: reaction.title ?? '(제목 없음)', + pushBody: `${profile.name}님이 이모지를 남겼어요.`, + pushPath: `/${reaction.spaceSlug}/${reaction.postPermalink}`, + }); +}; diff --git a/apps/website/src/lib/notification/maker/index.ts b/apps/website/src/lib/notification/maker/index.ts new file mode 100644 index 0000000000..2997fc9de7 --- /dev/null +++ b/apps/website/src/lib/notification/maker/index.ts @@ -0,0 +1,11 @@ +import { commentNotificationMaker } from './comment'; +import { emojiReactionNotificationMaker } from './emoji-reaction'; +import { purchaseNotificationMaker } from './purchase'; +import { subscribeNotificationMaker } from './subscribe'; + +export const notificationMaker = { + EMOJI_REACTION: emojiReactionNotificationMaker, + COMMENT: commentNotificationMaker, + PURCHASE: purchaseNotificationMaker, + SUBSCRIBE: subscribeNotificationMaker, +}; diff --git a/apps/website/src/lib/notification/maker/purchase.ts b/apps/website/src/lib/notification/maker/purchase.ts new file mode 100644 index 0000000000..33e947ccde --- /dev/null +++ b/apps/website/src/lib/notification/maker/purchase.ts @@ -0,0 +1,38 @@ +import { eq } from 'drizzle-orm'; +import { database, PostPurchases, PostRevisions, Posts } from '$lib/server/database'; +import { createNotification, getSpaceProfile, useFirstRow } from '$lib/server/utils'; +import type { NotificationMaker } from './type'; + +export const purchaseNotificationMaker: NotificationMaker = async (purchaseId) => { + const purchase = await database + .select({ + postId: PostPurchases.postId, + buyerId: PostPurchases.userId, + postWriterId: Posts.userId, + spaceId: Posts.spaceId, + postTitle: PostRevisions.title, + }) + .from(PostPurchases) + .innerJoin(Posts, eq(PostPurchases.postId, Posts.id)) + .innerJoin(PostRevisions, eq(Posts.publishedRevisionId, PostRevisions.id)) + .where(eq(PostPurchases.id, purchaseId)) + .then(useFirstRow); + + if (!purchase || !purchase.spaceId) { + return; + } + + const profile = await getSpaceProfile({ spaceId: purchase.spaceId, userId: purchase.buyerId }); + + await createNotification({ + userId: purchase.postWriterId, + category: 'PURCHASE', + actorId: profile.id, + data: { + postId: purchase.postId, + }, + pushTitle: purchase.postTitle ?? '(제목 없음)', + pushBody: `${profile.name}님이 포스트를 구매했어요.`, + pushPath: '/me/revenue', + }); +}; diff --git a/apps/website/src/lib/notification/maker/subscribe.ts b/apps/website/src/lib/notification/maker/subscribe.ts new file mode 100644 index 0000000000..4cdb951653 --- /dev/null +++ b/apps/website/src/lib/notification/maker/subscribe.ts @@ -0,0 +1,48 @@ +import { eq } from 'drizzle-orm'; +import { database, SpaceFollows, SpaceMembers, Spaces } from '$lib/server/database'; +import { createNotification, getSpaceProfile, useFirstRow } from '$lib/server/utils'; +import type { NotificationMaker } from './type'; + +export const subscribeNotificationMaker: NotificationMaker = async (spaceFollowId) => { + const spaceFollow = await database + .select({ + followerId: SpaceFollows.userId, + spaceId: SpaceFollows.spaceId, + spaceSlug: Spaces.slug, + spaceName: Spaces.name, + }) + .from(SpaceFollows) + .innerJoin(Spaces, eq(SpaceFollows.spaceId, Spaces.id)) + .where(eq(SpaceFollows.id, spaceFollowId)) + .then(useFirstRow); + + if (!spaceFollow) { + return; + } + + const profile = await getSpaceProfile({ spaceId: spaceFollow.spaceId, userId: spaceFollow.followerId }); + + const spaceMemberIds = await database + .select({ + userId: SpaceMembers.userId, + }) + .from(SpaceMembers) + .where(eq(SpaceMembers.spaceId, spaceFollow.spaceId)) + .then((rows) => rows.map((row) => row.userId)); + + await Promise.all( + spaceMemberIds.map(async (userId) => { + await createNotification({ + userId, + category: 'SUBSCRIBE', + actorId: profile.id, + data: { + spaceId: spaceFollow.spaceId, + }, + pushTitle: spaceFollow.spaceName, + pushBody: `${profile.name}님이 스페이스를 구독했어요.`, + pushPath: `/${spaceFollow.spaceSlug}`, + }); + }), + ); +}; diff --git a/apps/website/src/lib/notification/maker/type.ts b/apps/website/src/lib/notification/maker/type.ts new file mode 100644 index 0000000000..f633fd8d6d --- /dev/null +++ b/apps/website/src/lib/notification/maker/type.ts @@ -0,0 +1,3 @@ +import type { MaybePromise } from '$lib/types'; + +export type NotificationMaker = (targetId: string) => MaybePromise; diff --git a/apps/website/src/lib/server/graphql/schemas/comment.ts b/apps/website/src/lib/server/graphql/schemas/comment.ts index 606c471689..9a3810102a 100644 --- a/apps/website/src/lib/server/graphql/schemas/comment.ts +++ b/apps/website/src/lib/server/graphql/schemas/comment.ts @@ -13,7 +13,7 @@ import { UserPersonalIdentities, } from '$lib/server/database'; import { enqueueJob } from '$lib/server/jobs'; -import { getSpaceMember, Loader, makeMasquerade } from '$lib/server/utils'; +import { getSpaceMember, Loader, makeMasquerade, useFirstRowOrThrow } from '$lib/server/utils'; import { builder } from '../builder'; import { createObjectRef } from '../utils'; import { Post } from './post'; @@ -369,10 +369,8 @@ builder.mutationFields((t) => ({ } } - let notificationTargetUserId; - if (input.parentId) { - const rows = await database + await database .select() .from(PostComments) .where( @@ -381,15 +379,8 @@ builder.mutationFields((t) => ({ eq(PostComments.postId, input.postId), eq(PostComments.state, 'ACTIVE'), ), - ); - - if (rows.length === 0) { - throw new NotFoundError(); - } - - notificationTargetUserId = rows[0].userId; - } else { - notificationTargetUserId = post.userId; + ) + .then(useFirstRowOrThrow(new NotFoundError())); } let profileId: string; @@ -412,32 +403,24 @@ builder.mutationFields((t) => ({ profileId = masquerade.profileId; } - const commentId = await database.transaction(async (tx) => { - const [comment] = await tx - .insert(PostComments) - .values({ - postId: input.postId, - userId: context.session.userId, - profileId, - parentId: input.parentId, - content: input.content, - visibility: input.visibility, - state: 'ACTIVE', - }) - .returning({ id: PostComments.id }); - - return comment.id; - }); + const commentId = await database + .insert(PostComments) + .values({ + postId: input.postId, + userId: context.session.userId, + profileId, + parentId: input.parentId, + content: input.content, + visibility: input.visibility, + state: 'ACTIVE', + }) + .returning({ id: PostComments.id }) + .then((rows) => rows[0].id); - if (notificationTargetUserId !== context.session.userId) { - await enqueueJob('createNotification', { - userId: notificationTargetUserId, - category: 'COMMENT', - actorId: profileId, - data: { commentId }, - origin: context.event.url.origin, - }); - } + await enqueueJob('createNotification', { + category: 'COMMENT', + targetId: commentId, + }); return commentId; }, diff --git a/apps/website/src/lib/server/graphql/schemas/post.ts b/apps/website/src/lib/server/graphql/schemas/post.ts index d489aa6e14..ce7bf1d135 100644 --- a/apps/website/src/lib/server/graphql/schemas/post.ts +++ b/apps/website/src/lib/server/graphql/schemas/post.ts @@ -62,7 +62,6 @@ import { getPostContentState, getPostViewCount, getSpaceMember, - makeMasquerade, makePostContentId, makeQueryContainers, searchResultToIds, @@ -1862,7 +1861,7 @@ builder.mutationFields((t) => ({ throw new IntentionalError('이미 구매한 포스트예요'); } - await database.transaction(async (tx) => { + const purchaseId = await database.transaction(async (tx) => { const [purchase] = await tx .insert(PostPurchases) .values({ @@ -1888,19 +1887,13 @@ builder.mutationFields((t) => ({ kind: 'POST_PURCHASE', state: 'PENDING', }); - }); - const masquerade = await makeMasquerade({ - userId: context.session.userId, - spaceId: post.space.id, + return purchase.id; }); await enqueueJob('createNotification', { - userId: post.userId, category: 'PURCHASE', - actorId: masquerade.profileId, - data: { postId: input.postId }, - origin: context.event.url.origin, + targetId: purchaseId, }); return input.postId; @@ -1961,29 +1954,21 @@ builder.mutationFields((t) => ({ throw new IntentionalError('피드백을 받지 않는 포스트예요'); } - await database + const reaction = await database .insert(PostReactions) .values({ userId: context.session.userId, postId: input.postId, emoji: input.emoji, }) - .onConflictDoNothing(); - - if (post.userId !== context.session.userId && post.spaceId) { - const masquerade = await makeMasquerade({ - userId: context.session.userId, - spaceId: post.spaceId, - }); + .onConflictDoNothing() + .returning({ id: PostReactions.id }) + .then(useFirstRowOrThrow()); - await enqueueJob('createNotification', { - userId: post.userId, - category: 'EMOJI_REACTION', - actorId: masquerade.profileId, - data: { postId: input.postId, emoji: input.emoji }, - origin: context.event.url.origin, - }); - } + await enqueueJob('createNotification', { + category: 'EMOJI_REACTION', + targetId: reaction.id, + }); return input.postId; }, diff --git a/apps/website/src/lib/server/graphql/schemas/space.ts b/apps/website/src/lib/server/graphql/schemas/space.ts index 6d39468a1b..24076a71c3 100644 --- a/apps/website/src/lib/server/graphql/schemas/space.ts +++ b/apps/website/src/lib/server/graphql/schemas/space.ts @@ -799,33 +799,18 @@ builder.mutationFields((t) => ({ } } - await database + const spaceFollowId = await database .insert(SpaceFollows) .values({ userId: context.session.userId, spaceId: input.spaceId }) - .onConflictDoNothing(); + .onConflictDoNothing() + .returning({ id: SpaceFollows.id }) + .then((rows) => rows[0]?.id); - const masquerade = await makeMasquerade({ - spaceId: input.spaceId, - userId: context.session.userId, + enqueueJob('createNotification', { + category: 'SUBSCRIBE', + targetId: spaceFollowId, }); - const members = await database - .select({ userId: SpaceMembers.userId }) - .from(SpaceMembers) - .where(and(eq(SpaceMembers.spaceId, input.spaceId), eq(SpaceMembers.state, 'ACTIVE'))); - - await Promise.all( - members.map((member) => - enqueueJob('createNotification', { - userId: member.userId, - category: 'SUBSCRIBE', - actorId: masquerade.profileId, - data: { spaceId: input.spaceId }, - origin: context.event.url.origin, - }), - ), - ); - return input.spaceId; }, }), diff --git a/apps/website/src/lib/server/jobs/notification.ts b/apps/website/src/lib/server/jobs/notification.ts index 1396012dc6..17e4df68e3 100644 --- a/apps/website/src/lib/server/jobs/notification.ts +++ b/apps/website/src/lib/server/jobs/notification.ts @@ -1,157 +1,15 @@ -import { eq } from 'drizzle-orm'; -import { match } from 'ts-pattern'; -import { database, PostComments, PostRevisions, Posts, Profiles, Spaces, UserNotifications } from '../database'; -import { firebase } from '../external-api'; -import { checkNotificationPreferences, useFirstRowOrThrow } from '../utils'; +import { notificationMaker } from '$lib/notification/maker'; import { defineJob } from './types'; -import type { Notification } from '$lib/utils'; -type CreateNotificationParams = Notification & { - origin: string; +type CreateNotificationParams = { + category: keyof typeof notificationMaker; + targetId: string; }; -export const createNotificationJob = defineJob('createNotification', async (params: CreateNotificationParams) => { - const { userId, category, actorId, data } = params; - const preferences = await checkNotificationPreferences({ userId, category }); - - if (preferences.WEBSITE) { - await database.insert(UserNotifications).values({ - userId, - category, - actorId, - data, - state: 'UNREAD', - }); - } - - const actorProfile = await database - .select() - .from(Profiles) - .where(eq(Profiles.id, actorId)) - .then(useFirstRowOrThrow()); - - await firebase.sendPushNotification({ - userId, - ...(await match(params) - .with({ category: 'COMMENT' }, async ({ data }) => { - const notificationData = await database - .select({ - slug: Spaces.slug, - permalink: Posts.permalink, - title: PostRevisions.title, - content: PostComments.content, - }) - .from(PostComments) - .innerJoin(Posts, eq(PostComments.postId, Posts.id)) - .innerJoin(PostRevisions, eq(Posts.publishedRevisionId, PostRevisions.id)) - .innerJoin(Spaces, eq(Posts.spaceId, Spaces.id)) - .where(eq(PostComments.id, data.commentId)) - .then(useFirstRowOrThrow()); - - return { - title: notificationData.title ?? '(제목 없음)', - body: `${actorProfile.name}님이 "${notificationData.content}" 댓글을 남겼어요.`, - path: `/${notificationData.slug}/${notificationData.permalink}`, - }; - }) - .with({ category: 'EMOJI_REACTION' }, async ({ data }) => { - const notificationData = await database - .select({ slug: Spaces.slug, permalink: Posts.permalink, title: PostRevisions.title }) - .from(Posts) - .innerJoin(PostRevisions, eq(Posts.publishedRevisionId, PostRevisions.id)) - .innerJoin(Spaces, eq(Posts.spaceId, Spaces.id)) - .where(eq(Posts.id, data.postId)) - .then(useFirstRowOrThrow()); - - return { - title: notificationData.title ?? '(제목 없음)', - body: `${actorProfile.name}님이 이모지를 남겼어요.`, - path: `/${notificationData.slug}/${notificationData.permalink}`, - }; - }) - .with({ category: 'PURCHASE' }, async ({ data }) => { - const notificationData = await database - .select({ title: PostRevisions.title }) - .from(Posts) - .innerJoin(PostRevisions, eq(Posts.publishedRevisionId, PostRevisions.id)) - .where(eq(Posts.id, data.postId)) - .then(useFirstRowOrThrow()); - - return { - title: notificationData.title ?? '(제목 없음)', - body: `${actorProfile.name}님이 포스트를 구매했어요.`, - path: '/me/revenue', - }; - }) - .with({ category: 'SUBSCRIBE' }, async ({ data }) => { - const notificationData = await database - .select({ slug: Spaces.slug, name: Spaces.name }) - .from(Spaces) - .where(eq(Spaces.id, data.spaceId)) - .then(useFirstRowOrThrow()); - - return { - title: notificationData.name, - body: `${actorProfile.name}님이 스페이스를 구독했어요.`, - path: `/${notificationData.slug}`, - }; - }) - .otherwise(() => { - return { - title: '알림이 도착했어요.', - body: '앱에서 자세한 내용을 확인해보세요.', - path: '/me/notifications', - }; - })), - }); - - // 이메일 알림 일단 비활성화 - // if (preferences.EMAIL) { - // const user = await db.user.findUniqueOrThrow({ - // where: { id: userId }, - // }); - - // const actor = actorId - // ? await db.profile.findUniqueOrThrow({ - // where: { id: actorId }, - // }) - // : null; - - // switch (category) { - // case 'SUBSCRIBE': { - // const space = await db.space.findUniqueOrThrow({ - // where: { id: data?.spaceId as string }, - // }); - - // await sendEmail({ - // subject: `[펜슬] ${actor?.name}님이 ${space.name} 스페이스를 관심 추가했어요`, - // recipient: user.email, - // template: Subscribe, - // props: { - // profileName: actor?.name as string, - // spaceName: space.name as string, - // spaceURL: `${origin}/${space.slug}`, - // }, - // }); - // break; - // } - // case 'PURCHASE': { - // const post = await db.post.findUniqueOrThrow({ - // where: { id: data?.postId as string }, - // include: { publishedRevision: true }, - // }); - - // await sendEmail({ - // subject: `[펜슬] ${actor?.name}님이 ${post.publishedRevision?.title} 포스트를 구매했어요`, - // recipient: user.email, - // template: Purchase, - // props: { - // profileName: actor?.name as string, - // postTitle: post.publishedRevision?.title as string, - // postURL: `https://pnxl.me/${BigInt(post.permalink).toString(36)}`, - // }, - // }); - // } - // } - // } -}); +export const createNotificationJob = defineJob( + 'createNotification', + async ({ category, targetId }: CreateNotificationParams) => { + const maker = notificationMaker[category]; + await maker(targetId); + }, +); diff --git a/apps/website/src/lib/server/utils/notification.ts b/apps/website/src/lib/server/utils/notification.ts index 93ba78de21..51b1c47d6f 100644 --- a/apps/website/src/lib/server/utils/notification.ts +++ b/apps/website/src/lib/server/utils/notification.ts @@ -1,7 +1,8 @@ import { and, eq } from 'drizzle-orm'; import * as R from 'radash'; import { UserNotificationCategory, UserNotificationMethod } from '$lib/enums'; -import { database, inArray, UserNotificationPreferences } from '../database'; +import { database, inArray, UserNotificationPreferences, UserNotifications } from '../database'; +import { firebase } from '../external-api'; type CheckNotificationPreferencesParams = { userId: string; @@ -36,3 +37,34 @@ export const checkNotificationPreferences = async ({ true, ); }; + +type CreateNotificationParams = { + userId: string; + category: keyof typeof UserNotificationCategory; + actorId: string; + data: Record; + pushTitle: string; + pushBody: string; + pushPath?: string; +}; + +export const createNotification = async (params: CreateNotificationParams) => { + const preferences = await checkNotificationPreferences({ userId: params.userId, category: params.category }); + + if (preferences.WEBSITE) { + await database.insert(UserNotifications).values({ + state: 'UNREAD', + userId: params.userId, + category: params.category, + actorId: params.actorId, + data: params.data, + }); + + await firebase.sendPushNotification({ + userId: params.userId, + title: params.pushTitle, + body: params.pushBody, + path: params.pushPath, + }); + } +}; diff --git a/apps/website/src/lib/server/utils/space.ts b/apps/website/src/lib/server/utils/space.ts index 007ac74494..c2d551df3a 100644 --- a/apps/website/src/lib/server/utils/space.ts +++ b/apps/website/src/lib/server/utils/space.ts @@ -1,5 +1,8 @@ import { and, eq } from 'drizzle-orm'; -import { database, inArray, SpaceMembers } from '../database'; +import { database, inArray, Profiles, SpaceMembers } from '../database'; +import { Profile } from '../graphql/schemas/user'; +import { useFirstRow, useFirstRowOrThrow } from './database'; +import { makeMasquerade } from './masquerade'; import type { Context } from '../context'; export const getSpaceMember = async (context: Context, spaceId: string | null | undefined) => { @@ -32,3 +35,68 @@ export const getSpaceMember = async (context: Context, spaceId: string | null | return await loader.load(spaceId); }; + +type GetSpaceMemberParams = { + context?: Context; + spaceId: string; + userId?: string; +}; + +export const getSpaceMemberV2 = async ({ context, spaceId, ...params }: GetSpaceMemberParams) => { + const userId = params.userId ?? context?.session?.userId; + + if (!userId) { + return null; + } + + if (context) { + return await context + .loader({ + name: 'spaceMember(spaceId)', + nullable: true, + load: async (spaceIds: string[]) => { + return await database + .select({ SpaceMembers }) + .from(SpaceMembers) + .where( + and( + inArray(SpaceMembers.spaceId, spaceIds), + eq(SpaceMembers.userId, userId), + eq(SpaceMembers.state, 'ACTIVE'), + ), + ) + .then((rows) => rows.map((row) => row.SpaceMembers)); + }, + key: (row) => row?.spaceId, + }) + .load(spaceId); + } else { + return await database + .select() + .from(SpaceMembers) + .where(and(eq(SpaceMembers.spaceId, spaceId), eq(SpaceMembers.userId, userId), eq(SpaceMembers.state, 'ACTIVE'))) + .then(useFirstRow); + } +}; + +type GetSpaceProfileParams = + | { + context: Context; + spaceId: string; + userId?: string; + } + | { + spaceId: string; + userId: string; + }; + +export const getSpaceProfile = async (params: GetSpaceProfileParams) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const userId = 'context' in params ? params.userId ?? params.context.session!.userId : params.userId; + + const { profileId } = (await getSpaceMemberV2(params)) ?? (await makeMasquerade({ spaceId: params.spaceId, userId })); + + return await ('context' in params + ? Profile.getDataloader(params.context).load(profileId) + : database.select().from(Profiles).where(eq(Profiles.id, profileId)).then(useFirstRowOrThrow())); +};