Move to monorepo for React Native!

This commit is contained in:
2025-09-12 16:44:21 -05:00
parent 4cafc11422
commit b1eae564be
144 changed files with 2535 additions and 311 deletions

2
packages/backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env.local
.env

View File

@@ -0,0 +1,18 @@
import { ConvexError } from 'convex/values';
import { Password } from '@convex-dev/auth/providers/Password';
import { validatePassword } from './auth';
import type { DataModel } from './_generated/dataModel';
export default Password<DataModel>({
profile(params, ctx) {
return {
email: params.email as string,
name: params.name as string,
};
},
validatePasswordRequirements: (password: string) => {
if (!validatePassword(password)) {
throw new ConvexError('Invalid password.');
}
},
});

View File

@@ -0,0 +1,90 @@
# Welcome to your Convex functions directory!
Write your Convex functions here.
See https://docs.convex.dev/functions for more.
A query function that takes two arguments looks like:
```ts
// functions.js
import { query } from "./_generated/server";
import { v } from "convex/values";
export const myQueryFunction = query({
// Validators for arguments.
args: {
first: v.number(),
second: v.string(),
},
// Function implementation.
handler: async (ctx, args) => {
// Read the database as many times as you need here.
// See https://docs.convex.dev/database/reading-data.
const documents = await ctx.db.query("tablename").collect();
// Arguments passed from the client are properties of the args object.
console.log(args.first, args.second);
// Write arbitrary JavaScript here: filter, aggregate, build derived data,
// remove non-public properties, or create new objects.
return documents;
},
});
```
Using this query function in a React component looks like:
```ts
const data = useQuery(api.functions.myQueryFunction, {
first: 10,
second: "hello",
});
```
A mutation function looks like:
```ts
// functions.js
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const myMutationFunction = mutation({
// Validators for arguments.
args: {
first: v.string(),
second: v.string(),
},
// Function implementation.
handler: async (ctx, args) => {
// Insert or modify documents in the database here.
// Mutations can also read from the database like queries.
// See https://docs.convex.dev/database/writing-data.
const message = { body: args.first, author: args.second };
const id = await ctx.db.insert("messages", message);
// Optionally, return a value from your mutation.
return await ctx.db.get(id);
},
});
```
Using this mutation function in a React component looks like:
```ts
const mutation = useMutation(api.functions.myMutationFunction);
function handleButtonPress() {
// fire and forget, the most common way to use mutations
mutation({ first: "Hello!", second: "me" });
// OR
// use the result once the mutation has completed
mutation({ first: "Hello!", second: "me" }).then((result) =>
console.log(result),
);
}
```
Use the Convex CLI to push your functions to a deployment. See everything
the Convex CLI can do by running `npx convex -h` in your project root
directory. To learn more, launch the docs with `npx convex docs`.

View File

@@ -0,0 +1,46 @@
/* eslint-disable */
/**
* Generated `api` utility.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import type {
ApiFromModules,
FilterApi,
FunctionReference,
} from "convex/server";
import type * as CustomPassword from "../CustomPassword.js";
import type * as auth from "../auth.js";
import type * as crons from "../crons.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.
*
* Usage:
* ```js
* const myFunctionReference = api.myModule.myFunction;
* ```
*/
declare const fullApi: ApiFromModules<{
CustomPassword: typeof CustomPassword;
auth: typeof auth;
crons: typeof crons;
files: typeof files;
http: typeof http;
statuses: typeof statuses;
}>;
export declare const api: FilterApi<
typeof fullApi,
FunctionReference<any, "public">
>;
export declare const internal: FilterApi<
typeof fullApi,
FunctionReference<any, "internal">
>;

View File

@@ -0,0 +1,22 @@
/* eslint-disable */
/**
* Generated `api` utility.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import { anyApi } from "convex/server";
/**
* A utility for referencing Convex functions in your app's API.
*
* Usage:
* ```js
* const myFunctionReference = api.myModule.myFunction;
* ```
*/
export const api = anyApi;
export const internal = anyApi;

View File

