scheduling functions now works
This commit is contained in:
@@ -14,7 +14,7 @@ const nextOccurrenceMs = (hhmm: string, from = new Date()): number => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const LunchReminder = () => {
|
export const LunchReminder = () => {
|
||||||
const setStatus = useMutation(api.statuses.create);
|
const setStatus = useMutation(api.statuses.createLunchStatus);
|
||||||
const timeoutRef = useRef<number | null>(null);
|
const timeoutRef = useRef<number | null>(null);
|
||||||
const user = useQuery(api.auth.getUser);
|
const user = useQuery(api.auth.getUser);
|
||||||
const lunchTime = user?.lunchTime ?? '';
|
const lunchTime = user?.lunchTime ?? '';
|
||||||
@@ -44,7 +44,7 @@ export const LunchReminder = () => {
|
|||||||
description: 'Would you like to set your status to "At lunch"?',
|
description: 'Would you like to set your status to "At lunch"?',
|
||||||
action: {
|
action: {
|
||||||
label: 'Set to lunch',
|
label: 'Set to lunch',
|
||||||
onClick: () => void setStatus({ message: 'At lunch' }),
|
onClick: () => void setStatus(),
|
||||||
},
|
},
|
||||||
cancel: {
|
cancel: {
|
||||||
label: 'Not now',
|
label: 'Not now',
|
||||||
|
32
packages/backend/convex/_generated/api.d.ts
vendored
32
packages/backend/convex/_generated/api.d.ts
vendored
@@ -12,16 +12,16 @@ import type {
|
|||||||
ApiFromModules,
|
ApiFromModules,
|
||||||
FilterApi,
|
FilterApi,
|
||||||
FunctionReference,
|
FunctionReference,
|
||||||
} from 'convex/server';
|
} from "convex/server";
|
||||||
import type * as auth from '../auth.js';
|
import type * as auth from "../auth.js";
|
||||||
import type * as crons from '../crons.js';
|
import type * as crons from "../crons.js";
|
||||||
import type * as custom_auth_index from '../custom/auth/index.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_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_entra from "../custom/auth/providers/entra.js";
|
||||||
import type * as custom_auth_providers_password from '../custom/auth/providers/password.js';
|
import type * as custom_auth_providers_password from "../custom/auth/providers/password.js";
|
||||||
import type * as files from '../files.js';
|
import type * as files from "../files.js";
|
||||||
import type * as http from '../http.js';
|
import type * as http from "../http.js";
|
||||||
import type * as statuses from '../statuses.js';
|
import type * as statuses from "../statuses.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility for referencing Convex functions in your app's API.
|
* 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<{
|
declare const fullApi: ApiFromModules<{
|
||||||
auth: typeof auth;
|
auth: typeof auth;
|
||||||
crons: typeof crons;
|
crons: typeof crons;
|
||||||
'custom/auth/index': typeof custom_auth_index;
|
"custom/auth/index": typeof custom_auth_index;
|
||||||
'custom/auth/password/validate': typeof custom_auth_password_validate;
|
"custom/auth/password/validate": typeof custom_auth_password_validate;
|
||||||
'custom/auth/providers/entra': typeof custom_auth_providers_entra;
|
"custom/auth/providers/entra": typeof custom_auth_providers_entra;
|
||||||
'custom/auth/providers/password': typeof custom_auth_providers_password;
|
"custom/auth/providers/password": typeof custom_auth_providers_password;
|
||||||
files: typeof files;
|
files: typeof files;
|
||||||
http: typeof http;
|
http: typeof http;
|
||||||
statuses: typeof statuses;
|
statuses: typeof statuses;
|
||||||
}>;
|
}>;
|
||||||
export declare const api: FilterApi<
|
export declare const api: FilterApi<
|
||||||
typeof fullApi,
|
typeof fullApi,
|
||||||
FunctionReference<any, 'public'>
|
FunctionReference<any, "public">
|
||||||
>;
|
>;
|
||||||
export declare const internal: FilterApi<
|
export declare const internal: FilterApi<
|
||||||
typeof fullApi,
|
typeof fullApi,
|
||||||
FunctionReference<any, 'internal'>
|
FunctionReference<any, "internal">
|
||||||
>;
|
>;
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { anyApi } from 'convex/server';
|
import { anyApi } from "convex/server";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility for referencing Convex functions in your app's API.
|
* A utility for referencing Convex functions in your app's API.
|
||||||
|
@@ -13,9 +13,9 @@ import type {
|
|||||||
DocumentByName,
|
DocumentByName,
|
||||||
TableNamesInDataModel,
|
TableNamesInDataModel,
|
||||||
SystemTableNames,
|
SystemTableNames,
|
||||||
} from 'convex/server';
|
} from "convex/server";
|
||||||
import type { GenericId } from 'convex/values';
|
import type { GenericId } from "convex/values";
|
||||||
import schema from '../schema.js';
|
import schema from "../schema.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The names of all of your Convex tables.
|
* The names of all of your Convex tables.
|
||||||
|
16
packages/backend/convex/_generated/server.d.ts
vendored
16
packages/backend/convex/_generated/server.d.ts
vendored
@@ -18,8 +18,8 @@ import {
|
|||||||
GenericQueryCtx,
|
GenericQueryCtx,
|
||||||
GenericDatabaseReader,
|
GenericDatabaseReader,
|
||||||
GenericDatabaseWriter,
|
GenericDatabaseWriter,
|
||||||
} from 'convex/server';
|
} from "convex/server";
|
||||||
import type { DataModel } from './dataModel.js';
|
import type { DataModel } from "./dataModel.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a query in this Convex app's public API.
|
* 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.
|
* @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.
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
*/
|
*/
|
||||||
export declare const query: QueryBuilder<DataModel, 'public'>;
|
export declare const query: QueryBuilder<DataModel, "public">;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
* 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<DataModel, 'public'>;
|
|||||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
* @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.
|
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||||
*/
|
*/
|
||||||
export declare const internalQuery: QueryBuilder<DataModel, 'internal'>;
|
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a mutation in this Convex app's public API.
|
* Define a mutation in this Convex app's public API.
|
||||||
@@ -49,7 +49,7 @@ export declare const internalQuery: QueryBuilder<DataModel, 'internal'>;
|
|||||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
* @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.
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
*/
|
*/
|
||||||
export declare const mutation: MutationBuilder<DataModel, 'public'>;
|
export declare const mutation: MutationBuilder<DataModel, "public">;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
* 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<DataModel, 'public'>;
|
|||||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
* @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.
|
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||||
*/
|
*/
|
||||||
export declare const internalMutation: MutationBuilder<DataModel, 'internal'>;
|
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define an action in this Convex app's public API.
|
* Define an action in this Convex app's public API.
|
||||||
@@ -72,7 +72,7 @@ export declare const internalMutation: MutationBuilder<DataModel, 'internal'>;
|
|||||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
* @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.
|
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||||
*/
|
*/
|
||||||
export declare const action: ActionBuilder<DataModel, 'public'>;
|
export declare const action: ActionBuilder<DataModel, "public">;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
* 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<DataModel, 'public'>;
|
|||||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
* @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.
|
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||||
*/
|
*/
|
||||||
export declare const internalAction: ActionBuilder<DataModel, 'internal'>;
|
export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define an HTTP action.
|
* Define an HTTP action.
|
||||||
|
@@ -16,7 +16,7 @@ import {
|
|||||||
internalActionGeneric,
|
internalActionGeneric,
|
||||||
internalMutationGeneric,
|
internalMutationGeneric,
|
||||||
internalQueryGeneric,
|
internalQueryGeneric,
|
||||||
} from 'convex/server';
|
} from "convex/server";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a query in this Convex app's public API.
|
* Define a query in this Convex app's public API.
|
||||||
|
@@ -12,7 +12,6 @@ import type { Doc, Id } from './_generated/dataModel';
|
|||||||
import { paginationOptsValidator } from 'convex/server';
|
import { paginationOptsValidator } from 'convex/server';
|
||||||
|
|
||||||
type RWCtx = MutationCtx | QueryCtx;
|
type RWCtx = MutationCtx | QueryCtx;
|
||||||
|
|
||||||
type StatusRow = {
|
type StatusRow = {
|
||||||
user: {
|
user: {
|
||||||
id: Id<'users'>;
|
id: Id<'users'>;
|
||||||
@@ -34,28 +33,24 @@ type Paginated<T> = {
|
|||||||
continueCursor: string | null;
|
continueCursor: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// CHANGED: typed helpers
|
|
||||||
const ensureUser = async (ctx: RWCtx, userId: Id<'users'>) => {
|
const ensureUser = async (ctx: RWCtx, userId: Id<'users'>) => {
|
||||||
const user = await ctx.db.get(userId);
|
const user = await ctx.db.get(userId);
|
||||||
if (!user) throw new ConvexError('User not found.');
|
if (!user) throw new ConvexError('User not found.');
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
const latestStatusForOwner = async (ctx: RWCtx, ownerId: Id<'users'>) => {
|
const getName = (u: Doc<'users'>): string | null =>
|
||||||
const [latest] = await ctx.db
|
'name' in u && typeof u.name === 'string' ? u.name : null;
|
||||||
.query('statuses')
|
|
||||||
.withIndex('by_user_updatedAt', (q) => q.eq('userId', ownerId))
|
const getEmail = (u: Doc<'users'>): string | null =>
|
||||||
.order('desc')
|
'email' in u && typeof u.email === 'string' ? u.email : null;
|
||||||
.take(1);
|
|
||||||
return latest as Doc<'statuses'> | 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({
|
export const create = mutation({
|
||||||
args: {
|
args: {
|
||||||
message: v.string(),
|
message: v.string(),
|
||||||
@@ -64,12 +59,14 @@ export const create = mutation({
|
|||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const authUserId = await getAuthUserId(ctx);
|
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);
|
await ensureUser(ctx, userId);
|
||||||
|
|
||||||
const updatedBy = args.updatedBy ?? authUserId;
|
|
||||||
await ensureUser(ctx, updatedBy);
|
await ensureUser(ctx, updatedBy);
|
||||||
|
|
||||||
const message = args.message.trim();
|
const message = args.message.trim();
|
||||||
@@ -85,16 +82,10 @@ export const create = mutation({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await ctx.db.patch(userId, { currentStatusId: statusId });
|
await ctx.db.patch(userId, { currentStatusId: statusId });
|
||||||
|
|
||||||
return { 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({
|
export const bulkCreate = mutation({
|
||||||
args: {
|
args: {
|
||||||
message: v.string(),
|
message: v.string(),
|
||||||
@@ -118,91 +109,94 @@ export const bulkCreate = mutation({
|
|||||||
const statusIds: Id<'statuses'>[] = [];
|
const statusIds: Id<'statuses'>[] = [];
|
||||||
const now = Date.now();
|
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) {
|
for (const userId of args.userIds) {
|
||||||
await ensureUser(ctx, userId);
|
await ensureUser(ctx, userId);
|
||||||
|
|
||||||
const statusId = await ctx.db.insert('statuses', {
|
const statusId = await ctx.db.insert('statuses', {
|
||||||
message,
|
message,
|
||||||
userId,
|
userId,
|
||||||
updatedBy,
|
updatedBy,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.db.patch(userId, { currentStatusId: statusId });
|
await ctx.db.patch(userId, { currentStatusId: statusId });
|
||||||
statusIds.push(statusId);
|
statusIds.push(statusId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { statusIds };
|
return { statusIds };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
// Update all users - simplified
|
||||||
* Update all statuses for all users.
|
|
||||||
*/
|
|
||||||
export const updateAllStatuses = mutation({
|
export const updateAllStatuses = mutation({
|
||||||
args: { message: v.string() },
|
args: { message: v.string() },
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const userIds = await ctx.runQuery(api.auth.getAllUserIds);
|
const users = await ctx.db.query('users').collect();
|
||||||
const updatedAt = Date.now();
|
const message = args.message.trim();
|
||||||
|
if (message.length === 0) {
|
||||||
|
throw new ConvexError('Message cannot be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
const statusIds: Id<'statuses'>[] = [];
|
const statusIds: Id<'statuses'>[] = [];
|
||||||
for (const userId of userIds) {
|
const now = Date.now();
|
||||||
await ensureUser(ctx, userId);
|
|
||||||
|
for (const user of users) {
|
||||||
const statusId = await ctx.db.insert('statuses', {
|
const statusId = await ctx.db.insert('statuses', {
|
||||||
message: args.message,
|
message,
|
||||||
userId,
|
userId: user._id,
|
||||||
updatedAt,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
await ctx.db.patch(userId, { currentStatusId: statusId });
|
await ctx.db.patch(user._id, { currentStatusId: statusId });
|
||||||
statusIds.push(statusId);
|
statusIds.push(statusId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { statusIds };
|
return { statusIds };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
// Lunch status with automatic return - this should be an action
|
||||||
* Current status for a specific user.
|
export const createLunchStatus = mutation({
|
||||||
* - Uses users.currentStatusId if present,
|
args: {},
|
||||||
* otherwise falls back to latest by index.
|
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({
|
export const getCurrentForUser = query({
|
||||||
args: { userId: v.id('users') },
|
args: { userId: v.id('users') },
|
||||||
handler: async (ctx, { userId }) => {
|
handler: async (ctx, { userId }) => {
|
||||||
const user = await ensureUser(ctx, userId);
|
const user = await ensureUser(ctx, userId);
|
||||||
|
|
||||||
if (user.currentStatusId) {
|
if (user.currentStatusId) {
|
||||||
const status = await ctx.db.get(user.currentStatusId);
|
const status = await ctx.db.get(user.currentStatusId);
|
||||||
if (status) return status;
|
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({
|
export const getCurrentForAll = query({
|
||||||
args: {},
|
args: {},
|
||||||
handler: async (ctx): Promise<StatusRow[]> => {
|
handler: async (ctx): Promise<StatusRow[]> => {
|
||||||
const users = await ctx.db.query('users').collect();
|
const users = await ctx.db.query('users').collect();
|
||||||
|
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
users.map(async (u) => {
|
users.map(async (u) => {
|
||||||
// Resolve user's current or latest status
|
|
||||||
let curStatus: Doc<'statuses'> | null = null;
|
let curStatus: Doc<'statuses'> | null = null;
|
||||||
if ('currentStatusId' in u && u.currentStatusId) {
|
if ('currentStatusId' in u && u.currentStatusId) {
|
||||||
curStatus = await ctx.db.get(u.currentStatusId);
|
curStatus = await ctx.db.get(u.currentStatusId);
|
||||||
@@ -215,30 +209,26 @@ export const getCurrentForAll = query({
|
|||||||
.take(1);
|
.take(1);
|
||||||
curStatus = latest ?? null;
|
curStatus = latest ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// User display + URL
|
|
||||||
const userImageId = getImageId(u);
|
const userImageId = getImageId(u);
|
||||||
const userImageUrl = userImageId
|
const userImageUrl = userImageId
|
||||||
? await ctx.storage.getUrl(userImageId)
|
? await ctx.storage.getUrl(userImageId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Updated by (if different) + URL
|
|
||||||
let updatedByUser: StatusRow['user'] | null = null;
|
let updatedByUser: StatusRow['user'] | null = null;
|
||||||
if (curStatus && curStatus.updatedBy && curStatus.updatedBy !== u._id) {
|
if (curStatus && curStatus.updatedBy && curStatus.updatedBy !== u._id) {
|
||||||
const updater = await ctx.db.get(curStatus.updatedBy);
|
const updater = await ctx.db.get(curStatus.updatedBy);
|
||||||
if (!updater) throw new ConvexError('Updater not found.');
|
if (updater) {
|
||||||
const updaterImageId = getImageId(updater);
|
const updaterImageId = getImageId(updater);
|
||||||
const updaterImageUrl = updaterImageId
|
const updaterImageUrl = updaterImageId
|
||||||
? await ctx.storage.getUrl(updaterImageId)
|
? await ctx.storage.getUrl(updaterImageId)
|
||||||
: null;
|
: null;
|
||||||
updatedByUser = {
|
updatedByUser = {
|
||||||
id: updater._id,
|
id: updater._id,
|
||||||
email: getEmail(updater),
|
email: getEmail(updater),
|
||||||
name: getName(updater),
|
name: getName(updater),
|
||||||
imageUrl: updaterImageUrl,
|
imageUrl: updaterImageUrl,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const status: StatusRow['status'] = curStatus
|
const status: StatusRow['status'] = curStatus
|
||||||
? {
|
? {
|
||||||
id: curStatus._id,
|
id: curStatus._id,
|
||||||
@@ -247,7 +237,6 @@ export const getCurrentForAll = query({
|
|||||||
updatedBy: updatedByUser,
|
updatedBy: updatedByUser,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: u._id,
|
id: u._id,
|
||||||
@@ -262,19 +251,13 @@ export const getCurrentForAll = query({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
// Paginated history
|
||||||
* Paginated history for all users or for a specific user.
|
|
||||||
*/
|
|
||||||
export const listHistory = query({
|
export const listHistory = query({
|
||||||
args: {
|
args: {
|
||||||
userId: v.optional(v.id('users')),
|
userId: v.optional(v.id('users')),
|
||||||
paginationOpts: paginationOptsValidator,
|
paginationOpts: paginationOptsValidator,
|
||||||
},
|
},
|
||||||
handler: async (
|
handler: async (ctx, { userId, paginationOpts }): Promise<Paginated<StatusRow>> => {
|
||||||
ctx,
|
|
||||||
{ userId, paginationOpts },
|
|
||||||
): Promise<Paginated<StatusRow>> => {
|
|
||||||
// Query statuses newest-first, optionally filtered by user
|
|
||||||
const result = userId
|
const result = userId
|
||||||
? await ctx.db
|
? await ctx.db
|
||||||
.query('statuses')
|
.query('statuses')
|
||||||
@@ -282,21 +265,15 @@ export const listHistory = query({
|
|||||||
.order('desc')
|
.order('desc')
|
||||||
.paginate(paginationOpts)
|
.paginate(paginationOpts)
|
||||||
: await ctx.db.query('statuses').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<string, StatusRow['user']>();
|
const displayCache = new Map<string, StatusRow['user']>();
|
||||||
|
|
||||||
const getDisplay = async (uid: Id<'users'>): Promise<StatusRow['user']> => {
|
const getDisplay = async (uid: Id<'users'>): Promise<StatusRow['user']> => {
|
||||||
const key = uid as unknown as string;
|
const key = uid as unknown as string;
|
||||||
const cached = displayCache.get(key);
|
const cached = displayCache.get(key);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const user = await ctx.db.get(uid);
|
const user = await ctx.db.get(uid);
|
||||||
if (!user) throw new ConvexError('User not found.');
|
if (!user) throw new ConvexError('User not found.');
|
||||||
|
|
||||||
const imgId = getImageId(user);
|
const imgId = getImageId(user);
|
||||||
const imgUrl = imgId ? await ctx.storage.getUrl(imgId) : null;
|
const imgUrl = imgId ? await ctx.storage.getUrl(imgId) : null;
|
||||||
|
|
||||||
const display: StatusRow['user'] = {
|
const display: StatusRow['user'] = {
|
||||||
id: user._id,
|
id: user._id,
|
||||||
email: getEmail(user),
|
email: getEmail(user),
|
||||||
@@ -306,7 +283,6 @@ export const listHistory = query({
|
|||||||
displayCache.set(key, display);
|
displayCache.set(key, display);
|
||||||
return display;
|
return display;
|
||||||
};
|
};
|
||||||
|
|
||||||
const statuses: StatusRow[] = [];
|
const statuses: StatusRow[] = [];
|
||||||
for (const s of result.page) {
|
for (const s of result.page) {
|
||||||
const owner = await getDisplay(s.userId);
|
const owner = await getDisplay(s.userId);
|
||||||
@@ -314,7 +290,6 @@ export const listHistory = query({
|
|||||||
s.updatedBy && s.updatedBy !== s.userId
|
s.updatedBy && s.updatedBy !== s.userId
|
||||||
? await getDisplay(s.updatedBy)
|
? await getDisplay(s.updatedBy)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
statuses.push({
|
statuses.push({
|
||||||
user: owner,
|
user: owner,
|
||||||
status: {
|
status: {
|
||||||
@@ -325,12 +300,8 @@ export const listHistory = query({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const page = statuses.sort(
|
|
||||||
(a, b) => (b.status?.updatedAt ?? 0) - (a.status?.updatedAt ?? 0),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
page,
|
page: statuses,
|
||||||
isDone: result.isDone,
|
isDone: result.isDone,
|
||||||
continueCursor: result.continueCursor,
|
continueCursor: result.continueCursor,
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user