From a8bbfebd00e01fc330838ad0d24eedfb7b4a33e4 Mon Sep 17 00:00:00 2001 From: gibbyb Date: Fri, 19 Sep 2025 21:25:37 -0500 Subject: [PATCH] scheduling functions now works --- .../components/providers/lunch-reminder.tsx | 4 +- packages/backend/convex/_generated/api.d.ts | 32 ++-- packages/backend/convex/_generated/api.js | 2 +- .../backend/convex/_generated/dataModel.d.ts | 6 +- .../backend/convex/_generated/server.d.ts | 16 +- packages/backend/convex/_generated/server.js | 2 +- packages/backend/convex/statuses.ts | 177 ++++++++---------- 7 files changed, 105 insertions(+), 134 deletions(-) diff --git a/apps/next/src/components/providers/lunch-reminder.tsx b/apps/next/src/components/providers/lunch-reminder.tsx index 348ea6e..2743f9c 100644 --- a/apps/next/src/components/providers/lunch-reminder.tsx +++ b/apps/next/src/components/providers/lunch-reminder.tsx @@ -14,7 +14,7 @@ const nextOccurrenceMs = (hhmm: string, from = new Date()): number => { }; export const LunchReminder = () => { - const setStatus = useMutation(api.statuses.create); + const setStatus = useMutation(api.statuses.createLunchStatus); const timeoutRef = useRef(null); const user = useQuery(api.auth.getUser); const lunchTime = user?.lunchTime ?? ''; @@ -44,7 +44,7 @@ export const LunchReminder = () => { description: 'Would you like to set your status to "At lunch"?', action: { label: 'Set to lunch', - onClick: () => void setStatus({ message: 'At lunch' }), + onClick: () => void setStatus(), }, cancel: { label: 'Not now', diff --git a/packages/backend/convex/_generated/api.d.ts b/packages/backend/convex/_generated/api.d.ts index c0f71f4..2567445 100644 --- a/packages/backend/convex/_generated/api.d.ts +++ b/packages/backend/convex/_generated/api.d.ts @@ -12,16 +12,16 @@ import type { ApiFromModules, FilterApi, FunctionReference, -} from 'convex/server'; -import type * as auth from '../auth.js'; -import type * as crons from '../crons.js'; -import type * as custom_auth_index from '../custom/auth/index.js'; -import type * as custom_auth_password_validate from '../custom/auth/password/validate.js'; -import type * as custom_auth_providers_entra from '../custom/auth/providers/entra.js'; -import type * as custom_auth_providers_password from '../custom/auth/providers/password.js'; -import type * as files from '../files.js'; -import type * as http from '../http.js'; -import type * as statuses from '../statuses.js'; +} from "convex/server"; +import type * as auth from "../auth.js"; +import type * as crons from "../crons.js"; +import type * as custom_auth_index from "../custom/auth/index.js"; +import type * as custom_auth_password_validate from "../custom/auth/password/validate.js"; +import type * as custom_auth_providers_entra from "../custom/auth/providers/entra.js"; +import type * as custom_auth_providers_password from "../custom/auth/providers/password.js"; +import type * as files from "../files.js"; +import type * as http from "../http.js"; +import type * as statuses from "../statuses.js"; /** * A utility for referencing Convex functions in your app's API. @@ -34,19 +34,19 @@ import type * as statuses from '../statuses.js'; declare const fullApi: ApiFromModules<{ auth: typeof auth; crons: typeof crons; - 'custom/auth/index': typeof custom_auth_index; - 'custom/auth/password/validate': typeof custom_auth_password_validate; - 'custom/auth/providers/entra': typeof custom_auth_providers_entra; - 'custom/auth/providers/password': typeof custom_auth_providers_password; + "custom/auth/index": typeof custom_auth_index; + "custom/auth/password/validate": typeof custom_auth_password_validate; + "custom/auth/providers/entra": typeof custom_auth_providers_entra; + "custom/auth/providers/password": typeof custom_auth_providers_password; files: typeof files; http: typeof http; statuses: typeof statuses; }>; export declare const api: FilterApi< typeof fullApi, - FunctionReference + FunctionReference >; export declare const internal: FilterApi< typeof fullApi, - FunctionReference + FunctionReference >; diff --git a/packages/backend/convex/_generated/api.js b/packages/backend/convex/_generated/api.js index 2e31a22..3f9c482 100644 --- a/packages/backend/convex/_generated/api.js +++ b/packages/backend/convex/_generated/api.js @@ -8,7 +8,7 @@ * @module */ -import { anyApi } from 'convex/server'; +import { anyApi } from "convex/server"; /** * A utility for referencing Convex functions in your app's API. diff --git a/packages/backend/convex/_generated/dataModel.d.ts b/packages/backend/convex/_generated/dataModel.d.ts index afe7956..8541f31 100644 --- a/packages/backend/convex/_generated/dataModel.d.ts +++ b/packages/backend/convex/_generated/dataModel.d.ts @@ -13,9 +13,9 @@ import type { DocumentByName, TableNamesInDataModel, SystemTableNames, -} from 'convex/server'; -import type { GenericId } from 'convex/values'; -import schema from '../schema.js'; +} from "convex/server"; +import type { GenericId } from "convex/values"; +import schema from "../schema.js"; /** * The names of all of your Convex tables. diff --git a/packages/backend/convex/_generated/server.d.ts b/packages/backend/convex/_generated/server.d.ts index ad25580..7f337a4 100644 --- a/packages/backend/convex/_generated/server.d.ts +++ b/packages/backend/convex/_generated/server.d.ts @@ -18,8 +18,8 @@ import { GenericQueryCtx, GenericDatabaseReader, GenericDatabaseWriter, -} from 'convex/server'; -import type { DataModel } from './dataModel.js'; +} from "convex/server"; +import type { DataModel } from "./dataModel.js"; /** * Define a query in this Convex app's public API. @@ -29,7 +29,7 @@ import type { DataModel } from './dataModel.js'; * @param func - The query function. It receives a {@link QueryCtx} as its first argument. * @returns The wrapped query. Include this as an `export` to name it and make it accessible. */ -export declare const query: QueryBuilder; +export declare const query: QueryBuilder; /** * Define a query that is only accessible from other Convex functions (but not from the client). @@ -39,7 +39,7 @@ export declare const query: QueryBuilder; * @param func - The query function. It receives a {@link QueryCtx} as its first argument. * @returns The wrapped query. Include this as an `export` to name it and make it accessible. */ -export declare const internalQuery: QueryBuilder; +export declare const internalQuery: QueryBuilder; /** * Define a mutation in this Convex app's public API. @@ -49,7 +49,7 @@ export declare const internalQuery: QueryBuilder; * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. */ -export declare const mutation: MutationBuilder; +export declare const mutation: MutationBuilder; /** * Define a mutation that is only accessible from other Convex functions (but not from the client). @@ -59,7 +59,7 @@ export declare const mutation: MutationBuilder; * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. */ -export declare const internalMutation: MutationBuilder; +export declare const internalMutation: MutationBuilder; /** * Define an action in this Convex app's public API. @@ -72,7 +72,7 @@ export declare const internalMutation: MutationBuilder; * @param func - The action. It receives an {@link ActionCtx} as its first argument. * @returns The wrapped action. Include this as an `export` to name it and make it accessible. */ -export declare const action: ActionBuilder; +export declare const action: ActionBuilder; /** * Define an action that is only accessible from other Convex functions (but not from the client). @@ -80,7 +80,7 @@ export declare const action: ActionBuilder; * @param func - The function. It receives an {@link ActionCtx} as its first argument. * @returns The wrapped function. Include this as an `export` to name it and make it accessible. */ -export declare const internalAction: ActionBuilder; +export declare const internalAction: ActionBuilder; /** * Define an HTTP action. diff --git a/packages/backend/convex/_generated/server.js b/packages/backend/convex/_generated/server.js index 4651d7a..566d485 100644 --- a/packages/backend/convex/_generated/server.js +++ b/packages/backend/convex/_generated/server.js @@ -16,7 +16,7 @@ import { internalActionGeneric, internalMutationGeneric, internalQueryGeneric, -} from 'convex/server'; +} from "convex/server"; /** * Define a query in this Convex app's public API. diff --git a/packages/backend/convex/statuses.ts b/packages/backend/convex/statuses.ts index fdbc081..6aec71b 100644 --- a/packages/backend/convex/statuses.ts +++ b/packages/backend/convex/statuses.ts @@ -12,7 +12,6 @@ import type { Doc, Id } from './_generated/dataModel'; import { paginationOptsValidator } from 'convex/server'; type RWCtx = MutationCtx | QueryCtx; - type StatusRow = { user: { id: Id<'users'>; @@ -34,28 +33,24 @@ type Paginated = { continueCursor: string | 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; +const getName = (u: Doc<'users'>): string | null => + 'name' in u && typeof u.name === 'string' ? u.name : null; + +const getEmail = (u: Doc<'users'>): string | null => + 'email' in u && typeof u.email === 'string' ? u.email : 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; }; -/** - * 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(), @@ -64,12 +59,14 @@ export const create = mutation({ }, handler: async (ctx, args) => { const authUserId = await getAuthUserId(ctx); - if (!authUserId) throw new ConvexError('Not authenticated.'); + if (!args.userId && !authUserId) { + throw new ConvexError('Not authenticated.'); + } + + const userId = args.userId ?? authUserId!; + const updatedBy = args.updatedBy ?? authUserId!; - const userId = args.userId ?? authUserId; await ensureUser(ctx, userId); - - const updatedBy = args.updatedBy ?? authUserId; await ensureUser(ctx, updatedBy); const message = args.message.trim(); @@ -85,16 +82,10 @@ export const create = mutation({ }); 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(), @@ -118,91 +109,94 @@ export const bulkCreate = mutation({ 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 }; }, }); -/** - * Update all statuses for all users. - */ +// Update all users - simplified export const updateAllStatuses = mutation({ args: { message: v.string() }, handler: async (ctx, args) => { - const userIds = await ctx.runQuery(api.auth.getAllUserIds); - const updatedAt = Date.now(); + const users = await ctx.db.query('users').collect(); + const message = args.message.trim(); + if (message.length === 0) { + throw new ConvexError('Message cannot be empty.'); + } + const statusIds: Id<'statuses'>[] = []; - for (const userId of userIds) { - await ensureUser(ctx, userId); + const now = Date.now(); + + for (const user of users) { const statusId = await ctx.db.insert('statuses', { - message: args.message, - userId, - updatedAt, + message, + userId: user._id, + updatedAt: now, }); - await ctx.db.patch(userId, { currentStatusId: statusId }); + await ctx.db.patch(user._id, { 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. - */ +// Lunch status with automatic return - this should be an action +export const createLunchStatus = mutation({ + args: {}, + handler: async (ctx) => { + const authUserId = await getAuthUserId(ctx); + if (!authUserId) throw new ConvexError('Not authenticated.'); + // Create lunch status + await ctx.runMutation(api.statuses.create, { + message: 'At lunch', + userId: authUserId + }); + const oneHour = 60 * 60 * 1000; + console.log('Scheduling return to desk after 1 hour'); + await ctx.scheduler.runAfter(oneHour, api.statuses.create, { + message: 'At desk', + userId: authUserId + }); + return { success: true }; + }, +}); + 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; } + const [latest] = await ctx.db + .query('statuses') + .withIndex('by_user_updatedAt', (q) => q.eq('userId', userId)) + .order('desc') + .take(1); - return await latestStatusForOwner(ctx, userId); + return latest ?? null; }, }); -const getName = (u: Doc<'users'>): string | null => - 'name' in u && typeof u.name === 'string' ? u.name : null; -const getEmail = (u: Doc<'users'>): string | null => - 'email' in u && typeof u.email === 'string' ? u.email : 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); @@ -215,30 +209,26 @@ export const getCurrentForAll = query({ .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 && 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, - email: getEmail(updater), - name: getName(updater), - imageUrl: updaterImageUrl, - }; + if (updater) { + const updaterImageId = getImageId(updater); + const updaterImageUrl = updaterImageId + ? await ctx.storage.getUrl(updaterImageId) + : null; + updatedByUser = { + id: updater._id, + email: getEmail(updater), + name: getName(updater), + imageUrl: updaterImageUrl, + }; + } } - const status: StatusRow['status'] = curStatus ? { id: curStatus._id, @@ -247,7 +237,6 @@ export const getCurrentForAll = query({ updatedBy: updatedByUser, } : null; - return { user: { id: u._id, @@ -262,19 +251,13 @@ export const getCurrentForAll = query({ }, }); -/** - * Paginated history for all users or for a specific user. - */ +// Paginated history export const listHistory = query({ args: { userId: v.optional(v.id('users')), paginationOpts: paginationOptsValidator, }, - handler: async ( - ctx, - { userId, paginationOpts }, - ): Promise> => { - // Query statuses newest-first, optionally filtered by user + handler: async (ctx, { userId, paginationOpts }): Promise> => { const result = userId ? await ctx.db .query('statuses') @@ -282,21 +265,15 @@ export const listHistory = query({ .order('desc') .paginate(paginationOpts) : await ctx.db.query('statuses').order('desc').paginate(paginationOpts); - - // Cache user display objects to avoid refetching repeatedly const displayCache = new Map(); - const getDisplay = async (uid: Id<'users'>): Promise => { const key = uid as unknown as string; const cached = displayCache.get(key); if (cached) return cached; - const user = await ctx.db.get(uid); if (!user) throw new ConvexError('User not found.'); - const imgId = getImageId(user); const imgUrl = imgId ? await ctx.storage.getUrl(imgId) : null; - const display: StatusRow['user'] = { id: user._id, email: getEmail(user), @@ -306,7 +283,6 @@ export const listHistory = query({ displayCache.set(key, display); return display; }; - const statuses: StatusRow[] = []; for (const s of result.page) { const owner = await getDisplay(s.userId); @@ -314,7 +290,6 @@ export const listHistory = query({ s.updatedBy && s.updatedBy !== s.userId ? await getDisplay(s.updatedBy) : null; - statuses.push({ user: owner, status: { @@ -325,12 +300,8 @@ export const listHistory = query({ }, }); } - const page = statuses.sort( - (a, b) => (b.status?.updatedAt ?? 0) - (a.status?.updatedAt ?? 0), - ); - return { - page, + page: statuses, isDone: result.isDone, continueCursor: result.continueCursor, };