@@ -0,0 +1,60 @@
/* eslint-disable */
/**
* Generated data model types.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import type {
DataModelFromSchemaDefinition,
DocumentByName,
TableNamesInDataModel,
SystemTableNames,
} from "convex/server";
import type { GenericId } from "convex/values";
import schema from "../schema.js";
/**
* The names of all of your Convex tables.
*/
export type TableNames = TableNamesInDataModel<DataModel>;
/**
* The type of a document stored in Convex.
*
* @typeParam TableName - A string literal type of the table name (like "users").
*/
export type Doc<TableName extends TableNames> = DocumentByName<
DataModel,
TableName
>;
/**
* An identifier for a document in Convex.
*
* Convex documents are uniquely identified by their `Id`, which is accessible
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
*
* Documents can be loaded using `db.get(id)` in query and mutation functions.
*
* IDs are just strings at runtime, but this type can be used to distinguish them from other
* strings when type checking.
*
* @typeParam TableName - A string literal type of the table name (like "users").
*/
export type Id<TableName extends TableNames | SystemTableNames> =
GenericId<TableName>;
/**
* A type describing your Convex data model.
*
* This type includes information about what tables you have, the type of
* documents stored in those tables, and the indexes defined on them.
*
* This type is used to parameterize methods like `queryGeneric` and
* `mutationGeneric` to make them type-safe.
*/
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;

View File

@@ -0,0 +1,142 @@
/* eslint-disable */
/**
* Generated utilities for implementing server-side Convex query and mutation functions.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import {
ActionBuilder,
HttpActionBuilder,
MutationBuilder,
QueryBuilder,
GenericActionCtx,
GenericMutationCtx,
GenericQueryCtx,
GenericDatabaseReader,
GenericDatabaseWriter,
} from "convex/server";
import type { DataModel } from "./dataModel.js";
/**
* Define a query in this Convex app's public API.
*
* This function will be allowed to read your Convex database and will be accessible from the client.
*
* @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<DataModel, "public">;
/**
* Define a query that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
*
* @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<DataModel, "internal">;
/**
* Define a mutation in this Convex app's public API.
*
* This function will be allowed to modify your Convex database and will be accessible from the client.
*
* @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<DataModel, "public">;
/**
* Define a mutation that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
*
* @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<DataModel, "internal">;
/**
* Define an action in this Convex app's public API.
*
* An action is a function which can execute any JavaScript code, including non-deterministic
* code and code with side-effects, like calling third-party services.
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
*
* @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<DataModel, "public">;
/**
* Define an action that is only accessible from other Convex functions (but not from the client).
*
* @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<DataModel, "internal">;
/**
* Define an HTTP action.
*
* This function will be used to respond to HTTP requests received by a Convex
* deployment if the requests matches the path and method where this action
* is routed. Be sure to route your action in `convex/http.js`.
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
*/
export declare const httpAction: HttpActionBuilder;
/**
* A set of services for use within Convex query functions.
*
* The query context is passed as the first argument to any Convex query
* function run on the server.
*
* This differs from the {@link MutationCtx} because all of the services are
* read-only.
*/
export type QueryCtx = GenericQueryCtx<DataModel>;
/**
* A set of services for use within Convex mutation functions.
*
* The mutation context is passed as the first argument to any Convex mutation
* function run on the server.
*/
export type MutationCtx = GenericMutationCtx<DataModel>;
/**
* A set of services for use within Convex action functions.
*
* The action context is passed as the first argument to any Convex action
* function run on the server.
*/
export type ActionCtx = GenericActionCtx<DataModel>;
/**
* An interface to read from the database within Convex query functions.
*
* The two entry points are {@link DatabaseReader.get}, which fetches a single
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
* building a query.
*/
export type DatabaseReader = GenericDatabaseReader<DataModel>;
/**
* An interface to read from and write to the database within Convex mutation
* functions.
*
* Convex guarantees that all writes within a single mutation are
* executed atomically, so you never have to worry about partial writes leaving
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
* for the guarantees Convex provides your functions.
*/
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;

View File

