From 91d8c16ffcc238db0a77bacfef7276b9487cfac7 Mon Sep 17 00:00:00 2001 From: BilalG1 Date: Wed, 15 Oct 2025 15:50:04 -0700 Subject: [PATCH] convex example testing (#943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## High-level PR Summary This PR fixes dependency management issues by adding the missing `wait-on` package to the Convex example's dependencies, reorganizing the dependency order in `package.json` for consistency, and regenerating the `pnpm-lock.yaml` file to ensure proper dependency resolution across the monorepo. ⏱️ Estimated Review Time: 5-15 minutes
💡 Review Order Suggestion | Order | File Path | |-------|-----------| | 1 | `examples/convex/package.json` | | 2 | `pnpm-lock.yaml` |
[![Need help? Join our Discord](https://img.shields.io/badge/Need%20help%3F%20Join%20our%20Discord-5865F2?style=plastic&logo=discord&logoColor=white)](https://discord.gg/n3SsVDAW6U) [![Analyze latest changes](https://img.shields.io/badge/Analyze%20latest%20changes-238636?style=plastic)](https://squash-322339097191.europe-west3.run.app/interactive/c932fc0941a3f377a683c72390f091ad3b2c120001b7b49eaf2bab337e1efadc/?repo_owner=stack-auth&repo_name=stack-auth&pr_number=943) ## Summary by CodeRabbit - **New Features** - Added UI buttons to view user info via different clients, a server-side user info section, and an Action page to view/submit updates to user metadata. - Added a server-side action to update a user's client-read-only metadata. - **Documentation** - In-app link and guidance to the Action route for updating user data. - **Chores** - Updated project dependencies/devDependencies and added .env.local to .gitignore. - **Bug Fixes** - Token-missing scenario now handled gracefully instead of throwing. --------- Co-authored-by: Konsti Wohlwend --- examples/convex/.gitignore | 2 + examples/convex/app/action/page.tsx | 37 +++++++++++++++++++ examples/convex/app/page.tsx | 34 ++++++++++++++--- examples/convex/app/server/page.tsx | 13 +++++++ examples/convex/app/user-info.ts | 21 +++++++++++ examples/convex/convex/_generated/api.d.ts | 2 + examples/convex/convex/myActions.ts | 27 ++++++++++++++ examples/convex/convex/myFunctions.ts | 29 +-------------- examples/convex/package.json | 8 ++-- .../apps/implementations/client-app-impl.ts | 2 +- pnpm-lock.yaml | 6 +++ 11 files changed, 144 insertions(+), 37 deletions(-) create mode 100644 examples/convex/app/action/page.tsx create mode 100644 examples/convex/app/user-info.ts create mode 100644 examples/convex/convex/myActions.ts diff --git a/examples/convex/.gitignore b/examples/convex/.gitignore index bca563e19..625498e4d 100644 --- a/examples/convex/.gitignore +++ b/examples/convex/.gitignore @@ -39,3 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +.env.local diff --git a/examples/convex/app/action/page.tsx b/examples/convex/app/action/page.tsx new file mode 100644 index 000000000..e6091606c --- /dev/null +++ b/examples/convex/app/action/page.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useAction } from "convex/react"; +import { useState } from "react"; +import { api } from "@/convex/_generated/api"; +import { useUser } from "@stackframe/stack"; +import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; + +export default function Page() { + const myAction = useAction(api.myActions.myAction); + const user = useUser({ or: "redirect" }); + const [data, setData] = useState(null); + + + return ( +
+
+

User read-only metadata

+ +
{JSON.stringify(user.clientReadOnlyMetadata, null, 2)}
+
+
+ setData(e.target.value)} /> + +
+ ) +} diff --git a/examples/convex/app/page.tsx b/examples/convex/app/page.tsx index defc658ed..d66209cca 100644 --- a/examples/convex/app/page.tsx +++ b/examples/convex/app/page.tsx @@ -4,6 +4,7 @@ import { UserButton, useUser } from "@stackframe/stack"; import { useMutation, useQuery } from "convex/react"; import Link from "next/link"; import { api } from "../convex/_generated/api"; +import { getUserInfoConvexClient, getUserInfoConvexHttpClient } from "./user-info"; export default function Home() { const user = useUser(); @@ -41,11 +42,27 @@ function Content() { return (

Welcome {viewer ?? "Anonymous"}!

- +
+ + + +

Click the button below and open this page in another window - this data is persisted in the Convex cloud database! @@ -87,6 +104,13 @@ function Content() { {" "} for an example of loading data in a server component

+

+ See the{" "} + + /action route + {" "} + for an example of using a convex action to update user data in stack auth +

Useful resources:

diff --git a/examples/convex/app/server/page.tsx b/examples/convex/app/server/page.tsx index eb9a6cedd..a775dbeb1 100644 --- a/examples/convex/app/server/page.tsx +++ b/examples/convex/app/server/page.tsx @@ -1,6 +1,8 @@ import Home from "./inner"; import { preloadQuery, preloadedQueryResult } from "convex/nextjs"; import { api } from "@/convex/_generated/api"; +import { ConvexHttpClient } from "convex/browser"; +import { stackServerApp } from "@/stack/server"; export default async function ServerPage() { const preloaded = await preloadQuery(api.myFunctions.listNumbers, { @@ -9,9 +11,20 @@ export default async function ServerPage() { const data = preloadedQueryResult(preloaded); + const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + const token = await stackServerApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" }); + convex.setAuth(token); + const userInfo = await convex.query(api.myFunctions.getUserInfo, {}); + return (

Convex + Next.js

+
+

User info

+ +
{JSON.stringify(JSON.parse(userInfo), null, 2)}
+
+

Non-reactive server-loaded data

diff --git a/examples/convex/app/user-info.ts b/examples/convex/app/user-info.ts new file mode 100644 index 000000000..62254e8cf --- /dev/null +++ b/examples/convex/app/user-info.ts @@ -0,0 +1,21 @@ +import { api } from "@/convex/_generated/api"; +import { stackClientApp } from "@/stack/client"; +import { ConvexHttpClient, ConvexClient } from "convex/browser"; + +const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + +export async function getUserInfoConvexHttpClient() { + const token = await stackClientApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" }); + convexHttpClient.setAuth(token); + const userInfo = await convexHttpClient.query(api.myFunctions.getUserInfo, {}); + return userInfo; +} + + +const convexClient = new ConvexClient(process.env.NEXT_PUBLIC_CONVEX_URL!); +convexClient.setAuth(stackClientApp.getConvexClientAuth({ tokenStore: "nextjs-cookie" })) + +export async function getUserInfoConvexClient() { + const userInfo = await convexClient.query(api.myFunctions.getUserInfo, {}); + return userInfo; +} diff --git a/examples/convex/convex/_generated/api.d.ts b/examples/convex/convex/_generated/api.d.ts index ae44db071..8794141fb 100644 --- a/examples/convex/convex/_generated/api.d.ts +++ b/examples/convex/convex/_generated/api.d.ts @@ -8,6 +8,7 @@ * @module */ +import type * as myActions from "../myActions.js"; import type * as myFunctions from "../myFunctions.js"; import type { @@ -25,6 +26,7 @@ import type { * ``` */ declare const fullApi: ApiFromModules<{ + myActions: typeof myActions; myFunctions: typeof myFunctions; }>; declare const fullApiWithMounts: typeof fullApi; diff --git a/examples/convex/convex/myActions.ts b/examples/convex/convex/myActions.ts new file mode 100644 index 000000000..616edda3b --- /dev/null +++ b/examples/convex/convex/myActions.ts @@ -0,0 +1,27 @@ +"use node" + +import { v } from "convex/values"; +import { stackServerApp } from "../stack/server"; +import { action } from "./_generated/server"; + + +export const myAction = action({ + args: { + testMetadata: v.string(), + }, + + handler: async (ctx, args) => { + const partialUser = await stackServerApp.getPartialUser({ from: "convex", ctx }); + if (!partialUser) { + return null; + } + const user = await stackServerApp.getUser(partialUser?.id); + if (!user) { + return null; + } + await user.setClientReadOnlyMetadata({ + test: args.testMetadata, + }) + }, +}); + diff --git a/examples/convex/convex/myFunctions.ts b/examples/convex/convex/myFunctions.ts index 7f079f7e9..8fa218331 100644 --- a/examples/convex/convex/myFunctions.ts +++ b/examples/convex/convex/myFunctions.ts @@ -1,7 +1,7 @@ + import { v } from "convex/values"; import { stackClientApp } from "../stack/client"; import { stackServerApp } from "../stack/server"; -import { api } from "./_generated/api"; import { action, mutation, query } from "./_generated/server"; @@ -65,30 +65,3 @@ export const addNumber = mutation({ }, }); -// You can fetch data from and send data to third-party APIs via an action: -export const myAction = action({ - // Validators for arguments. - args: { - first: v.number(), - second: v.string(), - }, - - // Action implementation. - handler: async (ctx, args) => { - //// Use the browser-like `fetch` API to send HTTP requests. - //// See https://docs.convex.dev/functions/actions#calling-third-party-apis-and-using-npm-packages. - // const response = await ctx.fetch("https://api.thirdpartyservice.com"); - // const data = await response.json(); - - //// Query data by running Convex queries. - const data = await ctx.runQuery(api.myFunctions.listNumbers, { - count: 10, - }); - console.log(data); - - //// Write data by running Convex mutations. - await ctx.runMutation(api.myFunctions.addNumber, { - value: args.first, - }); - }, -}); diff --git a/examples/convex/package.json b/examples/convex/package.json index e91cc6c48..c165eecb9 100644 --- a/examples/convex/package.json +++ b/examples/convex/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@stackframe/stack": "workspace:*", + "@stackframe/stack-shared": "workspace:*", "convex": "^1.27.0", "next": "15.2.3", "react": "^19.0.0", @@ -31,11 +32,12 @@ "@types/react-dom": "^19", "eslint": "^8", "eslint-config-next": "14.2.5", - "rimraf": "^5.0.5", - "typescript": "5.3.3", "npm-run-all": "^4.1.5", + "postcss": "^8", "prettier": "^3.5.3", + "rimraf": "^5.0.5", "tailwindcss": "^4", - "postcss": "^8" + "typescript": "5.3.3", + "wait-on": "^8.0.1" } } diff --git a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts index 96092e160..d4d2d1016 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts @@ -1763,7 +1763,7 @@ export class _StackClientAppImplIncomplete { const session = await this._getSession(options.tokenStore); const tokens = await session.getOrFetchLikelyValidTokens(20_000); - return tokens?.accessToken.token ?? throwErr("No access token available"); + return tokens?.accessToken.token ?? ""; } protected async _updateClientUser(update: UserUpdateOptions, session: InternalSession) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6edb17856..928d883eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -759,6 +759,9 @@ importers: '@stackframe/stack': specifier: workspace:* version: link:../../packages/stack + '@stackframe/stack-shared': + specifier: workspace:* + version: link:../../packages/stack-shared convex: specifier: ^1.27.0 version: 1.27.0(react@19.0.0) @@ -811,6 +814,9 @@ importers: typescript: specifier: 5.3.3 version: 5.3.3 + wait-on: + specifier: ^8.0.1 + version: 8.0.1 examples/demo: dependencies: