convex example testing (#943)

<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->

<!-- RECURSEML_SUMMARY:START -->
## 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

<details>
<summary>💡 Review Order Suggestion</summary>

| Order | File Path |
|-------|-----------|
| 1 | `examples/convex/package.json` |
| 2 | `pnpm-lock.yaml` |
</details>



[![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](c932fc0941/?repo_owner=stack-auth&repo_name=stack-auth&pr_number=943)
<!-- RECURSEML_SUMMARY:END -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
BilalG1 2025-10-15 15:50:04 -07:00 committed by GitHub
parent 0ac2b00e1f
commit 91d8c16ffc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 144 additions and 37 deletions

View File

@ -39,3 +39,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.env.local

View File

@ -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<string | null>(null);
return (
<div className="flex flex-col gap-8 max-w-lg mx-auto pt-10">
<div className="flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md">
<h2 className="text-xl font-bold">User read-only metadata</h2>
<code>
<pre>{JSON.stringify(user.clientReadOnlyMetadata, null, 2)}</pre>
</code>
</div>
<input type="text" placeholder="test 123" className="border border-slate-300 rounded-md p-2" onChange={(e) => setData(e.target.value)} />
<button
className="bg-foreground text-background text-sm px-4 py-2 rounded-md"
onClick={() => {
runAsynchronouslyWithAlert(async () => {
await myAction({ testMetadata: data ?? "" })
alert("User's client read-only metadata updated, refresh to see changes")
})
}}
>
My Action
</button>
</div>
)
}

View File

@ -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 (
<div className="flex flex-col gap-8 max-w-lg mx-auto">
<p>Welcome {viewer ?? "Anonymous"}!</p>
<button className="bg-foreground text-background text-sm px-4 py-2 rounded-md" onClick={() => {
alert(getUserInfo);
}}>
View user info
</button>
<div className="flex flex-col gap-2">
<button className="bg-foreground text-background text-sm px-4 py-2 rounded-md" onClick={() => {
alert(getUserInfo);
}}>
View user info
</button>
<button className="bg-foreground text-background text-sm px-4 py-2 rounded-md" onClick={() => {
getUserInfoConvexHttpClient().then(userInfo => {
alert(userInfo);
});
}}>
View user info (http client)
</button>
<button className="bg-foreground text-background text-sm px-4 py-2 rounded-md" onClick={() => {
getUserInfoConvexClient().then(userInfo => {
alert(userInfo);
});
}}>
View user info (js client)
</button>
</div>
<p>
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() {
</Link>{" "}
for an example of loading data in a server component
</p>
<p>
See the{" "}
<Link href="/action" className="underline hover:no-underline">
/action route
</Link>{" "}
for an example of using a convex action to update user data in stack auth
</p>
<div className="flex flex-col">
<p className="text-lg font-bold">Useful resources:</p>
<div className="flex gap-2">

View File

@ -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 (
<main className="p-8 flex flex-col gap-4 mx-auto max-w-2xl">
<h1 className="text-4xl font-bold text-center">Convex + Next.js</h1>
<div className="flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md">
<h2 className="text-xl font-bold">User info</h2>
<code>
<pre>{JSON.stringify(JSON.parse(userInfo), null, 2)}</pre>
</code>
</div>
<div className="flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md">
<h2 className="text-xl font-bold">Non-reactive server-loaded data</h2>
<code>

View File

@ -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;
}

View File

@ -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;

View File

@ -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,
})
},
});

View File

@ -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,
});
},
});

View File

@ -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"
}
}

View File

@ -1763,7 +1763,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
async getConvexHttpClientAuth(options: { tokenStore: TokenStoreInit }): Promise<string> {
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) {

View File

@ -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: