import { ConvexError, v } from 'convex/values'; import { getAuthUserId } from '@convex-dev/auth/server'; import { type MutationCtx, type QueryCtx, mutation, query, } from './_generated/server'; import type { Doc, Id } from './_generated/dataModel'; import { paginationOptsValidator } from 'convex/server'; // NEW: shared ctx type for helpers type RWCtx = MutationCtx | QueryCtx; type StatusRow = { user: { id: Id<'users'>; name: string | null; imageUrl: string | null; }, status: { id: Id<'statuses'>; message: string; updatedAt: number; updatedBy: StatusRow['user'] | null; } | null, }; // CHANGED: typed helpers const ensureUser = async (ctx: RWCtx, userId: Id<'users'>) => { const user = await ctx.db.get(userId); if (!user) throw new ConvexError('User not found.'); return user; }; const latestStatusForOwner = async (ctx: RWCtx, ownerId: Id<'users'>) => { const [latest] = await ctx.db .query('statuses') .withIndex('by_user_updatedAt', (q) => q.eq('userId', ownerId)) .order('desc') .take(1); return latest as Doc<'statuses'> | null; }; /** * Create a new status for a single user. * - Defaults userId to the caller. * - updatedBy defaults to the caller. * - Updates the user's currentStatusId pointer. */ export const create = mutation({ args: { message: v.string(), userId: v.optional(v.id('users')), updatedBy: v.optional(v.id('users')), }, handler: async (ctx, args) => { const authUserId = await getAuthUserId(ctx); if (!authUserId) throw new ConvexError('Not authenticated.'); const userId = args.userId ?? authUserId; await ensureUser(ctx, userId); const updatedBy = args.updatedBy ?? authUserId; await ensureUser(ctx, updatedBy); const message = args.message.trim(); if (message.length === 0) { throw new ConvexError('Message cannot be empty.'); } const statusId = await ctx.db.insert('statuses', { message, userId, updatedBy, updatedAt: Date.now(), }); await ctx.db.patch(userId, { currentStatusId: statusId }); return { statusId }; }, }); /** * Bulk create the same status for many users. * - updatedBy defaults to the caller. * - Updates each user's currentStatusId pointer. */ export const bulkCreate = mutation({ args: { message: v.string(), userIds: v.array(v.id('users')), updatedBy: v.optional(v.id('users')), }, handler: async (ctx, args) => { const authUserId = await getAuthUserId(ctx); if (!authUserId) throw new ConvexError('Not authenticated.'); if (args.userIds.length === 0) return { statusIds: [] }; const updatedBy = args.updatedBy ?? authUserId; await ensureUser(ctx, updatedBy); const message = args.message.trim(); if (message.length === 0) { throw new ConvexError('Message cannot be empty.'); } const statusIds: Id<'statuses'>[] = []; const now = Date.now(); // Sequential to keep load predictable; switch to Promise.all // if your ownerIds lists are small and bounded. for (const userId of args.userIds) { await ensureUser(ctx, userId); const statusId = await ctx.db.insert('statuses', { message, userId, updatedBy, updatedAt: now, }); await ctx.db.patch(userId, { currentStatusId: statusId }); statusIds.push(statusId); } return { statusIds }; }, }); /** * Current status for a specific user. * - Uses users.currentStatusId if present, * otherwise falls back to latest by index. */ export const getCurrentForUser = query({ args: { userId: v.id('users') }, handler: async (ctx, { userId }) => { const user = await ensureUser(ctx, userId); if (user.currentStatusId) { const status = await ctx.db.get(user.currentStatusId); if (status) return status; } return await latestStatusForOwner(ctx, userId); }, }); const getName = (u: Doc<'users'>): string | null => 'name' in u && typeof u.name === 'string' ? u.name : null; const getImageId = (u: Doc<'users'>): Id<'_storage'> | null => { if (!('image' in u)) return null; const img = (u as { image?: unknown }).image as string | undefined; return img && img.length > 0 ? (img as Id<'_storage'>) : null; }; /** * Current statuses for all users. * - Reads each user's currentStatusId pointer. * - Falls back to latest-by-index if pointer is missing. */ export const getCurrentForAll = query({ args: {}, handler: async (ctx): Promise => { const users = await ctx.db.query('users').collect(); return await Promise.all( users.map(async (u) => { // Resolve user's current or latest status let curStatus: Doc<'statuses'> | null = null; if ('currentStatusId' in u && u.currentStatusId) { curStatus = await ctx.db.get(u.currentStatusId); } if (!curStatus) { const [latest] = await ctx.db .query('statuses') .withIndex('by_user_updatedAt', (q) => q.eq('userId', u._id)) .order('desc') .take(1); curStatus = latest ?? null; } // User display + URL const userImageId = getImageId(u); const userImageUrl = userImageId ? await ctx.storage.getUrl(userImageId) : null; // Updated by (if different) + URL let updatedByUser: StatusRow['user'] | null = null; if (curStatus && curStatus.updatedBy !== u._id) { const updater = await ctx.db.get(curStatus.updatedBy); if (!updater) throw new ConvexError('Updater not found.'); const updaterImageId = getImageId(updater); const updaterImageUrl = updaterImageId ? await ctx.storage.getUrl(updaterImageId) : null; updatedByUser = { id: updater._id, name: getName(updater), imageUrl: updaterImageUrl, }; } const status: StatusRow['status'] = curStatus ? { id: curStatus._id, message: curStatus.message, updatedAt: curStatus.updatedAt, updatedBy: updatedByUser, } : null; return { user: { id: u._id, name: getName(u), imageUrl: userImageUrl, }, status, }; }), ); }, }); /** * Paginated history for a specific user (newest first). */ export const listHistoryByUser = query({ args: { userId: v.id('users'), paginationOpts: paginationOptsValidator, }, handler: async (ctx, { userId, paginationOpts }) => { await ensureUser(ctx, userId); return await ctx.db .query('statuses') .withIndex('by_user_updatedAt', (q) => q.eq('userId', userId)) .order('desc') .paginate(paginationOpts); }, }); /** * Global paginated history (all users, newest first). * - Add an index on updatedAt if you want to avoid full-table scans * when the collection grows large. */ export const listHistoryAll = query({ args: { paginationOpts: paginationOptsValidator }, handler: async (ctx, { paginationOpts }) => { return await ctx.db .query('statuses') .order('desc') .paginate(paginationOpts); }, });