diff --git a/apps/app/src/components/Onboarding/shared/OrganizationFormFields.tsx b/apps/app/src/components/Onboarding/shared/OrganizationFormFields.tsx
index 64ea1dc09..2693fbb21 100644
--- a/apps/app/src/components/Onboarding/shared/OrganizationFormFields.tsx
+++ b/apps/app/src/components/Onboarding/shared/OrganizationFormFields.tsx
@@ -94,7 +94,10 @@ export const OrganizationFormFields = ({
if (!acceptedTypes.includes(file.type)) {
const types = acceptedTypes.map((t) => t.split('/')[1]).join(', ');
toast.error({
- message: t('That file type is not supported. Accepted types: {types}', { types }),
+ message: t(
+ 'That file type is not supported. Accepted types: {types}',
+ { types },
+ ),
});
return;
}
@@ -102,7 +105,9 @@ export const OrganizationFormFields = ({
if (file.size > DEFAULT_MAX_SIZE) {
const maxSizeMB = (DEFAULT_MAX_SIZE / 1024 / 1024).toFixed(2);
toast.error({
- message: t('File too large. Maximum size: {maxSizeMB}MB', { maxSizeMB }),
+ message: t('File too large. Maximum size: {maxSizeMB}MB', {
+ maxSizeMB,
+ }),
});
return;
}
@@ -128,7 +133,6 @@ export const OrganizationFormFields = ({
<>
handleImageUpload(file, setBannerImage, uploadImage)
diff --git a/apps/app/src/components/Profile/ProfileDetails/UpdateOrganizationForm.tsx b/apps/app/src/components/Profile/ProfileDetails/UpdateOrganizationForm.tsx
index 544153c23..dd11e078f 100644
--- a/apps/app/src/components/Profile/ProfileDetails/UpdateOrganizationForm.tsx
+++ b/apps/app/src/components/Profile/ProfileDetails/UpdateOrganizationForm.tsx
@@ -2,6 +2,7 @@
import { DEFAULT_MAX_SIZE } from '@/hooks/useFileUpload';
import { getPublicUrl } from '@/utils';
+import { analyzeError, useConnectionStatus } from '@/utils/connectionErrors';
import { trpc } from '@op/api/client';
import type { Organization } from '@op/api/encoders';
import { AvatarUploader } from '@op/ui/AvatarUploader';
@@ -17,7 +18,6 @@ import { forwardRef, useState } from 'react';
import { LuLink } from 'react-icons/lu';
import { useTranslations } from '@/lib/i18n';
-import { analyzeError, useConnectionStatus } from '@/utils/connectionErrors';
import { GeoNamesMultiSelect } from '../../GeoNamesMultiSelect';
import { type ImageData } from '../../Onboarding/shared/OrganizationFormFields';
@@ -166,13 +166,13 @@ export const UpdateOrganizationForm = forwardRef<
slug: profile.profile.slug,
});
router.refresh();
-
+
onSuccess();
} catch (error) {
console.error('Failed to update organization:', error);
-
+
const errorInfo = analyzeError(error);
-
+
if (errorInfo.isConnectionError) {
toast.error({
title: 'Connection issue',
@@ -260,7 +260,6 @@ export const UpdateOrganizationForm = forwardRef<
{/* Header Images */}
handleImageUpload(file, setBannerImage, uploadImage)
diff --git a/packages/common/src/services/organization/getOrganizationsByProfile.ts b/packages/common/src/services/organization/getOrganizationsByProfile.ts
new file mode 100644
index 000000000..69dc2c168
--- /dev/null
+++ b/packages/common/src/services/organization/getOrganizationsByProfile.ts
@@ -0,0 +1,72 @@
+import { db, sql } from '@op/db/client';
+import { locations } from '@op/db/schema';
+
+export const getOrganizationsByProfile = async (profileId: string) => {
+ // Find all users who have access to this profile
+ // Either as their personal profile or as their current profile
+ const usersWithProfile = await db.query.users.findMany({
+ where: (table, { eq, or }) =>
+ or(
+ eq(table.profileId, profileId),
+ eq(table.currentProfileId, profileId),
+ ),
+ with: {
+ organizationUsers: {
+ with: {
+ organization: {
+ with: {
+ projects: true,
+ links: true,
+ profile: {
+ with: {
+ headerImage: true,
+ avatarImage: true,
+ },
+ },
+ whereWeWork: {
+ with: {
+ location: {
+ extras: {
+ x: sql`ST_X(${locations.location})`.as('x'),
+ y: sql`ST_Y(${locations.location})`.as('y'),
+ },
+ columns: {
+ id: true,
+ name: true,
+ placeId: true,
+ countryCode: true,
+ countryName: true,
+ metadata: true,
+ latLng: false,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ // Collect all unique organizations
+ const organizationMap = new Map();
+
+ for (const user of usersWithProfile) {
+ for (const orgUser of user.organizationUsers) {
+ if (orgUser.organization) {
+ const org = orgUser.organization;
+
+ // Transform whereWeWork to match expected format
+ const transformedOrg = {
+ ...org,
+ whereWeWork: org.whereWeWork.map((item: any) => item.location),
+ };
+
+ organizationMap.set(org.id, transformedOrg);
+ }
+ }
+ }
+
+ return Array.from(organizationMap.values());
+};
\ No newline at end of file
diff --git a/packages/common/src/services/organization/index.ts b/packages/common/src/services/organization/index.ts
index dfeade4dd..a6b1aa552 100644
--- a/packages/common/src/services/organization/index.ts
+++ b/packages/common/src/services/organization/index.ts
@@ -9,3 +9,5 @@ export * from './searchOrganizations';
export * from './matchingDomainOrganizations';
export * from './joinOrganization';
export * from './validators';
+export * from './inviteUsers';
+export * from './getOrganizationsByProfile';
diff --git a/packages/common/src/services/organization/inviteUsers.ts b/packages/common/src/services/organization/inviteUsers.ts
new file mode 100644
index 000000000..82a732c58
--- /dev/null
+++ b/packages/common/src/services/organization/inviteUsers.ts
@@ -0,0 +1,144 @@
+import { db } from '@op/db/client';
+import { allowList } from '@op/db/schema';
+import { sendInvitationEmail } from '../email';
+import { OPURLConfig } from '@op/core';
+
+export interface InviteUsersInput {
+ emails: string[];
+ role?: string;
+ organizationId?: string;
+ personalMessage?: string;
+ authUserId: string;
+ authUserEmail?: string;
+}
+
+export interface InviteResult {
+ success: boolean;
+ message: string;
+ details: {
+ successful: string[];
+ failed: { email: string; reason: string }[];
+ };
+}
+
+export const inviteUsersToOrganization = async (input: InviteUsersInput): Promise => {
+ const { emails, role = 'Admin', organizationId, personalMessage, authUserId, authUserEmail } = input;
+
+ // Get the current user's database record with organization details
+ const authUser = await db.query.users.findFirst({
+ where: (table, { eq }) => eq(table.authUserId, authUserId),
+ with: {
+ currentOrganization: {
+ with: {
+ profile: true,
+ },
+ },
+ currentProfile: true,
+ },
+ });
+
+ // For new organization invites, we don't need the user to be in an organization
+ // For existing organization invites, we do need it
+ if (
+ (!authUser?.currentProfileId && !authUser?.lastOrgId) ||
+ (!authUser.currentOrganization && !authUser.currentProfile)
+ ) {
+ throw new Error('User must be associated with an organization to send invites');
+ }
+
+ const currentProfile =
+ authUser.currentProfile ??
+ (authUser.currentOrganization as any)?.profile;
+
+ const results = {
+ successful: [] as string[],
+ failed: [] as { email: string; reason: string }[],
+ };
+
+ // Process each email
+ for (const rawEmail of emails) {
+ const email = rawEmail.toLowerCase();
+ try {
+ // Check if email is already in the allowList
+ const existingEntry = await db.query.allowList.findFirst({
+ where: (table, { eq }) => eq(table.email, email),
+ });
+
+ if (!existingEntry) {
+ // Determine metadata based on whether it's a new organization invite
+ const metadata = organizationId
+ ? {
+ invitedBy: authUserId,
+ invitedAt: new Date().toISOString(),
+ inviteType: 'new_organization',
+ personalMessage: personalMessage,
+ inviterOrganizationName:
+ (currentProfile as any)?.profile?.name || 'Common',
+ }
+ : {
+ invitedBy: authUserId,
+ invitedAt: new Date().toISOString(),
+ personalMessage: personalMessage,
+ role,
+ };
+
+ // Add the email to the allowList
+ await db.insert(allowList).values({
+ email,
+ organizationId: organizationId ?? null,
+ metadata,
+ });
+ }
+
+ // Send invitation email
+ try {
+ await sendInvitationEmail({
+ to: email,
+ inviterName:
+ authUser?.name || authUserEmail || 'A team member',
+ organizationName: organizationId
+ ? (authUser?.currentOrganization as any)?.profile?.name ||
+ 'an organization'
+ : undefined,
+ inviteUrl: OPURLConfig('APP').ENV_URL,
+ message: personalMessage,
+ });
+ results.successful.push(email);
+ } catch (emailError) {
+ console.error(
+ `Failed to send invitation email to ${email}:`,
+ emailError,
+ );
+ // Email failed but database insertion succeeded
+ results.successful.push(email);
+ }
+ } catch (error) {
+ console.error(`Failed to process invitation for ${email}:`, error);
+ results.failed.push({
+ email,
+ reason: error instanceof Error ? error.message : 'Unknown error',
+ });
+ }
+ }
+
+ const totalEmails = emails.length;
+ const successCount = results.successful.length;
+
+ let message: string;
+ if (successCount === totalEmails) {
+ message = `All ${totalEmails} invitation${totalEmails > 1 ? 's' : ''} sent successfully`;
+ } else if (successCount > 0) {
+ message = `${successCount} of ${totalEmails} invitations sent successfully`;
+ } else {
+ message = 'No invitations were sent successfully';
+ }
+
+ return {
+ success: successCount > 0,
+ message,
+ details: {
+ successful: results.successful,
+ failed: results.failed,
+ },
+ };
+};
\ No newline at end of file
diff --git a/packages/common/src/services/posts/createPostInOrganization.ts b/packages/common/src/services/posts/createPostInOrganization.ts
new file mode 100644
index 000000000..fac095049
--- /dev/null
+++ b/packages/common/src/services/posts/createPostInOrganization.ts
@@ -0,0 +1,98 @@
+import { db } from '@op/db/client';
+import { attachments, posts, postsToOrganizations } from '@op/db/schema';
+import type { User } from '@supabase/supabase-js';
+import { TRPCError } from '@trpc/server';
+
+import { getOrgAccessUser } from '../';
+import { UnauthorizedError } from '../../utils/error';
+
+export interface CreatePostInOrganizationOptions {
+ id: string;
+ content: string;
+ attachmentIds?: string[];
+ user: User;
+}
+
+export const createPostInOrganization = async (
+ options: CreatePostInOrganizationOptions,
+) => {
+ const { id, content, attachmentIds = [], user } = options;
+
+ const orgUser = await getOrgAccessUser({
+ organizationId: id,
+ user,
+ });
+
+ if (!orgUser) {
+ throw new UnauthorizedError();
+ }
+
+ try {
+ // Get all storage objects that were attached to the post
+ const allStorageObjects =
+ attachmentIds.length > 0
+ ? await db.query.objectsInStorage.findMany({
+ where: (table, { inArray }) => inArray(table.id, attachmentIds),
+ })
+ : [];
+
+ const [post] = await db
+ .insert(posts)
+ .values({
+ content,
+ })
+ .returning();
+
+ if (!post) {
+ throw new TRPCError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: 'Failed to add post to organization',
+ });
+ }
+
+ // Create the join record associating the post with the organization
+ const queryPromises: Promise[] = [
+ db.insert(postsToOrganizations).values({
+ organizationId: id,
+ postId: post.id,
+ }),
+ ];
+
+ // Create attachment records if any attachments were uploaded
+ if (allStorageObjects.length > 0) {
+ const attachmentValues = allStorageObjects.map((storageObject) => ({
+ postId: post.id,
+ storageObjectId: storageObject.id,
+ uploadedBy: orgUser.id,
+ fileName:
+ storageObject?.name
+ ?.split('/')
+ .slice(-1)[0]
+ ?.split('_')
+ .slice(1)
+ .join('_') ?? '',
+ mimeType: (storageObject.metadata as { mimetype: string }).mimetype,
+ }));
+
+ queryPromises.push(db.insert(attachments).values(attachmentValues));
+ }
+
+ // Run attachments and join record in parallel
+ await Promise.all(queryPromises);
+
+ return {
+ result: {
+ ...post,
+ reactionCounts: {},
+ userReactions: [],
+ },
+ allStorageObjects,
+ };
+ } catch (error) {
+ console.log('ERROR', error);
+ throw new TRPCError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: 'Something went wrong when adding post to organization',
+ });
+ }
+};
diff --git a/packages/common/src/services/posts/deletePost.ts b/packages/common/src/services/posts/deletePost.ts
new file mode 100644
index 000000000..3bac691a7
--- /dev/null
+++ b/packages/common/src/services/posts/deletePost.ts
@@ -0,0 +1,34 @@
+import { db, eq, and } from '@op/db/client';
+import { posts, postsToOrganizations } from '@op/db/schema';
+
+export interface DeletePostByIdOptions {
+ postId: string;
+ organizationId: string;
+}
+
+export const deletePostById = async (options: DeletePostByIdOptions) => {
+ const { postId, organizationId } = options;
+ // Verify the post exists and belongs to the organization
+ const postExists = await db
+ .select()
+ .from(posts)
+ .innerJoin(
+ postsToOrganizations,
+ eq(posts.id, postsToOrganizations.postId),
+ )
+ .where(
+ and(
+ eq(posts.id, postId),
+ eq(postsToOrganizations.organizationId, organizationId),
+ ),
+ )
+ .limit(1);
+
+ if (!postExists.length) {
+ throw new Error('Post not found or does not belong to the specified organization');
+ }
+
+ await db.delete(posts).where(eq(posts.id, postId));
+
+ return { success: true };
+};
\ No newline at end of file
diff --git a/packages/common/src/services/posts/index.ts b/packages/common/src/services/posts/index.ts
index c7efef45a..4024d47d0 100644
--- a/packages/common/src/services/posts/index.ts
+++ b/packages/common/src/services/posts/index.ts
@@ -1,3 +1,6 @@
export * from './listPosts';
export * from './getPosts';
export * from './createPost';
+export * from './deletePost';
+export * from './listRelatedOrganizationPosts';
+export * from './createPostInOrganization';
diff --git a/packages/common/src/services/posts/listRelatedOrganizationPosts.ts b/packages/common/src/services/posts/listRelatedOrganizationPosts.ts
new file mode 100644
index 000000000..f33484630
--- /dev/null
+++ b/packages/common/src/services/posts/listRelatedOrganizationPosts.ts
@@ -0,0 +1,135 @@
+import {
+ getCurrentProfileId,
+ getItemsWithReactionsAndComments,
+ getRelatedOrganizations,
+} from '../';
+import { decodeCursor, encodeCursor } from '../../utils';
+import { and, eq, inArray, lt, or, db } from '@op/db/client';
+import { postsToOrganizations } from '@op/db/schema';
+import type { User } from '@supabase/supabase-js';
+
+export interface ListAllPostsOptions {
+ limit?: number;
+ cursor?: string | null;
+}
+
+export interface ListRelatedPostsOptions {
+ organizationId: string;
+ user: User;
+}
+
+export const listAllRelatedOrganizationPosts = async (options: ListAllPostsOptions = {}) => {
+ const { limit = 200, cursor } = options;
+
+ // Parse cursor
+ const cursorData = cursor ? decodeCursor(cursor) : null;
+
+ // Build cursor condition for pagination
+ const cursorCondition = cursorData
+ ? or(
+ lt(postsToOrganizations.createdAt, cursorData.createdAt),
+ and(
+ eq(postsToOrganizations.createdAt, cursorData.createdAt),
+ lt(postsToOrganizations.postId, cursorData.id),
+ ),
+ )
+ : undefined;
+
+ // Fetch posts for all organizations with pagination
+ const [result, profileId] = await Promise.all([
+ db.query.postsToOrganizations.findMany({
+ where: cursorCondition,
+ with: {
+ post: {
+ where: (table, { isNull }) => isNull(table.parentPostId), // Only show top-level posts
+ with: {
+ attachments: {
+ with: {
+ storageObject: true,
+ },
+ },
+ reactions: true,
+ },
+ },
+ organization: {
+ with: {
+ profile: {
+ with: {
+ avatarImage: true,
+ },
+ },
+ },
+ },
+ },
+ orderBy: (table, { desc }) => desc(table.createdAt),
+ limit: limit + 1, // Fetch one extra to check hasMore
+ }),
+ getCurrentProfileId(),
+ ]);
+
+ // Filter out any items where post is null (due to parentPostId filtering)
+ const filteredResult = result.filter((item) => item.post !== null);
+
+ const hasMore = filteredResult.length > limit;
+ const items = hasMore ? filteredResult.slice(0, limit) : filteredResult;
+ const lastItem = items[items.length - 1];
+ const nextCursor =
+ hasMore && lastItem && lastItem.createdAt
+ ? encodeCursor(new Date(lastItem.createdAt), lastItem.postId)
+ : null;
+
+ const itemsWithReactionsAndComments =
+ await getItemsWithReactionsAndComments({ items, profileId });
+
+ return {
+ items: itemsWithReactionsAndComments,
+ next: nextCursor,
+ hasMore,
+ };
+};
+
+export const listRelatedOrganizationPosts = async (options: ListRelatedPostsOptions) => {
+ const { organizationId, user } = options;
+
+ // Get related organizations
+ const { records: organizations } = await getRelatedOrganizations({
+ user,
+ orgId: organizationId,
+ pending: false,
+ });
+
+ const orgIds = organizations?.map((org: any) => org.id) ?? [];
+ orgIds.push(organizationId); // Add our own org so we see our own posts
+
+ // Fetch posts for all related organizations
+ const result = await db.query.postsToOrganizations.findMany({
+ where: () => inArray(postsToOrganizations.organizationId, orgIds),
+ with: {
+ post: {
+ where: (table, { isNull }) => isNull(table.parentPostId), // Only show top-level posts
+ with: {
+ attachments: {
+ with: {
+ storageObject: true,
+ },
+ },
+ },
+ },
+ organization: {
+ with: {
+ profile: {
+ with: {
+ avatarImage: true,
+ },
+ },
+ },
+ },
+ },
+ orderBy: (table, { desc }) => desc(table.createdAt),
+ });
+
+ // Filter out any items where post is null (due to parentPostId filtering)
+ const filteredResult = result.filter((item) => item.post !== null);
+
+ return filteredResult;
+};
diff --git a/packages/common/src/services/reactions/addReaction.ts b/packages/common/src/services/reactions/addReaction.ts
new file mode 100644
index 000000000..18942ae6c
--- /dev/null
+++ b/packages/common/src/services/reactions/addReaction.ts
@@ -0,0 +1,30 @@
+import { and, db, eq } from '@op/db/client';
+import { postReactions } from '@op/db/schema';
+
+export interface AddReactionOptions {
+ postId: string;
+ profileId: string;
+ reactionType: string;
+}
+
+export const addReaction = async (options: AddReactionOptions) => {
+ const { postId, profileId, reactionType } = options;
+ await db.transaction(async (tx) => {
+ // First, remove any existing reaction from this user on this post
+ await tx
+ .delete(postReactions)
+ .where(
+ and(
+ eq(postReactions.postId, postId),
+ eq(postReactions.profileId, profileId),
+ ),
+ );
+
+ // Then add the new reaction
+ await tx.insert(postReactions).values({
+ postId,
+ profileId,
+ reactionType,
+ });
+ });
+};
diff --git a/packages/common/src/services/reactions/getExistingReaction.ts b/packages/common/src/services/reactions/getExistingReaction.ts
new file mode 100644
index 000000000..2657cb634
--- /dev/null
+++ b/packages/common/src/services/reactions/getExistingReaction.ts
@@ -0,0 +1,23 @@
+import { and, db, eq } from '@op/db/client';
+import { postReactions } from '@op/db/schema';
+
+export interface GetExistingReactionOptions {
+ postId: string;
+ profileId: string;
+}
+
+export const getExistingReaction = async (options: GetExistingReactionOptions) => {
+ const { postId, profileId } = options;
+ const existingReaction = await db
+ .select()
+ .from(postReactions)
+ .where(
+ and(
+ eq(postReactions.postId, postId),
+ eq(postReactions.profileId, profileId),
+ ),
+ )
+ .limit(1);
+
+ return existingReaction[0] || null;
+};
diff --git a/packages/common/src/services/reactions/index.ts b/packages/common/src/services/reactions/index.ts
index 37cdcb547..7fb48b20c 100644
--- a/packages/common/src/services/reactions/index.ts
+++ b/packages/common/src/services/reactions/index.ts
@@ -1 +1,5 @@
-export * from './validateReaction';
\ No newline at end of file
+export * from './validateReaction';
+export * from './addReaction';
+export * from './removeReaction';
+export * from './getExistingReaction';
+export * from './toggleReaction';
diff --git a/packages/common/src/services/reactions/removeReaction.ts b/packages/common/src/services/reactions/removeReaction.ts
new file mode 100644
index 000000000..5ac677f31
--- /dev/null
+++ b/packages/common/src/services/reactions/removeReaction.ts
@@ -0,0 +1,19 @@
+import { and, db, eq } from '@op/db/client';
+import { postReactions } from '@op/db/schema';
+
+export interface RemoveReactionOptions {
+ postId: string;
+ profileId: string;
+}
+
+export const removeReaction = async (options: RemoveReactionOptions) => {
+ const { postId, profileId } = options;
+ await db
+ .delete(postReactions)
+ .where(
+ and(
+ eq(postReactions.postId, postId),
+ eq(postReactions.profileId, profileId),
+ ),
+ );
+};
diff --git a/packages/common/src/services/reactions/toggleReaction.ts b/packages/common/src/services/reactions/toggleReaction.ts
new file mode 100644
index 000000000..69a99ca9b
--- /dev/null
+++ b/packages/common/src/services/reactions/toggleReaction.ts
@@ -0,0 +1,31 @@
+import { addReaction } from './addReaction';
+import { getExistingReaction } from './getExistingReaction';
+import { removeReaction } from './removeReaction';
+
+export interface ToggleReactionOptions {
+ postId: string;
+ profileId: string;
+ reactionType: string;
+}
+
+export const toggleReaction = async (options: ToggleReactionOptions) => {
+ const { postId, profileId, reactionType } = options;
+
+ const existingReaction = await getExistingReaction({ postId, profileId });
+
+ if (existingReaction) {
+ // If user has the same reaction type, remove it
+ if (existingReaction.reactionType === reactionType) {
+ await removeReaction({ postId, profileId });
+ return { success: true, action: 'removed' as const };
+ } else {
+ // If user has a different reaction type, replace it
+ await addReaction({ postId, profileId, reactionType });
+ return { success: true, action: 'replaced' as const };
+ }
+ } else {
+ // No existing reaction, add new one
+ await addReaction({ postId, profileId, reactionType });
+ return { success: true, action: 'added' as const };
+ }
+};
diff --git a/packages/common/src/services/reactions/validateReaction.ts b/packages/common/src/services/reactions/validateReaction.ts
index e58cf6e96..61d2f1ef9 100644
--- a/packages/common/src/services/reactions/validateReaction.ts
+++ b/packages/common/src/services/reactions/validateReaction.ts
@@ -36,4 +36,4 @@ export function validateReactionEmoji(emoji: string): ReactionValidationResult {
return {
isValid: true,
};
-}
\ No newline at end of file
+}
diff --git a/packages/common/src/services/user/index.ts b/packages/common/src/services/user/index.ts
index cf4ce3645..3875cfd91 100644
--- a/packages/common/src/services/user/index.ts
+++ b/packages/common/src/services/user/index.ts
@@ -1,5 +1,5 @@
-import { db, eq } from '@op/db/client';
-import { allowList, users } from '@op/db/schema';
+import { db, eq, sql, and } from '@op/db/client';
+import { allowList, users, organizationUsers, usersUsedStorage, organizations } from '@op/db/schema';
export interface User {
id: number;
@@ -58,3 +58,235 @@ export const getAllowListUser = async ({ email }: { email?: string }) => {
return allowedEmail;
};
+
+export const getUserByAuthId = async ({ authUserId }: { authUserId: string }) => {
+ return await db.query.users.findFirst({
+ where: (table, { eq }) => eq(table.authUserId, authUserId),
+ with: {
+ avatarImage: true,
+ organizationUsers: {
+ with: {
+ organization: {
+ with: {
+ profile: {
+ with: {
+ avatarImage: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ currentOrganization: {
+ with: {
+ profile: {
+ with: {
+ avatarImage: true,
+ },
+ },
+ },
+ },
+ currentProfile: {
+ with: {
+ avatarImage: true,
+ headerImage: true,
+ },
+ },
+ profile: {
+ with: {
+ avatarImage: true,
+ headerImage: true,
+ },
+ },
+ },
+ });
+};
+
+export const createUserByAuthId = async ({ authUserId, email }: { authUserId: string; email: string }) => {
+ const [newUser] = await db
+ .insert(users)
+ .values({
+ authUserId,
+ email,
+ })
+ .returning();
+
+ if (!newUser) {
+ throw new Error('Could not create user');
+ }
+
+ return await db.query.users.findFirst({
+ where: (table, { eq }) => eq(table.id, newUser.id),
+ with: {
+ avatarImage: true,
+ organizationUsers: {
+ with: {
+ organization: {
+ with: {
+ profile: {
+ with: {
+ avatarImage: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ currentOrganization: {
+ with: {
+ profile: {
+ with: {
+ avatarImage: true,
+ },
+ },
+ },
+ },
+ currentProfile: {
+ with: {
+ avatarImage: true,
+ headerImage: true,
+ },
+ },
+ profile: {
+ with: {
+ avatarImage: true,
+ headerImage: true,
+ },
+ },
+ },
+ });
+};
+
+export const getUserWithProfiles = async ({ authUserId }: { authUserId: string }) => {
+ return await db.query.users.findFirst({
+ where: (table, { eq }) => eq(table.authUserId, authUserId),
+ with: {
+ profile: {
+ with: {
+ avatarImage: true,
+ },
+ },
+ organizationUsers: {
+ with: {
+ organization: {
+ with: {
+ profile: {
+ with: {
+ avatarImage: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+};
+
+export const getUserForProfileSwitch = async ({ authUserId }: { authUserId: string }) => {
+ return await db.query.users.findFirst({
+ where: (table, { eq }) => eq(table.authUserId, authUserId),
+ with: {
+ profile: true,
+ organizationUsers: {
+ with: {
+ organization: {
+ with: {
+ profile: true,
+ },
+ },
+ },
+ },
+ },
+ });
+};
+
+export interface UpdateUserCurrentProfileOptions {
+ authUserId: string;
+ profileId: string;
+ orgId?: number;
+}
+
+export const updateUserCurrentProfile = async (options: UpdateUserCurrentProfileOptions) => {
+ const { authUserId, profileId, orgId } = options;
+ return await db
+ .update(users)
+ .set({
+ currentProfileId: profileId,
+ ...(orgId ? { lastOrgId: orgId.toString() } : {}),
+ })
+ .where(eq(users.authUserId, authUserId))
+ .returning();
+};
+
+export const checkUsernameAvailability = async ({ username }: { username: string }) => {
+ if (username === '') {
+ return { available: true };
+ }
+
+ const result = await db
+ .select({
+ exists: sql`true`,
+ })
+ .from(organizationUsers)
+ .where(eq(users.username, username))
+ .limit(1);
+
+ if (!result.length || !result[0]) {
+ return { available: true };
+ }
+
+ return { available: false };
+};
+
+export const getUserStorageUsage = async ({ userId }: { userId: string }) => {
+ const result = await db
+ .select()
+ .from(usersUsedStorage)
+ .where(and(eq(usersUsedStorage.userId, userId)))
+ .limit(1);
+
+ if (!result.length || !result[0]) {
+ return {
+ usedStorage: 0,
+ maxStorage: 4000000000 as const,
+ };
+ }
+
+ return {
+ usedStorage: Number.parseInt(result[0].totalSize as string),
+ maxStorage: 4000000000 as const,
+ };
+};
+
+export interface SwitchUserOrganizationOptions {
+ authUserId: string;
+ organizationId: string;
+}
+
+export const switchUserOrganization = async (options: SwitchUserOrganizationOptions) => {
+ const { authUserId, organizationId } = options;
+ // First, get the organization to find its profile ID
+ const organization = await db.query.organizations.findFirst({
+ where: eq(organizations.id, organizationId),
+ });
+
+ if (!organization) {
+ throw new Error('Organization not found');
+ }
+
+ const result = await db
+ .update(users)
+ .set({
+ lastOrgId: organization.id,
+ currentProfileId: organization.profileId,
+ })
+ .where(eq(users.authUserId, authUserId))
+ .returning();
+
+ if (!result.length || !result[0]) {
+ throw new Error('User not found');
+ }
+
+ return result[0];
+};
\ No newline at end of file
diff --git a/packages/ui/tailwind.shared.ts b/packages/ui/tailwind.shared.ts
index 27f2f00ff..c0d5fad2b 100644
--- a/packages/ui/tailwind.shared.ts
+++ b/packages/ui/tailwind.shared.ts
@@ -140,38 +140,6 @@ const config: Omit = {
colors: {
accent: commonColors,
border: 'hsl(var(--op-offWhite))',
- // input: 'hsl(var(--input))',
- // ring: 'hsl(var(--ring))',
- // background: 'hsl(var(--background))',
- // foreground: 'hsl(var(--foreground))',
- // primary: {
- // DEFAULT: 'hsl(var(--primary))',
- // foreground: 'hsl(var(--primary-foreground))',
- // },
- // secondary: {
- // DEFAULT: 'hsl(var(--secondary))',
- // foreground: 'hsl(var(--secondary-foreground))',
- // },
- // destructive: {
- // DEFAULT: 'hsl(var(--destructive))',
- // foreground: 'hsl(var(--destructive-foreground))',
- // },
- // muted: {
- // DEFAULT: 'hsl(var(--muted))',
- // foreground: 'hsl(var(--muted-foreground))',
- // },
- // accent: {
- // DEFAULT: 'hsl(var(--accent))',
- // foreground: 'hsl(var(--accent-foreground))',
- // },
- // popover: {
- // DEFAULT: 'hsl(var(--popover))',
- // foreground: 'hsl(var(--popover-foreground))',
- // },
- // card: {
- // DEFAULT: 'hsl(var(--card))',
- // foreground: 'hsl(var(--card-foreground))',
- // },
teal: {
DEFAULT: 'hsl(var(--op-teal-500))',
50: 'hsl(var(--op-teal-50))',
diff --git a/services/api/src/routers/account/getMyAccount.ts b/services/api/src/routers/account/getMyAccount.ts
index c3d19aa3e..ab716921a 100644
--- a/services/api/src/routers/account/getMyAccount.ts
+++ b/services/api/src/routers/account/getMyAccount.ts
@@ -1,11 +1,9 @@
-import { CommonError, NotFoundError } from '@op/common';
-import { users } from '@op/db/schema';
+import { CommonError, NotFoundError, getUserByAuthId, createUserByAuthId } from '@op/common';
import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import { userEncoder } from '../../encoders';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -25,55 +23,14 @@ export const getMyAccount = router({
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// Router
.meta(meta)
.input(z.undefined())
.output(userEncoder)
.query(async ({ ctx }) => {
- const { db } = ctx.database;
const { id, email } = ctx.user;
- const result = await db.query.users.findFirst({
- where: (table, { eq }) => eq(table.authUserId, id),
- with: {
- avatarImage: true,
- organizationUsers: {
- with: {
- organization: {
- with: {
- profile: {
- with: {
- avatarImage: true,
- },
- },
- },
- },
- },
- },
- currentOrganization: {
- with: {
- profile: {
- with: {
- avatarImage: true,
- },
- },
- },
- },
- currentProfile: {
- with: {
- avatarImage: true,
- headerImage: true,
- },
- },
- profile: {
- with: {
- avatarImage: true,
- headerImage: true,
- },
- },
- },
- });
+ const result = await getUserByAuthId({ authUserId: id });
if (!result) {
if (!email) {
@@ -81,59 +38,15 @@ export const getMyAccount = router({
}
// if there is no user but the user is authenticated, create one
- const [newUser] = await db
- .insert(users)
- .values({
- authUserId: id,
- email: ctx.user.email!,
- })
- .returning();
+ const newUserWithRelations = await createUserByAuthId({
+ authUserId: id,
+ email: ctx.user.email!,
+ });
- if (!newUser) {
+ if (!newUserWithRelations) {
throw new CommonError('Could not create user');
}
- const newUserWithRelations = await db.query.users.findFirst({
- where: (table, { eq }) => eq(table.id, newUser.id),
- with: {
- avatarImage: true,
- organizationUsers: {
- with: {
- organization: {
- with: {
- profile: {
- with: {
- avatarImage: true,
- },
- },
- },
- },
- },
- },
- currentOrganization: {
- with: {
- profile: {
- with: {
- avatarImage: true,
- },
- },
- },
- },
- currentProfile: {
- with: {
- avatarImage: true,
- headerImage: true,
- },
- },
- profile: {
- with: {
- avatarImage: true,
- headerImage: true,
- },
- },
- },
- });
-
return userEncoder.parse(newUserWithRelations);
}
diff --git a/services/api/src/routers/account/getUserProfiles.ts b/services/api/src/routers/account/getUserProfiles.ts
index 2537122f9..0934c2c8f 100644
--- a/services/api/src/routers/account/getUserProfiles.ts
+++ b/services/api/src/routers/account/getUserProfiles.ts
@@ -1,10 +1,9 @@
-import { UnauthorizedError } from '@op/common';
+import { UnauthorizedError, getUserWithProfiles } from '@op/common';
import { EntityType, ObjectsInStorage, Profile } from '@op/db/schema';
import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -37,7 +36,6 @@ export const getUserProfiles = router({
getUserProfiles: loggedProcedure
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
.meta(meta)
.input(z.undefined())
.output(
@@ -53,33 +51,10 @@ export const getUserProfiles = router({
),
)
.query(async ({ ctx }) => {
- const { db } = ctx.database;
const { id: authUserId } = ctx.user;
// Get the user's database record
- const user = await db.query.users.findFirst({
- where: (table, { eq }) => eq(table.authUserId, authUserId),
- with: {
- profile: {
- with: {
- avatarImage: true,
- },
- },
- organizationUsers: {
- with: {
- organization: {
- with: {
- profile: {
- with: {
- avatarImage: true,
- },
- },
- },
- },
- },
- },
- },
- });
+ const user = await getUserWithProfiles({ authUserId });
if (!user) {
throw new UnauthorizedError('User not found');
diff --git a/services/api/src/routers/account/switchProfile.ts b/services/api/src/routers/account/switchProfile.ts
index 2a2065058..50bda6f32 100644
--- a/services/api/src/routers/account/switchProfile.ts
+++ b/services/api/src/routers/account/switchProfile.ts
@@ -1,12 +1,10 @@
-import { users } from '@op/db/schema';
+import { getUserForProfileSwitch, updateUserCurrentProfile } from '@op/common';
import { TRPCError } from '@trpc/server';
-import { eq } from 'drizzle-orm';
import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import { userEncoder } from '../../encoders';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -27,30 +25,14 @@ export const switchProfile = router({
switchProfile: loggedProcedure
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
.meta(meta)
.input(z.object({ profileId: z.string().uuid() }))
.output(userEncoder)
.mutation(async ({ input, ctx }) => {
- const { db } = ctx.database;
const { id } = ctx.user;
// Verify the profile exists and the user has access to it
- const user = await db.query.users.findFirst({
- where: eq(users.authUserId, id),
- with: {
- profile: true,
- organizationUsers: {
- with: {
- organization: {
- with: {
- profile: true,
- },
- },
- },
- },
- },
- });
+ const user = await getUserForProfileSwitch({ authUserId: id });
if (!user) {
throw new TRPCError({
@@ -81,14 +63,7 @@ export const switchProfile = router({
let result;
try {
- result = await db
- .update(users)
- .set({
- currentProfileId: input.profileId,
- ...(org ? { lastOrgId: org.organization?.id } : {}),
- })
- .where(eq(users.authUserId, id))
- .returning();
+ result = await updateUserCurrentProfile({ authUserId: id, profileId: input.profileId, orgId: org?.organization?.id });
} catch (error) {
console.error(error);
throw new TRPCError({
diff --git a/services/api/src/routers/account/updateLastOrgId.ts b/services/api/src/routers/account/updateLastOrgId.ts
index 647a418bc..c2cc3e18d 100644
--- a/services/api/src/routers/account/updateLastOrgId.ts
+++ b/services/api/src/routers/account/updateLastOrgId.ts
@@ -1,12 +1,10 @@
-import { organizations, users } from '@op/db/schema';
+import { switchUserOrganization } from '@op/common';
import { TRPCError } from '@trpc/server';
-import { eq } from 'drizzle-orm';
import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import { userEncoder } from '../../encoders';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -27,51 +25,37 @@ export const switchOrganization = router({
switchOrganization: loggedProcedure
.use(withRateLimited({ windowSize: 10, maxRequests: 5 }))
.use(withAuthenticated)
- .use(withDB)
.meta(meta)
.input(z.object({ organizationId: z.string().min(1) }))
.output(userEncoder)
.mutation(async ({ input, ctx }) => {
- const { db } = ctx.database;
const { id } = ctx.user;
- let result;
try {
- // First, get the organization to find its profile ID
- const organization = await db.query.organizations.findFirst({
- where: eq(organizations.id, input.organizationId),
- });
-
- if (!organization) {
- throw new TRPCError({
- code: 'NOT_FOUND',
- message: 'Organization not found',
- });
- }
-
- result = await db
- .update(users)
- .set({
- lastOrgId: organization.id,
- currentProfileId: organization.profileId,
- })
- .where(eq(users.authUserId, id))
- .returning();
+ const result = await switchUserOrganization({ authUserId: id, organizationId: input.organizationId });
+ return userEncoder.parse(result);
} catch (error) {
console.error(error);
+
+ if (error instanceof Error) {
+ if (error.message === 'Organization not found') {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'Organization not found',
+ });
+ }
+ if (error.message === 'User not found') {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'User not found',
+ });
+ }
+ }
+
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to update currentProfileId',
});
}
-
- if (!result.length || !result[0]) {
- throw new TRPCError({
- code: 'NOT_FOUND',
- message: 'User not found',
- });
- }
-
- return userEncoder.parse(result[0]);
}),
});
diff --git a/services/api/src/routers/account/updateUserProfile.ts b/services/api/src/routers/account/updateUserProfile.ts
index 5b67d57f7..c78036cd2 100644
--- a/services/api/src/routers/account/updateUserProfile.ts
+++ b/services/api/src/routers/account/updateUserProfile.ts
@@ -5,7 +5,6 @@ import { ZodError, z } from 'zod';
import { userEncoder } from '../../encoders';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -27,7 +26,6 @@ const updateUserProfile = router({
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 3 }))
.use(withAuthenticated)
- .use(withDB)
// Router
.meta(meta)
.input(
@@ -61,14 +59,12 @@ const updateUserProfile = router({
)
.output(userEncoder)
.mutation(async ({ input, ctx }) => {
- const { db } = ctx.database;
const { user } = ctx;
try {
const result = await updateUserProfileService({
input,
user,
- db,
});
return userEncoder.parse(result);
diff --git a/services/api/src/routers/account/usedStorage.ts b/services/api/src/routers/account/usedStorage.ts
index 807b1073e..76f2d5767 100644
--- a/services/api/src/routers/account/usedStorage.ts
+++ b/services/api/src/routers/account/usedStorage.ts
@@ -1,10 +1,8 @@
-import { usersUsedStorage } from '@op/db/schema';
-import { and, eq } from 'drizzle-orm';
+import { getUserStorageUsage } from '@op/common';
import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -26,7 +24,6 @@ const usedStorage = router({
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// Router
.meta(meta)
.input(z.undefined())
@@ -37,25 +34,7 @@ const usedStorage = router({
}),
)
.query(async ({ ctx }) => {
- const { db } = ctx.database;
-
- const result = await db
- .select()
- .from(usersUsedStorage)
- .where(and(eq(usersUsedStorage.userId, ctx.user.id)))
- .limit(1);
-
- if (!result.length || !result[0]) {
- return {
- usedStorage: 0,
- maxStorage: 4000000000,
- };
- }
-
- return {
- usedStorage: Number.parseInt(result[0].totalSize as string),
- maxStorage: 4000000000,
- };
+ return await getUserStorageUsage({ userId: ctx.user.id });
}),
});
diff --git a/services/api/src/routers/account/usernameAvailable.ts b/services/api/src/routers/account/usernameAvailable.ts
index a23e9aebf..32f274f27 100644
--- a/services/api/src/routers/account/usernameAvailable.ts
+++ b/services/api/src/routers/account/usernameAvailable.ts
@@ -1,10 +1,8 @@
-import { organizationUsers, users } from '@op/db/schema';
-import { eq, sql } from 'drizzle-orm';
+import { checkUsernameAvailability } from '@op/common';
import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -26,7 +24,6 @@ const usernameAvailable = router({
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// Router
.meta(meta)
.input(
@@ -42,33 +39,9 @@ const usernameAvailable = router({
available: z.boolean(),
}),
)
- .query(async ({ input, ctx }) => {
- const { db } = ctx.database;
+ .query(async ({ input }) => {
const { username } = input;
-
- if (username === '') {
- return {
- available: true,
- };
- }
-
- const result = await db
- .select({
- exists: sql`true`,
- })
- .from(organizationUsers)
- .where(eq(users.username, username))
- .limit(1);
-
- if (!result.length || !result[0]) {
- return {
- available: true,
- };
- }
-
- return {
- available: false,
- };
+ return await checkUsernameAvailability({ username });
}),
});
diff --git a/services/api/src/routers/individual/getIndividual.ts b/services/api/src/routers/individual/getIndividual.ts
index 51b883b89..420a68403 100644
--- a/services/api/src/routers/individual/getIndividual.ts
+++ b/services/api/src/routers/individual/getIndividual.ts
@@ -9,7 +9,6 @@ import { z } from 'zod';
import { individualsTermsEncoder } from '../../encoders/individuals';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -29,7 +28,6 @@ export const getIndividualRouter = router({
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// Router
.meta(meta)
.input(z.object({ id: z.string(), termUri: z.string().optional() }))
@@ -71,7 +69,6 @@ export const getIndividualRouter = router({
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// Router
.input(z.object({ profileId: z.string(), termUri: z.string().optional() }))
.output(individualsTermsEncoder)
@@ -101,4 +98,4 @@ export const getIndividualRouter = router({
});
}
}),
-});
\ No newline at end of file
+});
diff --git a/services/api/src/routers/organization/createOrganization.ts b/services/api/src/routers/organization/createOrganization.ts
index b6eed9902..a1d4e7745 100644
--- a/services/api/src/routers/organization/createOrganization.ts
+++ b/services/api/src/routers/organization/createOrganization.ts
@@ -4,7 +4,6 @@ import type { OpenApiMeta } from 'trpc-to-openapi';
import { organizationsEncoder } from '../../encoders/organizations';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
import { createOrganizationInputSchema } from './validators';
@@ -25,7 +24,6 @@ export const createOrganizationRouter = router({
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// Router
.meta(meta)
.input(createOrganizationInputSchema)
diff --git a/services/api/src/routers/organization/createPostInOrganization.ts b/services/api/src/routers/organization/createPostInOrganization.ts
index 8b193e8e6..38b411693 100644
--- a/services/api/src/routers/organization/createPostInOrganization.ts
+++ b/services/api/src/routers/organization/createPostInOrganization.ts
@@ -1,14 +1,11 @@
import { trackUserPost } from '@op/analytics';
-import { UnauthorizedError, getOrgAccessUser } from '@op/common';
-import { attachments, posts, postsToOrganizations } from '@op/db/schema';
-import { TRPCError } from '@trpc/server';
+import { createPostInOrganization } from '@op/common';
import { waitUntil } from '@vercel/functions';
// import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import { postsEncoder } from '../../encoders';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -25,12 +22,11 @@ import { loggedProcedure, router } from '../../trpcFactory';
const outputSchema = postsEncoder;
-export const createPostInOrganization = router({
+export const createPostInOrganizationRouter = router({
createPost: loggedProcedure
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 3 }))
.use(withAuthenticated)
- .use(withDB)
// Router
// .meta(meta)
.input(
@@ -42,90 +38,15 @@ export const createPostInOrganization = router({
)
.output(outputSchema)
.mutation(async ({ input, ctx }) => {
- const { db } = ctx.database;
-
- const user = await getOrgAccessUser({
- organizationId: input.id,
+ const { result, allStorageObjects } = await createPostInOrganization({
+ id: input.id,
+ content: input.content,
+ attachmentIds: input.attachmentIds,
user: ctx.user,
});
- if (!user) {
- throw new UnauthorizedError();
- }
-
- try {
- // Get all storage objects that were attached to the post
- const allStorageObjects =
- input.attachmentIds.length > 0
- ? await db.query.objectsInStorage.findMany({
- where: (table, { inArray }) =>
- inArray(table.id, input.attachmentIds),
- })
- : [];
-
- const [post] = await db
- .insert(posts)
- .values({
- content: input.content,
- })
- .returning();
-
- if (!post) {
- throw new TRPCError({
- code: 'INTERNAL_SERVER_ERROR',
- message: 'Failed to add post to organization',
- });
- }
-
- // Create the join record associating the post with the organization
- const queryPromises: Promise[] = [
- db.insert(postsToOrganizations).values({
- organizationId: input.id,
- postId: post.id,
- }),
- ];
-
- // Create attachment records if any attachments were uploaded
- if (allStorageObjects.length > 0) {
- const attachmentValues = allStorageObjects.map((storageObject) => ({
- postId: post.id,
- storageObjectId: storageObject.id,
- uploadedBy: user.id,
- fileName:
- // @ts-expect-error - We check for this existence first. TODO: find the source of this TS error
- storageObject?.name
- ?.split('/')
- .slice(-1)[0]
- .split('_')
- .slice(1)
- .join('_') ?? '',
- mimeType: (storageObject.metadata as { mimetype: string }).mimetype,
- }));
-
- // @ts-ignore
- queryPromises.push(db.insert(attachments).values(attachmentValues));
- }
-
- // Run attachments and join record in parallel
- await Promise.all(queryPromises);
-
- // Track analytics (non-blocking)
- waitUntil(trackUserPost(ctx.user.id, input.content, allStorageObjects));
-
- const newPost = post;
+ waitUntil(trackUserPost(ctx.user.id, input.content, allStorageObjects));
- const output = outputSchema.parse({
- ...newPost,
- reactionCounts: {},
- userReactions: [],
- });
- return output;
- } catch (error) {
- console.log('ERROR', error);
- throw new TRPCError({
- code: 'INTERNAL_SERVER_ERROR',
- message: 'Something went wrong when adding post to organization',
- });
- }
+ return outputSchema.parse(result);
}),
});
diff --git a/services/api/src/routers/organization/deletePost.ts b/services/api/src/routers/organization/deletePost.ts
index f0443ffaf..97cf0f3ac 100644
--- a/services/api/src/routers/organization/deletePost.ts
+++ b/services/api/src/routers/organization/deletePost.ts
@@ -1,12 +1,9 @@
-import { UnauthorizedError, getOrgAccessUser } from '@op/common';
-import { posts, postsToOrganizations } from '@op/db/schema';
+import { UnauthorizedError, getOrgAccessUser, deletePostById } from '@op/common';
import { TRPCError } from '@trpc/server';
-import { and, eq } from 'drizzle-orm';
import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -26,7 +23,6 @@ export const deletePost = router({
deletePost: loggedProcedure
.use(withRateLimited({ windowSize: 10, maxRequests: 5 }))
.use(withAuthenticated)
- .use(withDB)
.meta(meta)
.input(
z.object({
@@ -39,7 +35,6 @@ export const deletePost = router({
.output(z.object({ success: z.boolean() }))
.mutation(async ({ input, ctx }) => {
const { id, organizationId } = input;
- const { db } = ctx.database;
const user = await getOrgAccessUser({
organizationId,
@@ -50,32 +45,16 @@ export const deletePost = router({
throw new UnauthorizedError();
}
- // Verify the post exists and belongs to the organization
- const postExists = await db
- .select()
- .from(posts)
- .innerJoin(
- postsToOrganizations,
- eq(posts.id, postsToOrganizations.postId),
- )
- .where(
- and(
- eq(posts.id, id),
- eq(postsToOrganizations.organizationId, organizationId),
- ),
- )
- .limit(1);
-
- if (!postExists.length) {
- throw new TRPCError({
- code: 'NOT_FOUND',
- message:
- 'Post not found or does not belong to the specified organization',
- });
+ try {
+ return await deletePostById({ postId: id, organizationId });
+ } catch (error) {
+ if (error instanceof Error && error.message.includes('Post not found')) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: error.message,
+ });
+ }
+ throw error;
}
-
- await db.delete(posts).where(eq(posts.id, id));
-
- return { success: true };
}),
});
diff --git a/services/api/src/routers/organization/getOrganization.ts b/services/api/src/routers/organization/getOrganization.ts
index efd8d562d..3b907a2c3 100644
--- a/services/api/src/routers/organization/getOrganization.ts
+++ b/services/api/src/routers/organization/getOrganization.ts
@@ -13,7 +13,6 @@ import {
organizationsWithProfileEncoder,
} from '../../encoders/organizations';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -37,7 +36,6 @@ export const getOrganizationRouter = router({
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// Router
.meta(meta)
.input(inputSchema)
@@ -80,7 +78,6 @@ export const getOrganizationRouter = router({
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// Router
// .meta(meta)
.input(z.object({ id: z.string() }))
@@ -123,7 +120,6 @@ export const getOrganizationRouter = router({
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// Router
// .meta(meta)
.input(z.object({ id: z.string(), termUri: z.string().optional() }))
diff --git a/services/api/src/routers/organization/getOrganizationsByProfile.ts b/services/api/src/routers/organization/getOrganizationsByProfile.ts
index 35e22457e..c3c174d7c 100644
--- a/services/api/src/routers/organization/getOrganizationsByProfile.ts
+++ b/services/api/src/routers/organization/getOrganizationsByProfile.ts
@@ -1,12 +1,10 @@
-import { sql } from '@op/db/client';
-import { locations } from '@op/db/schema';
+import { getOrganizationsByProfile } from '@op/common';
import { TRPCError } from '@trpc/server';
import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import { organizationsWithProfileEncoder } from '../../encoders/organizations';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -25,82 +23,14 @@ export const getOrganizationsByProfileRouter = router({
getOrganizationsByProfile: loggedProcedure
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
.meta(meta)
.input(z.object({ profileId: z.string().uuid() }))
.output(z.array(organizationsWithProfileEncoder))
- .query(async ({ input, ctx }) => {
- const { db } = ctx.database;
+ .query(async ({ input }) => {
const { profileId } = input;
try {
- // Find all users who have access to this profile
- // Either as their personal profile or as their current profile
- const usersWithProfile = await db.query.users.findMany({
- where: (table, { eq, or }) =>
- or(
- eq(table.profileId, profileId),
- eq(table.currentProfileId, profileId),
- ),
- with: {
- organizationUsers: {
- with: {
- organization: {
- with: {
- projects: true,
- links: true,
- profile: {
- with: {
- headerImage: true,
- avatarImage: true,
- },
- },
- whereWeWork: {
- with: {
- location: {
- extras: {
- x: sql`ST_X(${locations.location})`.as('x'),
- y: sql`ST_Y(${locations.location})`.as('y'),
- },
- columns: {
- id: true,
- name: true,
- placeId: true,
- countryCode: true,
- countryName: true,
- metadata: true,
- latLng: false,
- },
- },
- },
- },
- },
- },
- },
- },
- },
- });
-
- // Collect all unique organizations
- const organizationMap = new Map();
-
- for (const user of usersWithProfile) {
- for (const orgUser of user.organizationUsers) {
- if (orgUser.organization) {
- const org = orgUser.organization;
-
- // Transform whereWeWork to match expected format
- const transformedOrg = {
- ...org,
- whereWeWork: org.whereWeWork.map((item: any) => item.location),
- };
-
- organizationMap.set(org.id, transformedOrg);
- }
- }
- }
-
- const organizations = Array.from(organizationMap.values());
+ const organizations = await getOrganizationsByProfile(profileId);
return organizations.map((org) =>
organizationsWithProfileEncoder.parse(org),
diff --git a/services/api/src/routers/organization/index.ts b/services/api/src/routers/organization/index.ts
index 64caaa098..27fefc015 100644
--- a/services/api/src/routers/organization/index.ts
+++ b/services/api/src/routers/organization/index.ts
@@ -2,7 +2,7 @@ import { mergeRouters } from '../../trpcFactory';
import { addRelationshipRouter } from './addRelationship';
import { approveRelationshipRouter } from './approveRelationship';
import { createOrganizationRouter } from './createOrganization';
-import { createPostInOrganization } from './createPostInOrganization';
+import { createPostInOrganizationRouter } from './createPostInOrganization';
import { declineRelationshipRouter } from './declineRelationship';
import { deletePost } from './deletePost';
import { getOrganizationRouter } from './getOrganization';
@@ -27,7 +27,7 @@ export const organizationRouter = mergeRouters(
listOrganizationsRouter,
listOrganizationPostsRouter,
searchOrganizationsRouter,
- createPostInOrganization,
+ createPostInOrganizationRouter,
deletePost,
createOrganizationRouter,
updateOrganizationRouter,
diff --git a/services/api/src/routers/organization/inviteUser.ts b/services/api/src/routers/organization/inviteUser.ts
index 9db0b60d0..37927f251 100644
--- a/services/api/src/routers/organization/inviteUser.ts
+++ b/services/api/src/routers/organization/inviteUser.ts
@@ -1,12 +1,9 @@
-import { UnauthorizedError, sendInvitationEmail } from '@op/common';
-import { OPURLConfig } from '@op/core';
-import { allowList } from '@op/db/schema';
+import { UnauthorizedError, inviteUsersToOrganization } from '@op/common';
import { TRPCError } from '@trpc/server';
// import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -60,14 +57,12 @@ export const inviteUserRouter = router({
// Middlewares
.use(withRateLimited({ windowSize: 60, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// Router
// .meta(meta)
.input(inputSchema)
.output(outputSchema)
.mutation(async ({ ctx, input }) => {
try {
- const { db } = ctx.database;
const { id: authUserId } = ctx.user;
// Handle both single email and multiple emails input
@@ -77,131 +72,25 @@ export const inviteUserRouter = router({
const targetOrganizationId = input.organizationId;
const personalMessage = input.personalMessage;
- // Get the current user's database record with organization details
- const authUser = await db.query.users.findFirst({
- where: (table, { eq }) => eq(table.authUserId, authUserId),
- with: {
- currentOrganization: {
- with: {
- profile: true,
- },
- },
- currentProfile: true,
- },
+ return await inviteUsersToOrganization({
+ emails: emailsToProcess,
+ role,
+ organizationId: targetOrganizationId,
+ personalMessage,
+ authUserId,
+ authUserEmail: ctx.user.email,
});
-
- // For new organization invites, we don't need the user to be in an organization
- // For existing organization invites, we do need it
- if (
- (!authUser?.currentProfileId && !authUser?.lastOrgId) ||
- (!authUser.currentOrganization && !authUser.currentProfile)
- ) {
- throw new UnauthorizedError(
- 'User must be associated with an organization to send invites',
- );
- }
-
- const currentProfile =
- authUser.currentProfile ??
- (authUser.currentOrganization as any)?.profile;
-
- const results = {
- successful: [] as string[],
- failed: [] as { email: string; reason: string }[],
- };
-
- // Process each email
- for (const rawEmail of emailsToProcess) {
- const email = rawEmail.toLowerCase();
- try {
- // Check if email is already in the allowList
- const existingEntry = await db.query.allowList.findFirst({
- where: (table, { eq }) => eq(table.email, email),
- });
-
- if (!existingEntry) {
- // Determine metadata based on whether it's a new organization invite
- const metadata = targetOrganizationId
- ? {
- invitedBy: authUserId,
- invitedAt: new Date().toISOString(),
- inviteType: 'new_organization',
- personalMessage: personalMessage,
- inviterOrganizationName:
- (currentProfile as any)?.profile?.name || 'Common',
- }
- : {
- invitedBy: authUserId,
- invitedAt: new Date().toISOString(),
- personalMessage: personalMessage,
- role,
- };
-
- // Add the email to the allowList
- await db.insert(allowList).values({
- email,
- organizationId: targetOrganizationId ?? null,
- metadata,
- });
- }
-
- // Send invitation email
- try {
- await sendInvitationEmail({
- to: email,
- inviterName:
- authUser?.name || ctx.user.email || 'A team member',
- organizationName: targetOrganizationId
- ? (authUser?.currentOrganization as any)?.profile?.name ||
- 'an organization'
- : undefined,
- inviteUrl: OPURLConfig('APP').ENV_URL,
- message: personalMessage,
- });
- results.successful.push(email);
- } catch (emailError) {
- console.error(
- `Failed to send invitation email to ${email}:`,
- emailError,
- );
- // Email failed but database insertion succeeded
- results.successful.push(email);
- }
- } catch (error) {
- console.error(`Failed to process invitation for ${email}:`, error);
- results.failed.push({
- email,
- reason: error instanceof Error ? error.message : 'Unknown error',
- });
- }
- }
-
- const totalEmails = emailsToProcess.length;
- const successCount = results.successful.length;
-
- let message: string;
- if (successCount === totalEmails) {
- message = `All ${totalEmails} invitation${totalEmails > 1 ? 's' : ''} sent successfully`;
- } else if (successCount > 0) {
- message = `${successCount} of ${totalEmails} invitations sent successfully`;
- } else {
- message = 'No invitations were sent successfully';
- }
-
- return {
- success: successCount > 0,
- message,
- details: {
- successful: results.successful,
- failed: results.failed,
- },
- };
} catch (error) {
// Re-throw TRPCError as-is
if (error instanceof TRPCError) {
throw error;
}
+ // Handle specific errors
+ if (error instanceof Error && error.message.includes('User must be associated')) {
+ throw new UnauthorizedError(error.message);
+ }
+
// Handle other errors
const message =
error instanceof Error ? error.message : 'Failed to send invitation';
diff --git a/services/api/src/routers/organization/listRelatedOrganizationPosts.ts b/services/api/src/routers/organization/listRelatedOrganizationPosts.ts
index 0d8f4c045..e0448bf57 100644
--- a/services/api/src/routers/organization/listRelatedOrganizationPosts.ts
+++ b/services/api/src/routers/organization/listRelatedOrganizationPosts.ts
@@ -1,12 +1,4 @@
-import {
- decodeCursor,
- encodeCursor,
- getCurrentProfileId,
- getItemsWithReactionsAndComments,
- getRelatedOrganizations,
-} from '@op/common';
-import { and, eq, inArray, lt, or } from '@op/db/client';
-import { postsToOrganizations } from '@op/db/schema';
+import { listAllRelatedOrganizationPosts, listRelatedOrganizationPosts } from '@op/common';
// import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
@@ -19,7 +11,6 @@ import {
postsToOrganizationsEncoder,
} from '../../encoders/posts';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
import { dbFilter } from '../../utils';
@@ -54,7 +45,6 @@ export const listRelatedOrganizationPostsRouter = router({
listAllPosts: loggedProcedure
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// .meta(metaAllPosts)
.input(
dbFilter
@@ -70,133 +60,37 @@ export const listRelatedOrganizationPostsRouter = router({
hasMore: z.boolean(),
}),
)
- .query(async ({ ctx, input }) => {
- const { db } = ctx.database;
+ .query(async ({ input }) => {
const { limit = 200, cursor } = input ?? {};
- // Parse cursor
- const cursorData = cursor ? decodeCursor(cursor) : null;
-
- // Build cursor condition for pagination
- const cursorCondition = cursorData
- ? or(
- lt(postsToOrganizations.createdAt, cursorData.createdAt),
- and(
- eq(postsToOrganizations.createdAt, cursorData.createdAt),
- lt(postsToOrganizations.postId, cursorData.id),
- ),
- )
- : undefined;
-
- // Fetch posts for all organizations with pagination
- const [result, profileId] = await Promise.all([
- db.query.postsToOrganizations.findMany({
- where: cursorCondition,
- with: {
- post: {
- where: (table, { isNull }) => isNull(table.parentPostId), // Only show top-level posts
- with: {
- attachments: {
- with: {
- storageObject: true,
- },
- },
- reactions: true,
- },
- },
- organization: {
- with: {
- profile: {
- with: {
- avatarImage: true,
- },
- },
- },
- },
- },
- orderBy: (table, { desc }) => desc(table.createdAt),
- limit: limit + 1, // Fetch one extra to check hasMore
- }),
- getCurrentProfileId(),
- ]);
-
- // Filter out any items where post is null (due to parentPostId filtering)
- const filteredResult = result.filter((item) => item.post !== null);
-
- const hasMore = filteredResult.length > limit;
- const items = hasMore ? filteredResult.slice(0, limit) : filteredResult;
- const lastItem = items[items.length - 1];
- const nextCursor =
- hasMore && lastItem && lastItem.createdAt
- ? encodeCursor(new Date(lastItem.createdAt), lastItem.postId)
- : null;
-
- const itemsWithReactionsAndComments =
- await getItemsWithReactionsAndComments({ items, profileId });
+ const result = await listAllRelatedOrganizationPosts({ limit, cursor });
return {
- items: itemsWithReactionsAndComments.map((postToOrg) => ({
+ items: result.items.map((postToOrg) => ({
...postToOrg,
organization: organizationsEncoder.parse(postToOrg.organization),
post: postsEncoder.parse(postToOrg.post),
})),
- next: nextCursor,
- hasMore,
+ next: result.next,
+ hasMore: result.hasMore,
};
}),
listRelatedPosts: loggedProcedure
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// .meta(meta)
.input(inputSchema)
.output(z.array(postsToOrganizationsEncoder))
.query(async ({ ctx, input }) => {
- const { db } = ctx.database;
const { organizationId } = input;
const { user } = ctx;
- // Get related organizations
- const { records: organizations } = await getRelatedOrganizations({
+ const result = await listRelatedOrganizationPosts({
+ organizationId,
user,
- orgId: organizationId,
- pending: false,
});
- const orgIds = organizations?.map((org: any) => org.id) ?? [];
- orgIds.push(organizationId); // Add our own org so we see our own posts
-
- // Fetch posts for all related organizations
- const result = await db.query.postsToOrganizations.findMany({
- where: () => inArray(postsToOrganizations.organizationId, orgIds),
- with: {
- post: {
- where: (table, { isNull }) => isNull(table.parentPostId), // Only show top-level posts
- with: {
- attachments: {
- with: {
- storageObject: true,
- },
- },
- },
- },
- organization: {
- with: {
- profile: {
- with: {
- avatarImage: true,
- },
- },
- },
- },
- },
- orderBy: (table, { desc }) => desc(table.createdAt),
- });
-
- // Filter out any items where post is null (due to parentPostId filtering)
- const filteredResult = result.filter((item) => item.post !== null);
-
- return filteredResult.map((postToOrg) => ({
+ return result.map((postToOrg) => ({
...postToOrg,
organization: organizationsWithProfileEncoder.parse(
postToOrg.organization,
diff --git a/services/api/src/routers/organization/reactions.ts b/services/api/src/routers/organization/reactions.ts
index 04bc08414..1b95e0ecb 100644
--- a/services/api/src/routers/organization/reactions.ts
+++ b/services/api/src/routers/organization/reactions.ts
@@ -1,12 +1,9 @@
-import { CommonError, getCurrentProfileId } from '@op/common';
-import { postReactions } from '@op/db/schema';
+import { CommonError, getCurrentProfileId, addReaction, removeReaction, toggleReaction } from '@op/common';
import { VALID_REACTION_TYPES } from '@op/types';
import { TRPCError } from '@trpc/server';
-import { and, eq } from 'drizzle-orm';
import { z } from 'zod';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -16,37 +13,18 @@ export const reactionsRouter = router({
addReaction: loggedProcedure
.use(withRateLimited({ windowSize: 10, maxRequests: 20 }))
.use(withAuthenticated)
- .use(withDB)
.input(
z.object({
postId: z.string(),
reactionType: reactionTypeEnum,
}),
)
- .mutation(async ({ input, ctx }) => {
+ .mutation(async ({ input }) => {
const { postId, reactionType } = input;
- const { database } = ctx;
try {
const profileId = await getCurrentProfileId();
-
- // First, remove any existing reaction from this user on this post
- await database.db
- .delete(postReactions)
- .where(
- and(
- eq(postReactions.postId, postId),
- eq(postReactions.profileId, profileId),
- ),
- );
-
- // Then add the new reaction
- await database.db.insert(postReactions).values({
- postId,
- profileId,
- reactionType,
- });
-
+ await addReaction({ postId, profileId, reactionType });
return { success: true };
} catch (error) {
throw new TRPCError({
@@ -59,25 +37,16 @@ export const reactionsRouter = router({
removeReaction: loggedProcedure
.use(withRateLimited({ windowSize: 10, maxRequests: 20 }))
.use(withAuthenticated)
- .use(withDB)
.input(
z.object({
postId: z.string(),
}),
)
- .mutation(async ({ input, ctx }) => {
+ .mutation(async ({ input }) => {
const { postId } = input;
- const { database } = ctx;
const profileId = await getCurrentProfileId();
- await database.db
- .delete(postReactions)
- .where(
- and(
- eq(postReactions.postId, postId),
- eq(postReactions.profileId, profileId),
- ),
- );
+ await removeReaction({ postId, profileId });
return { success: true };
}),
@@ -85,74 +54,18 @@ export const reactionsRouter = router({
toggleReaction: loggedProcedure
.use(withRateLimited({ windowSize: 10, maxRequests: 20 }))
.use(withAuthenticated)
- .use(withDB)
.input(
z.object({
postId: z.string(),
reactionType: reactionTypeEnum,
}),
)
- .mutation(async ({ input, ctx }) => {
+ .mutation(async ({ input }) => {
const { postId, reactionType } = input;
- const { database } = ctx;
try {
const profileId = await getCurrentProfileId();
-
- // Check if user has any existing reaction on this post
- const existingReaction = await database.db
- .select()
- .from(postReactions)
- .where(
- and(
- eq(postReactions.postId, postId),
- eq(postReactions.profileId, profileId),
- ),
- )
- .limit(1);
-
- if (existingReaction.length > 0) {
- // If user has the same reaction type, remove it
- if (existingReaction[0]?.reactionType === reactionType) {
- await database.db
- .delete(postReactions)
- .where(
- and(
- eq(postReactions.postId, postId),
- eq(postReactions.profileId, profileId),
- ),
- );
-
- return { success: true, action: 'removed' };
- } else {
- // If user has a different reaction type, replace it
- await database.db
- .delete(postReactions)
- .where(
- and(
- eq(postReactions.postId, postId),
- eq(postReactions.profileId, profileId),
- ),
- );
-
- await database.db.insert(postReactions).values({
- postId,
- profileId,
- reactionType,
- });
-
- return { success: true, action: 'replaced' };
- }
- } else {
- // No existing reaction, add new one
- await database.db.insert(postReactions).values({
- postId,
- profileId,
- reactionType,
- });
-
- return { success: true, action: 'added' };
- }
+ return await toggleReaction({ postId, profileId, reactionType });
} catch (e) {
throw new CommonError('Failed to toggle reaction');
}
diff --git a/services/api/src/routers/organization/updateOrganization.ts b/services/api/src/routers/organization/updateOrganization.ts
index 51548db52..657b7571f 100644
--- a/services/api/src/routers/organization/updateOrganization.ts
+++ b/services/api/src/routers/organization/updateOrganization.ts
@@ -7,7 +7,6 @@ import type { OpenApiMeta } from 'trpc-to-openapi';
import { organizationsEncoder } from '../../encoders/organizations';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
import { updateOrganizationInputSchema } from './validators';
@@ -28,7 +27,6 @@ export const updateOrganizationRouter = router({
// Middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 20 }))
.use(withAuthenticated)
- .use(withDB)
// Router
.meta(meta)
.input(updateOrganizationInputSchema)
diff --git a/services/api/src/routers/organization/uploadAvatarImage.ts b/services/api/src/routers/organization/uploadAvatarImage.ts
index 593b4bfdb..33b95802c 100644
--- a/services/api/src/routers/organization/uploadAvatarImage.ts
+++ b/services/api/src/routers/organization/uploadAvatarImage.ts
@@ -1,13 +1,12 @@
+import { trackImageUpload } from '@op/analytics';
import { createServerClient } from '@op/supabase/lib';
import { TRPCError } from '@trpc/server';
import { waitUntil } from '@vercel/functions';
import { Buffer } from 'buffer';
import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
-import { trackImageUpload } from '@op/analytics';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
import { MAX_FILE_SIZE, sanitizeS3Filename } from '../../utils';
@@ -37,7 +36,6 @@ export const uploadAvatarImage = router({
// middlewares
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
// Router
.meta(meta)
.input(
@@ -149,11 +147,11 @@ export const uploadAvatarImage = router({
logger.info(
'RETURNING UPLOAD URL' + signedUrlData.signedUrl + ' - ' + filePath,
);
-
+
// Track analytics - for organization uploads, we'll track as new uploads since they're temporary (non-blocking)
const imageType = filePath.includes('banner') ? 'banner' : 'profile';
waitUntil(trackImageUpload(ctx.user.id, imageType, false));
-
+
return {
url: signedUrlData.signedUrl,
path: filePath,
diff --git a/services/api/src/routers/taxonomy/taxonomyTerms.ts b/services/api/src/routers/taxonomy/taxonomyTerms.ts
index 79469c77c..f6c5b52b6 100644
--- a/services/api/src/routers/taxonomy/taxonomyTerms.ts
+++ b/services/api/src/routers/taxonomy/taxonomyTerms.ts
@@ -3,7 +3,6 @@ import { z } from 'zod';
import { taxonomyTermsWithChildrenEncoder } from '../../encoders/taxonomyTerms';
import withAuthenticated from '../../middlewares/withAuthenticated';
-import withDB from '../../middlewares/withDB';
import withRateLimited from '../../middlewares/withRateLimited';
import { loggedProcedure, router } from '../../trpcFactory';
@@ -11,7 +10,6 @@ export const termsRouter = router({
getTerms: loggedProcedure
.use(withRateLimited({ windowSize: 10, maxRequests: 10 }))
.use(withAuthenticated)
- .use(withDB)
.input(z.object({ name: z.string().min(3), q: z.string().optional() }))
.output(z.array(taxonomyTermsWithChildrenEncoder))
.query(async ({ input }) => {