@@ -0,0 +1,89 @@
/* eslint-disable */
/**
* Generated utilities for implementing server-side Convex query and mutation functions.
*
* THIS CODE IS AUTOMATICALLY GENERATED.
*
* To regenerate, run `npx convex dev`.
* @module
*/
import {
actionGeneric,
httpActionGeneric,
queryGeneric,
mutationGeneric,
internalActionGeneric,
internalMutationGeneric,
internalQueryGeneric,
} from "convex/server";
/**
* Define a query in this Convex app's public API.
*
* This function will be allowed to read your Convex database and will be accessible from the client.
*
* @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 const query = queryGeneric;
/**
* Define a query that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
*
* @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 const internalQuery = internalQueryGeneric;
/**
* Define a mutation in this Convex app's public API.
*
* This function will be allowed to modify your Convex database and will be accessible from the client.
*
* @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 const mutation = mutationGeneric;
/**
* Define a mutation that is only accessible from other Convex functions (but not from the client).
*
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
*
* @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 const internalMutation = internalMutationGeneric;
/**
* Define an action in this Convex app's public API.
*
* An action is a function which can execute any JavaScript code, including non-deterministic
* code and code with side-effects, like calling third-party services.
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
*
* @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 const action = actionGeneric;
/**
* Define an action that is only accessible from other Convex functions (but not from the client).
*
* @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 const internalAction = internalActionGeneric;
/**
* Define a Convex HTTP action.
*
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
* as its second.
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
*/
export const httpAction = httpActionGeneric;

View File

@@ -0,0 +1,8 @@
export default {
providers: [
{
domain: process.env.CONVEX_SITE_URL,
applicationID: 'convex',
},
],
};

View File

@@ -0,0 +1,138 @@
import { ConvexError, v } from 'convex/values';
import {
convexAuth,
getAuthUserId,
retrieveAccount,
modifyAccountCredentials,
} from '@convex-dev/auth/server';
import { api } from './_generated/api';
import { type Id } from './_generated/dataModel';
import { action, mutation, query } from './_generated/server';
import Password from './CustomPassword';
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [Password],
});
export const PASSWORD_MIN = 8;
export const PASSWORD_MAX = 100;
export const PASSWORD_REGEX =
/^(?=.{8,100}$)(?!.*\s)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\p{P}\p{S}]).*$/u;
export const getUser = query(async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) return null;
const user = await ctx.db.get(userId);
if (!user) throw new ConvexError('User not found.');
const image: Id<'_storage'> | null =
typeof user.image === 'string' && user.image.length > 0
? (user.image as Id<'_storage'>)
: null;
return {
id: user._id,
email: user.email ?? null,
name: user.name ?? null,
image,
};
});
export const getAllUsers = query(async (ctx) => {
const users = await ctx.db.query('users').collect();
return users.map((u) => ({
id: u._id,
email: u.email ?? null,
name: u.name ?? null,
image: u.image ?? null,
}));
});
export const getAllUserIds = query(async (ctx) => {
const users = await ctx.db.query('users').collect();
const userIds = users.map((u) => u._id);
return userIds;
});
export const updateUserName = mutation({
args: {
name: v.string(),
},
handler: async (ctx, { name }) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError('Not authenticated.');
const user = await ctx.db.get(userId);
if (!user) throw new ConvexError('User not found.');
await ctx.db.patch(userId, { name });
return { success: true };
},
});
export const updateUserEmail = mutation({
args: {
email: v.string(),
},
handler: async (ctx, { email }) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError('Not authenticated.');
const user = await ctx.db.get(userId);
if (!user) throw new ConvexError('User not found.');
await ctx.db.patch(userId, { email });
return { success: true };
},
});
export const updateUserImage = mutation({
args: {
storageId: v.id('_storage'),
},
handler: async (ctx, { storageId }) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError('Not authenticated.');
const user = await ctx.db.get(userId);
if (!user) throw new ConvexError('User not found.');
const oldImage = user.image as Id<'_storage'> | undefined;
await ctx.db.patch(userId, { image: storageId });
if (oldImage && oldImage !== storageId) await ctx.storage.delete(oldImage);
return { success: true };
},
});
export const validatePassword = (password: string): boolean => {
if (
password.length < 8 ||
password.length > 100 ||
!/\d/.test(password) ||
!/[a-z]/.test(password) ||
!/[A-Z]/.test(password)
) {
return false;
}
return true;
};
export const updateUserPassword = action({
args: {
currentPassword: v.string(),
newPassword: v.string(),
},
handler: async (ctx, { currentPassword, newPassword }) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError('Not authenticated.');
const user = await ctx.runQuery(api.auth.getUser);
if (!user?.email) throw new ConvexError('User not found.');
const verified = await retrieveAccount(ctx, {
provider: 'password',
account: { id: user.email, secret: currentPassword },
});
if (!verified) throw new ConvexError('Current password is incorrect.');
if (!validatePassword(newPassword))
throw new ConvexError('Invalid password.');
await modifyAccountCredentials(ctx, {
provider: 'password',
account: { id: user.email, secret: newPassword },
});
return { success: true };
},
});

