import { ConvexError, v } from 'convex/values'; import { getAuthUserId } from '@convex-dev/auth/server'; import { mutation, query } from './_generated/server'; import type { Doc, Id } from './_generated/dataModel'; import { paginationOptsValidator } from 'convex/server'; // NEW: import ctx and data model types import type { MutationCtx, QueryCtx } from './_generated/server'; // NEW: shared ctx type for helpers type RWCtx = MutationCtx | QueryCtx; // 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(), ownerIds: 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.ownerIds.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.ownerIds) { 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); }, }); /** * 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) => { const users = await ctx.db.query('users').collect(); const results = await Promise.all( users.map(async (u) => { let status = null; if (u.currentStatusId) { status = await ctx.db.get(u.currentStatusId); } status ??= await latestStatusForOwner(ctx, u._id); return { userId: u._id as Id<'users'>, status, }; }), ); return results; }, }); /** * 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); }, });