From 19b77cb95f8f8cfee09b0e38851098393c94af9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adel=20Rodr=C3=ADguez?= Date: Thu, 19 Feb 2026 00:25:51 -0400 Subject: [PATCH] refactor: replace convex-helpers with fluent-convex --- bun.lock | 4 +- packages/backend/package.json | 2 +- .../backend/src/functions/_generated/api.d.ts | 16 +- .../{shared/auth/index.ts => auth.ts} | 8 +- .../components/better-auth/adapter.ts | 6 +- .../functions/components/better-auth/auth.ts | 8 +- .../components/better-auth/convex.config.ts | 2 +- .../components/better-auth/schema.ts | 2 +- packages/backend/src/functions/http.ts | 10 +- .../backend/src/functions/models/documents.ts | 29 ++++ .../backend/src/functions/private/users.ts | 12 +- packages/backend/src/functions/public/auth.ts | 10 ++ .../backend/src/functions/public/documents.ts | 6 +- .../backend/src/functions/public/messages.ts | 21 +-- .../shared/{auth/options.ts => auth.ts} | 14 +- .../backend/src/functions/shared/convex.ts | 152 ++++++------------ .../backend/src/functions/system/health.ts | 5 + 17 files changed, 160 insertions(+), 147 deletions(-) rename packages/backend/src/functions/{shared/auth/index.ts => auth.ts} (62%) create mode 100644 packages/backend/src/functions/models/documents.ts create mode 100644 packages/backend/src/functions/public/auth.ts rename packages/backend/src/functions/shared/{auth/options.ts => auth.ts} (74%) create mode 100644 packages/backend/src/functions/system/health.ts diff --git a/bun.lock b/bun.lock index 65c7d6bf..cd57423c 100644 --- a/bun.lock +++ b/bun.lock @@ -328,7 +328,7 @@ "@init/utils": "workspace:*", "@tanstack/react-query": "^5.90.20", "convex": "1.31.6", - "convex-helpers": "0.1.111", + "fluent-convex": "0.12.3", "qte": "0.1.0", "std-env": "3.10.0", }, @@ -3160,6 +3160,8 @@ "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], + "fluent-convex": ["fluent-convex@0.12.3", "", { "peerDependencies": { "convex": "^1.31.0", "convex-helpers": ">=0.1.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["convex-helpers", "zod"] }, "sha512-z3mYifNVUZJu8ZpXioZMS9vAU/dcUefVwD1hB1Z6aG1cT3hfwptB5PEXOItFRR2dMBrYJ00Hty9p2OI3PixX/A=="], + "fontace": ["fontace@0.4.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-moThBCItUe2bjZip5PF/iZClpKHGLwMvR79Kp8XpGRBrvoRSnySN4VcILdv3/MJzbhvUA5WeiUXF5o538m5fvg=="], "fontfaceobserver": ["fontfaceobserver@2.3.0", "", {}, "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="], diff --git a/packages/backend/package.json b/packages/backend/package.json index 66fa1fa9..606ddf4f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -31,7 +31,7 @@ "@init/utils": "workspace:*", "@tanstack/react-query": "^5.90.20", "convex": "1.31.6", - "convex-helpers": "0.1.111", + "fluent-convex": "0.12.3", "qte": "0.1.0", "std-env": "3.10.0" }, diff --git a/packages/backend/src/functions/_generated/api.d.ts b/packages/backend/src/functions/_generated/api.d.ts index 981a1c43..dc4e887a 100644 --- a/packages/backend/src/functions/_generated/api.d.ts +++ b/packages/backend/src/functions/_generated/api.d.ts @@ -8,15 +8,18 @@ * @module */ +import type * as auth from "../auth.js"; import type * as crons from "../crons.js"; import type * as http from "../http.js"; +import type * as models_documents from "../models/documents.js"; import type * as private_users from "../private/users.js"; +import type * as public_auth from "../public/auth.js"; import type * as public_documents from "../public/documents.js"; import type * as public_messages from "../public/messages.js"; -import type * as shared_auth_index from "../shared/auth/index.js"; -import type * as shared_auth_options from "../shared/auth/options.js"; +import type * as shared_auth from "../shared/auth.js"; import type * as shared_convex from "../shared/convex.js"; import type * as shared_env from "../shared/env.js"; +import type * as system_health from "../system/health.js"; import type { ApiFromModules, @@ -25,15 +28,18 @@ import type { } from "convex/server"; declare const fullApi: ApiFromModules<{ + auth: typeof auth; crons: typeof crons; http: typeof http; + "models/documents": typeof models_documents; "private/users": typeof private_users; + "public/auth": typeof public_auth; "public/documents": typeof public_documents; "public/messages": typeof public_messages; - "shared/auth/index": typeof shared_auth_index; - "shared/auth/options": typeof shared_auth_options; + "shared/auth": typeof shared_auth; "shared/convex": typeof shared_convex; "shared/env": typeof shared_env; + "system/health": typeof system_health; }>; /** @@ -63,7 +69,7 @@ export declare const internal: FilterApi< >; export declare const components: { - betterAuth: { + auth: { adapter: { create: FunctionReference< "mutation", diff --git a/packages/backend/src/functions/shared/auth/index.ts b/packages/backend/src/functions/auth.ts similarity index 62% rename from packages/backend/src/functions/shared/auth/index.ts rename to packages/backend/src/functions/auth.ts index f846aed4..4c27292a 100644 --- a/packages/backend/src/functions/shared/auth/index.ts +++ b/packages/backend/src/functions/auth.ts @@ -1,12 +1,16 @@ import type { GenericCtx } from "@convex-dev/better-auth" import { createAuth } from "@init/auth/server" import type { DataModel } from "#functions/_generated/dataModel.js" -import { authOptions } from "#functions/shared/auth/options.ts" +import { authComponent, createAuthOptions } from "#functions/shared/auth.ts" import env from "#functions/shared/env.ts" +export { authComponent } from "#functions/shared/auth.ts" + +export const { onCreate, onDelete, onUpdate } = authComponent.triggersApi() + export const convexAuth = (ctx: GenericCtx) => createAuth({ - ...authOptions(ctx), + ...createAuthOptions(ctx), baseURL: env.CONVEX_SITE_URL, secret: env.AUTH_SECRET, trustedOrigins: env.AUTH_TRUSTED_ORIGINS, diff --git a/packages/backend/src/functions/components/better-auth/adapter.ts b/packages/backend/src/functions/components/better-auth/adapter.ts index e06053fe..fd6443be 100644 --- a/packages/backend/src/functions/components/better-auth/adapter.ts +++ b/packages/backend/src/functions/components/better-auth/adapter.ts @@ -1,6 +1,6 @@ import { createApi } from "@convex-dev/better-auth" -import schema from "#functions/components/better-auth/schema.ts" -import { authOptions } from "#functions/shared/auth/options.ts" +import { createAuthOptions } from "#functions/shared/auth.ts" +import schema from "./schema" export const { create, findOne, findMany, updateOne, updateMany, deleteOne, deleteMany } = - createApi(schema, authOptions) + createApi(schema, createAuthOptions) diff --git a/packages/backend/src/functions/components/better-auth/auth.ts b/packages/backend/src/functions/components/better-auth/auth.ts index 06198158..0b104d1e 100644 --- a/packages/backend/src/functions/components/better-auth/auth.ts +++ b/packages/backend/src/functions/components/better-auth/auth.ts @@ -1,11 +1,9 @@ import type { GenericCtx } from "@convex-dev/better-auth" import { createAuth } from "@init/auth/server" import type { DataModel } from "#functions/_generated/dataModel.js" -import { authOptions } from "#functions/shared/auth/options.ts" +import { createAuthOptions } from "#functions/shared/auth.ts" -// Export a static instance for Better Auth schema generation +// Static instance for Better Auth schema generation only export const auth = createAuth({ - // Casting as GenericCtx since the Convex component does not need - // the running context - ...authOptions({} as GenericCtx), + ...createAuthOptions({} as GenericCtx), }) diff --git a/packages/backend/src/functions/components/better-auth/convex.config.ts b/packages/backend/src/functions/components/better-auth/convex.config.ts index 230e311d..2c55877f 100644 --- a/packages/backend/src/functions/components/better-auth/convex.config.ts +++ b/packages/backend/src/functions/components/better-auth/convex.config.ts @@ -1,5 +1,5 @@ import { defineComponent } from "convex/server" -const component = defineComponent("betterAuth") +const component = defineComponent("auth") export default component diff --git a/packages/backend/src/functions/components/better-auth/schema.ts b/packages/backend/src/functions/components/better-auth/schema.ts index e5e71fae..08d954af 100644 --- a/packages/backend/src/functions/components/better-auth/schema.ts +++ b/packages/backend/src/functions/components/better-auth/schema.ts @@ -1,4 +1,4 @@ import { defineSchema } from "convex/server" import { tables } from "#functions/components/better-auth/schema.generated.ts" -export default defineSchema({ ...tables }) +export default defineSchema({ ...tables, user: tables.user.index("by_email", ["email"]) }) diff --git a/packages/backend/src/functions/http.ts b/packages/backend/src/functions/http.ts index 7079c116..c878fa17 100644 --- a/packages/backend/src/functions/http.ts +++ b/packages/backend/src/functions/http.ts @@ -1,9 +1,13 @@ import { httpRouter } from "convex/server" -import { convexAuth } from "#functions/shared/auth/index.ts" -import { authComponent } from "#functions/shared/auth/options.ts" +import { authComponent, convexAuth } from "#functions/auth.ts" +import env from "#functions/shared/env.ts" const http = httpRouter() -authComponent.registerRoutes(http, convexAuth) +authComponent.registerRoutes(http, convexAuth, { + cors: { + allowedOrigins: env.AUTH_TRUSTED_ORIGINS, + }, +}) export default http diff --git a/packages/backend/src/functions/models/documents.ts b/packages/backend/src/functions/models/documents.ts new file mode 100644 index 00000000..240bc222 --- /dev/null +++ b/packages/backend/src/functions/models/documents.ts @@ -0,0 +1,29 @@ +import { UnauthenticatedError } from "@init/error" +import { v } from "convex/values" +import { protectedQuery, publicQuery } from "#functions/shared/convex.ts" + +export const getDocument = publicQuery + .input({ name: v.string() }) + .handler(async (ctx, { name }) => { + const document = await ctx.db + .query("documents") + .withIndex("by_name", (q) => q.eq("name", name)) + .unique() + + return document + }) + +export const withDocument = protectedQuery.createMiddleware(async (ctx, next) => { + const identity = await ctx.auth.getUserIdentity() + + if (!identity) { + throw new UnauthenticatedError() + } + + const document = await ctx.db + .query("documents") + .withIndex("by_name", (q) => q.eq("name", identity.subject)) + .unique() + + return next({ ...ctx, document }) +}) diff --git a/packages/backend/src/functions/private/users.ts b/packages/backend/src/functions/private/users.ts index 8a747529..c6309e53 100644 --- a/packages/backend/src/functions/private/users.ts +++ b/packages/backend/src/functions/private/users.ts @@ -1,14 +1,12 @@ import type { UserWithRole } from "@init/auth/server/plugins" -import { convexAuth } from "#functions/shared/auth/index.ts" -import { authComponent } from "#functions/shared/auth/options.ts" +import { authComponent, convexAuth } from "#functions/auth.ts" import { privateQuery } from "#functions/shared/convex.ts" -export const list = privateQuery({ - handler: async (ctx) => { +export const list = privateQuery + .handler(async (ctx) => { const { auth, headers } = await authComponent.getAuth(convexAuth, ctx) - const result = await auth.api.listUsers({ headers, query: { limit: 100 } }) return result.users as UserWithRole[] - }, -}) + }) + .public() diff --git a/packages/backend/src/functions/public/auth.ts b/packages/backend/src/functions/public/auth.ts new file mode 100644 index 00000000..68f1a5a2 --- /dev/null +++ b/packages/backend/src/functions/public/auth.ts @@ -0,0 +1,10 @@ +import { authComponent } from "#functions/shared/auth.ts" +import { publicQuery } from "#functions/shared/convex.ts" + +export const getCurrentUser = publicQuery + .handler(async (ctx) => { + const user = await authComponent.getAuthUser(ctx) + + return user ?? null + }) + .public() diff --git a/packages/backend/src/functions/public/documents.ts b/packages/backend/src/functions/public/documents.ts index 983984e3..ad23d5bc 100644 --- a/packages/backend/src/functions/public/documents.ts +++ b/packages/backend/src/functions/public/documents.ts @@ -1,5 +1,5 @@ import { publicQuery } from "#functions/shared/convex.ts" -export const list = publicQuery({ - handler: async (ctx) => await ctx.db.query("documents").collect(), -}) +export const list = publicQuery + .handler(async (ctx) => await ctx.db.query("documents").collect()) + .public() diff --git a/packages/backend/src/functions/public/messages.ts b/packages/backend/src/functions/public/messages.ts index 0c69303f..134d9bb8 100644 --- a/packages/backend/src/functions/public/messages.ts +++ b/packages/backend/src/functions/public/messages.ts @@ -1,10 +1,13 @@ -import { publicQuery, vv } from "#functions/shared/convex.ts" +import { v } from "convex/values" +import { publicQuery } from "#functions/shared/convex.ts" -export const list = publicQuery({ - args: { documentId: vv.id("documents") }, - handler: async (ctx, args) => - await ctx.db - .query("messages") - .withIndex("by_document_id", (q) => q.eq("documentId", args.documentId)) - .collect(), -}) +export const list = publicQuery + .input({ documentId: v.id("documents") }) + .handler( + async (ctx, args) => + await ctx.db + .query("messages") + .withIndex("by_document_id", (q) => q.eq("documentId", args.documentId)) + .collect() + ) + .public() diff --git a/packages/backend/src/functions/shared/auth/options.ts b/packages/backend/src/functions/shared/auth.ts similarity index 74% rename from packages/backend/src/functions/shared/auth/options.ts rename to packages/backend/src/functions/shared/auth.ts index 7d6012a3..65ca1d89 100644 --- a/packages/backend/src/functions/shared/auth/options.ts +++ b/packages/backend/src/functions/shared/auth.ts @@ -1,7 +1,4 @@ -// Separated this from the main auth.ts file to avoid environment variable -// errors when trying to use the auth options inside the better auth component.. - -import type { GenericCtx } from "@convex-dev/better-auth" +import type { AuthFunctions, GenericCtx } from "@convex-dev/better-auth" import type { AuthOptions } from "@init/auth/server" import { createClient } from "@convex-dev/better-auth" import { convex } from "@convex-dev/better-auth/plugins" @@ -9,15 +6,18 @@ import { admin, anonymous, organization } from "@init/auth/server/plugins" import { APP_ID, APP_NAME } from "@init/utils/constants" import { seconds } from "qte" import type { DataModel } from "#functions/_generated/dataModel.js" -import { components } from "#functions/_generated/api.js" +import { components, internal } from "#functions/_generated/api.js" import authConfig from "#functions/auth.config.ts" import authSchema from "#functions/components/better-auth/schema.ts" -export const authComponent = createClient(components.betterAuth, { +const authFunctions: AuthFunctions = internal.auth + +export const authComponent = createClient(components.auth, { + authFunctions, local: { schema: authSchema }, }) -export const authOptions = (ctx: GenericCtx) => +export const createAuthOptions = (ctx: GenericCtx) => ({ advanced: { cookiePrefix: APP_ID, diff --git a/packages/backend/src/functions/shared/convex.ts b/packages/backend/src/functions/shared/convex.ts index 488df60d..b591554d 100644 --- a/packages/backend/src/functions/shared/convex.ts +++ b/packages/backend/src/functions/shared/convex.ts @@ -1,115 +1,69 @@ import { UnauthenticatedError, UnauthorizedError } from "@init/error" -import { getLogger, LoggerCategory } from "@init/observability/logger" -import { - customAction, - customCtx, - customMutation, - customQuery, -} from "convex-helpers/server/customFunctions" -import { typedV } from "convex-helpers/validators" -import { - type ActionCtx, - action, - type MutationCtx, - mutation, - type QueryCtx, - query, -} from "#functions/_generated/server.js" -import schema from "#functions/schema.ts" -import { convexAuth } from "#functions/shared/auth/index.ts" - -export const vv = typedV(schema) - -const baseContext = { - logger: getLogger(LoggerCategory.CONVEX), -} - -async function validateIdentity(ctx: QueryCtx | MutationCtx | ActionCtx) { +import { buildLogger, LoggerCategory } from "@init/observability/logger" +import { singleton } from "@init/utils/singleton" +import { createBuilder } from "fluent-convex" +import type { DataModel } from "#functions/_generated/dataModel.js" +import type { ActionCtx, MutationCtx, QueryCtx } from "#functions/_generated/server.js" +import { authComponent } from "#functions/shared/auth.ts" + +export const convex = createBuilder() + +export const withLogger = convex.createMiddleware((ctx, next) => + next({ ...ctx, logger: singleton("logger:convex", () => buildLogger([LoggerCategory.CONVEX])) }) +) + +export type GenericCtx = QueryCtx | ActionCtx | MutationCtx + +export const withAuthentication = convex + .$context() + .createMiddleware(async (ctx, next) => { + const identity = await ctx.auth.getUserIdentity() + + if (!identity) { + throw new UnauthenticatedError() + } + + const authUser = await authComponent.getAuthUser(ctx) + + if (!authUser) { + throw new UnauthenticatedError() + } + + return next({ ...ctx, authUser, identity }) + }) + +export const withAdmin = convex.$context().createMiddleware(async (ctx, next) => { const identity = await ctx.auth.getUserIdentity() if (!identity) { throw new UnauthenticatedError() } - return identity -} - -async function validateAdmin(ctx: QueryCtx | MutationCtx | ActionCtx) { - const identity = await ctx.auth.getUserIdentity() + const authUser = await authComponent.getAuthUser(ctx) - if (!identity) { + if (!authUser) { throw new UnauthenticatedError() } - if (identity.role !== "admin") { - throw new UnauthorizedError({ userId: identity.tokenIdentifier }) + if (authUser.role !== "admin") { + throw new UnauthorizedError({ userId: identity.subject }) } - return identity -} + return next({ ...ctx, authUser, identity }) +}) -export const publicQuery = customQuery( - query, - customCtx(() => baseContext) -) -export const publicMutation = customMutation( - mutation, - customCtx(() => baseContext) -) -export const publicAction = customAction( - action, - customCtx(() => baseContext) -) +export const publicQuery = convex.query().use(withLogger) +export const publicMutation = convex.mutation().use(withLogger) +export const publicAction = convex.action().use(withLogger) -export const protectedQuery = customQuery( - query, - customCtx(async (ctx) => ({ - ...baseContext, - auth: { - ...ctx.auth, - ...convexAuth(ctx), - }, - identity: await validateIdentity(ctx), - })) -) -export const protectedMutation = customMutation( - mutation, - customCtx(async (ctx) => ({ - ...baseContext, - auth: { ...ctx.auth, ...convexAuth(ctx) }, - identity: await validateIdentity(ctx), - })) -) -export const protectedAction = customAction( - action, - customCtx(async (ctx) => ({ - ...baseContext, - auth: { ...ctx.auth, ...convexAuth(ctx) }, - identity: await validateIdentity(ctx), - })) -) +export const protectedQuery = convex.query().use(withLogger).use(withAuthentication) +export const protectedMutation = convex.mutation().use(withLogger).use(withAuthentication) +export const protectedAction = convex.action().use(withLogger).use(withAuthentication) -export const privateQuery = customQuery( - query, - customCtx(async (ctx) => ({ - ...baseContext, - auth: { ...ctx.auth, ...convexAuth(ctx) }, - identity: await validateAdmin(ctx), - })) -) -export const privateMutation = customMutation( - mutation, - customCtx(async (ctx) => ({ - ...baseContext, - auth: { ...ctx.auth, ...convexAuth(ctx) }, - identity: await validateAdmin(ctx), - })) -) -export const privateAction = customAction( - action, - customCtx(async (ctx) => ({ - ...baseContext, - auth: { ...ctx.auth, ...convexAuth(ctx) }, - identity: await validateAdmin(ctx), - })) -) +export const privateQuery = convex.query().use(withLogger).use(withAdmin) +export const privateMutation = convex.mutation().use(withLogger).use(withAdmin) +export const privateAction = convex.action().use(withLogger).use(withAdmin) + +export const internalQuery = convex.query().use(withLogger) +export const internalMutation = convex.mutation().use(withLogger) +export const internalAction = convex.action().use(withLogger) diff --git a/packages/backend/src/functions/system/health.ts b/packages/backend/src/functions/system/health.ts new file mode 100644 index 00000000..17356102 --- /dev/null +++ b/packages/backend/src/functions/system/health.ts @@ -0,0 +1,5 @@ +import { internalQuery } from "#functions/shared/convex.ts" + +export const ping = internalQuery + .handler(async () => ({ ok: true, timestamp: Date.now() })) + .internal()