View File

@@ -0,0 +1,15 @@
// convex/crons.ts
import { cronJobs } from 'convex/server';
import { api } from './_generated/api';
const crons = cronJobs();
// Runs at 5:00 PM America/Chicago, MondayFriday.
// Convex will handle DST if your project version supports `timeZone`.
crons.cron(
'End of shift (weekdays 5pm CT)',
'0 22 * * 1-5',
api.statuses.endOfShiftUpdate,
);
export default crons;

View File

@@ -0,0 +1,17 @@
import { mutation, query } from './_generated/server';
import { ConvexError, v } from 'convex/values';
import { getAuthUserId } from '@convex-dev/auth/server';
export const generateUploadUrl = mutation(async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError('Not authenticated.');
return await ctx.storage.generateUploadUrl();
});
export const getImageUrl = query({
args: { storageId: v.id('_storage') },
handler: async (ctx, { storageId }) => {
const url = await ctx.storage.getUrl(storageId);
return url ?? null;
},
});

View File

@@ -0,0 +1,8 @@
import { httpRouter } from 'convex/server';
import { auth } from './auth';
const http = httpRouter();
auth.addHttpRoutes(http);
export default http;

View File

@@ -0,0 +1,30 @@
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
import { authTables } from '@convex-dev/auth/server';
// The schema is normally optional, but Convex Auth
// requires indexes defined on `authTables`.
// The schema provides more precise TypeScript types.
export default defineSchema({
...authTables,
users: defineTable({
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.string()),
currentStatusId: v.optional(v.id('statuses')),
emailVerificationTime: v.optional(v.number()),
phone: v.optional(v.string()),
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
})
.index('email', ['email'])
.index('phone', ['phone']),
statuses: defineTable({
userId: v.id('users'),
message: v.string(),
updatedAt: v.number(),
updatedBy: v.optional(v.id('users')),
})
.index('by_user', ['userId'])
.index('by_user_updatedAt', ['userId', 'updatedAt']),
});

View File

@@ -0,0 +1,361 @@
import { ConvexError, v } from 'convex/values';
import { getAuthUserId } from '@convex-dev/auth/server';
import {
type MutationCtx,
type QueryCtx,
action,
internalMutation,
mutation,
query,
} from './_generated/server';
import { api } from './_generated/api';
import type { Doc, Id } from './_generated/dataModel';
import { paginationOptsValidator } from 'convex/server';
type RWCtx = MutationCtx | QueryCtx;
type StatusRow = {
user: {
id: Id<'users'>;
email: string | null;
name: string | null;
imageUrl: string | null;
};
status: {
id: Id<'statuses'>;
message: string;
updatedAt: number;
updatedBy: StatusRow['user'] | null;
} | null;
};
type Paginated<T> = {
page: T[];
isDone: boolean;
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;
};
/**
* 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 };
},
});
/**
* Update all statuses for all users.
*/
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 statusIds: Id<'statuses'>[] = [];
for (const userId of userIds) {
await ensureUser(ctx, userId);
const statusId = await ctx.db.insert('statuses', {
message: args.message,
userId,
updatedAt,
});
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 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<StatusRow[]> => {
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 && 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,
};
}
const status: StatusRow['status'] = curStatus
? {
id: curStatus._id,
message: curStatus.message,
updatedAt: curStatus.updatedAt,
updatedBy: updatedByUser,
}
: null;
return {
user: {
id: u._id,
email: getEmail(u),
name: getName(u),
imageUrl: userImageUrl,
},
status,
};
}),
);
},
});
/**
* Paginated history for all users or for a specific user.
*/
export const listHistory = query({
args: {
userId: v.optional(v.id('users')),
paginationOpts: paginationOptsValidator,
},
handler: async (
ctx,
{ userId, paginationOpts },
): Promise<Paginated<StatusRow>> => {
// Query statuses newest-first, optionally filtered by user
const result = userId
? await ctx.db
.query('statuses')
.withIndex('by_user_updatedAt', (q) => q.eq('userId', userId))
.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 getDisplay = async (uid: Id<'users'>): Promise<StatusRow['user']> => {
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),
name: getName(user),
imageUrl: imgUrl,
};
displayCache.set(key, display);
return display;
};
const statuses: StatusRow[] = [];
for (const s of result.page) {
const owner = await getDisplay(s.userId);
const updatedBy =
s.updatedBy && s.updatedBy !== s.userId
? await getDisplay(s.updatedBy)
: null;
statuses.push({
user: owner,
status: {
id: s._id,
message: s.message,
updatedAt: s.updatedAt,
updatedBy,
},
});
}
const page = statuses.sort(
(a, b) => (b.status?.updatedAt ?? 0) - (a.status?.updatedAt ?? 0),
);
return {
page,
isDone: result.isDone,
continueCursor: result.continueCursor,
};
},
});
export const endOfShiftUpdate = action({
handler: async (ctx) => {
const now = new Date(
new Date().toLocaleString('en-US', {
timeZone: 'America/Chicago',
}),
);
const day = now.getDay();
const hour = now.getHours();
const minute = now.getMinutes();
if (day == 0 || day === 6) return;
if (hour === 5) {
await ctx.runMutation(api.statuses.updateAllStatuses, {
message: 'End of shift',
});
} else if (hour === 4) {
const ms = ((60 - minute) % 60) * 60 * 1000;
await ctx.scheduler.runAfter(ms, api.statuses.endOfShiftUpdate);
} else return;
},
});

View File

@@ -0,0 +1,25 @@
{
/* This TypeScript project config describes the environment that
* Convex functions run in and is used to typecheck them.
* You can modify it, but some settings required to use Convex.
*/
"compilerOptions": {
/* These settings are not required by Convex and can be modified. */
"allowJs": true,
"strict": true,
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
/* These compiler options are required by Convex */
"target": "ESNext",
"lib": ["ES2021", "dom"],
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"isolatedModules": true,
"noEmit": true
},
"include": ["./**/*"],
"exclude": ["./_generated"]
}

View File

@@ -0,0 +1,18 @@
{
"name": "@techtracker/convex",
"version": "1.0.0",
"description": "Convex Backend for Tech Tracker",
"scripts": {
"dev": "convex dev",
"predev": "convex dev --until-success && convex dev --once --run-sh \"node setup.mjs --once\" && convex dashboard",
"setup": "convex dev --until-success"
},
"author": "Gib",
"license": "ISC",
"dependencies": {
"convex": "^1.27.0"
},
"devDependencies": {
"typescript": "5.9.2"
}
}

View File

@@ -0,0 +1,35 @@
/**
* This script runs `npx @convex-dev/auth` to help with setting up
* environment variables for Convex Auth.
*
* You can safely delete it and remove it from package.json scripts.
*/
import fs from "fs";
import { config as loadEnvFile } from "dotenv";
import { spawnSync } from "child_process";
if (!fs.existsSync(".env.local")) {
// Something is off, skip the script.
process.exit(0);
}
const config = {};
loadEnvFile({ path: ".env.local", processEnv: config });
const runOnceWorkflow = process.argv.includes("--once");
if (runOnceWorkflow && config.SETUP_SCRIPT_RAN !== undefined) {
// The script has already ran once, skip.
process.exit(0);
}
const result = spawnSync("npx", ["@convex-dev/auth", "--skip-git-check"], {
stdio: "inherit",
});
if (runOnceWorkflow) {
fs.writeFileSync(".env.local", `\nSETUP_SCRIPT_RAN=1\n`, { flag: "a" });
}
process.exit(result.status);

View File

@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "es2022",
"allowJs": true,
"composite": true,
"declaration": true,
"emitDeclarationOnly": false,
"moduleResolution": "bundler",
"module": "ESNext",
"jsx": "preserve",
"strict": true
},
"include": ["./**/*.ts", "./**/*.tsx"]
}