use-convex 0.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -35
- package/dist/module.d.mts +5 -3
- package/dist/module.mjs +35 -1
- package/dist/runtime/composables/useAuth.d.ts +5 -5
- package/dist/runtime/composables/useAuth.js +11 -24
- package/dist/runtime/composables/useAuthToken.d.ts +1 -1
- package/dist/runtime/composables/useAuthToken.js +2 -10
- package/dist/runtime/composables/useConvex.js +1 -1
- package/dist/runtime/composables/useConvexAction.js +6 -18
- package/dist/runtime/composables/useConvexAuth.js +4 -13
- package/dist/runtime/composables/useConvexFileUpload.d.ts +43 -0
- package/dist/runtime/composables/useConvexFileUpload.js +102 -0
- package/dist/runtime/composables/useConvexGate.js +3 -9
- package/dist/runtime/composables/useConvexMutation.js +7 -17
- package/dist/runtime/composables/useConvexPaginatedQuery.d.ts +1 -1
- package/dist/runtime/composables/useConvexPaginatedQuery.js +7 -29
- package/dist/runtime/composables/useConvexQueries.d.ts +12 -7
- package/dist/runtime/composables/useConvexQueries.js +1 -3
- package/dist/runtime/composables/useConvexQuery.js +6 -12
- package/dist/runtime/composables/useConvexR2Upload.d.ts +40 -0
- package/dist/runtime/composables/useConvexR2Upload.js +86 -0
- package/dist/runtime/plugin.client.js +3 -7
- package/dist/runtime/plugin.devtools.client.d.ts +15 -2
- package/dist/runtime/plugin.devtools.client.js +18 -4
- package/dist/runtime/plugin.server.js +3 -7
- package/dist/runtime/server/api/convex/auth/session.js +7 -12
- package/dist/runtime/server/auth.js +1 -9
- package/dist/runtime/server/authCookies.js +8 -30
- package/dist/runtime/server/convexConfig.d.ts +12 -0
- package/dist/runtime/server/convexConfig.js +9 -0
- package/dist/runtime/server/fetch.d.ts +2 -4
- package/dist/runtime/server/fetch.js +3 -13
- package/dist/runtime/server/routes/__convex_devtools.get.d.ts +6 -2
- package/dist/runtime/server/routes/__convex_devtools.get.js +184 -16
- package/dist/runtime/server/sameOrigin.js +1 -5
- package/dist/runtime/utils/authCookie.d.ts +0 -2
- package/dist/runtime/utils/authCookie.js +0 -1
- package/dist/runtime/utils/authState.js +1 -6
- package/dist/runtime/utils/authStorage.js +1 -1
- package/dist/runtime/utils/context.js +3 -2
- package/dist/runtime/utils/convexDashboard.d.ts +44 -0
- package/dist/runtime/utils/convexDashboard.js +77 -0
- package/dist/runtime/utils/errors.d.ts +14 -0
- package/dist/runtime/utils/errors.js +25 -0
- package/dist/runtime/utils/oauthRedirect.js +3 -2
- package/dist/runtime/utils/paginatedOptimistic.js +4 -22
- package/dist/runtime/utils/payloadCache.d.ts +6 -0
- package/dist/runtime/utils/payloadCache.js +7 -0
- package/dist/runtime/utils/pendingError.d.ts +9 -0
- package/dist/runtime/utils/pendingError.js +23 -0
- package/dist/types.d.mts +7 -3
- package/package.json +36 -20
package/README.md
CHANGED
|
@@ -6,8 +6,10 @@ SSR snapshots hydrate through the Nuxt payload, then the browser overlays a live
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
+
[`use-convex` on npm](https://www.npmjs.com/package/use-convex)
|
|
10
|
+
|
|
9
11
|
```bash
|
|
10
|
-
|
|
12
|
+
npm i use-convex convex
|
|
11
13
|
```
|
|
12
14
|
|
|
13
15
|
```ts
|
|
@@ -24,7 +26,7 @@ export default defineNuxtConfig({
|
|
|
24
26
|
<script setup lang="ts">
|
|
25
27
|
import { api } from '~~/convex/_generated/api'
|
|
26
28
|
|
|
27
|
-
const { data, pending, error } = await useConvexQuery(api.tasks.list, {})
|
|
29
|
+
const { data, pending, error } = await useConvexQuery(api.tasks.list, {}, { authenticated: true })
|
|
28
30
|
</script>
|
|
29
31
|
```
|
|
30
32
|
|
|
@@ -56,17 +58,18 @@ Set `convex.server: false` to skip SSR snapshots globally, or pass `{ server: fa
|
|
|
56
58
|
const { data, pending, error, refresh } = await useConvexQuery(
|
|
57
59
|
api.tasks.list,
|
|
58
60
|
{}, // args, a ref / getter, or 'skip'
|
|
61
|
+
{ authenticated: true }, // wait for Convex auth before live subscribe
|
|
59
62
|
)
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
| Option
|
|
63
|
-
|
|
64
|
-
| `key`
|
|
65
|
-
| `server`
|
|
66
|
-
| `lazy`
|
|
67
|
-
| `live`
|
|
68
|
-
| `authenticated` | `false`
|
|
69
|
-
| `token`
|
|
65
|
+
| Option | Default | Purpose |
|
|
66
|
+
| --------------- | ------------------------ | ------------------------------------------ |
|
|
67
|
+
| `key` | function name + args | Nuxt payload / cache key |
|
|
68
|
+
| `server` | `convex.server` (`true`) | SSR HttpClient snapshot |
|
|
69
|
+
| `lazy` | `false` | Non-blocking on client navigation |
|
|
70
|
+
| `live` | `true` | Subscribe after hydration |
|
|
71
|
+
| `authenticated` | `false` | Wait for Convex auth before live subscribe |
|
|
72
|
+
| `token` | cookie / none | Per-request JWT for SSR |
|
|
70
73
|
|
|
71
74
|
Args accept the query's `FunctionArgs`, `'skip'`, or a `MaybeRefOrGetter` of either.
|
|
72
75
|
|
|
@@ -82,13 +85,14 @@ prewarmQuery(api.tasks.list, {})
|
|
|
82
85
|
|
|
83
86
|
### Several queries
|
|
84
87
|
|
|
88
|
+
Browser-only live map (no SSR). Each value is `data | undefined` (loading) | `Error`. Combine with `useConvexQuery` when you need an SSR snapshot for known queries.
|
|
89
|
+
|
|
85
90
|
```ts
|
|
86
91
|
const results = useConvexQueries(() => ({
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
? { query: api.messages.get, args: { id: selectedId.value } }
|
|
90
|
-
: 'skip',
|
|
92
|
+
tasks: { query: api.tasks.list, args: {} },
|
|
93
|
+
files: showFiles.value ? { query: api.files.list, args: {} } : 'skip',
|
|
91
94
|
}))
|
|
95
|
+
// results.value.tasks
|
|
92
96
|
```
|
|
93
97
|
|
|
94
98
|
### Pagination
|
|
@@ -97,8 +101,9 @@ const results = useConvexQueries(() => ({
|
|
|
97
101
|
const { results, status, isLoading, loadMore } = await useConvexPaginatedQuery(
|
|
98
102
|
api.tasks.listPaginated,
|
|
99
103
|
{},
|
|
100
|
-
{ initialNumItems: 20 },
|
|
104
|
+
{ initialNumItems: 20, authenticated: true },
|
|
101
105
|
)
|
|
106
|
+
// loadMore() when status === 'CanLoadMore'
|
|
102
107
|
```
|
|
103
108
|
|
|
104
109
|
The first page is SSR'd. On the browser, every loaded page stays live. `loadMore` fetches the next page.
|
|
@@ -115,10 +120,10 @@ const { mutate, pending, error } = useConvexMutation(api.tasks.create, {
|
|
|
115
120
|
])
|
|
116
121
|
},
|
|
117
122
|
})
|
|
118
|
-
await mutate({ text: 'Ship it' })
|
|
123
|
+
await mutate({ text: 'Ship it' }) // browser-only
|
|
119
124
|
|
|
120
|
-
const { run } = useConvexAction(api.
|
|
121
|
-
await run({ text: '
|
|
125
|
+
const { run, pending, error } = useConvexAction(api.tasks.shout)
|
|
126
|
+
const shouted = await run({ text: 'hello' }) // browser-only
|
|
122
127
|
```
|
|
123
128
|
|
|
124
129
|
For paginated lists:
|
|
@@ -133,7 +138,48 @@ insertAtTop({
|
|
|
133
138
|
|
|
134
139
|
Also available: `insertAtBottomIfLoaded`, `insertAtPosition`, `optimisticallyUpdateValueInPaginatedQuery`.
|
|
135
140
|
|
|
136
|
-
`useConvex()` returns the browser `ConvexClient` when you need an escape hatch. `useConvexConnectionState()` is a reactive WebSocket `
|
|
141
|
+
`useConvex()` returns the browser `ConvexClient` when you need an escape hatch. `useConvexConnectionState()` is a reactive `ShallowRef<ConnectionState | null>` (WebSocket status: `isWebSocketConnected`, `hasInflightRequests`, …).
|
|
142
|
+
|
|
143
|
+
## File uploads
|
|
144
|
+
|
|
145
|
+
Convex file storage is a three-step client flow: generate a short-lived upload URL, `POST` the file bytes, then save the returned `storageId` in a mutation. `useConvexFileUpload` wraps that for Nuxt (browser-only, with `pending` / `error` / `progress`).
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const { upload, pending, error, progress } = useConvexFileUpload({
|
|
149
|
+
generateUploadUrl: api.files.generateUploadUrl,
|
|
150
|
+
saveFile: api.files.save, // ({ storageId, name, contentType, size, ... }) => Id<"files">
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
await upload(file) // progress: 0..1 while bytes fly
|
|
154
|
+
// optional extra save args: await upload(file, { caption: '…' })
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Your Convex mutations must enforce auth — never expose an unauthenticated `generateUploadUrl`. Prefer storing `storageId` (and resolving URLs with `ctx.storage.getUrl`) over persisting raw public URLs. See the playground `files` module and the `/files` page for a full list/upload/delete example.
|
|
158
|
+
|
|
159
|
+
### Cloudflare R2 (`@convex-dev/r2`)
|
|
160
|
+
|
|
161
|
+
For larger objects or R2-backed apps, use `useConvexR2Upload` — the Vue counterpart of `@convex-dev/r2/react`'s `useUploadFile`. It does not depend on the R2 package at runtime; pass the `clientApi()` exports from your Convex app:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// convex/r2.ts
|
|
165
|
+
import { R2 } from '@convex-dev/r2'
|
|
166
|
+
import { components } from './_generated/api'
|
|
167
|
+
|
|
168
|
+
const r2 = new R2(components.r2)
|
|
169
|
+
export const { generateUploadUrl, syncMetadata } = r2.clientApi({
|
|
170
|
+
checkUpload: async (ctx) => {
|
|
171
|
+
/* auth */
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// Pass r2.clientApi() exports: { generateUploadUrl, syncMetadata }
|
|
178
|
+
const { upload, pending, error, progress } = useConvexR2Upload(api.r2)
|
|
179
|
+
const key = await upload(file) // R2 object key
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
That runs `generateUploadUrl` → `PUT` to the signed URL → `syncMetadata({ key })`, with XHR progress. Use built-in `useConvexFileUpload` for Convex storage; use this helper when you adopt the R2 component.
|
|
137
183
|
|
|
138
184
|
## Auth
|
|
139
185
|
|
|
@@ -158,6 +204,7 @@ const { signIn, signOut } = useAuth()
|
|
|
158
204
|
|
|
159
205
|
<template>
|
|
160
206
|
<AuthLoading>Resolving…</AuthLoading>
|
|
207
|
+
<AuthRefreshing>Refreshing session…</AuthRefreshing>
|
|
161
208
|
<Authenticated>
|
|
162
209
|
<!-- signed-in shell -->
|
|
163
210
|
</Authenticated>
|
|
@@ -167,7 +214,7 @@ const { signIn, signOut } = useAuth()
|
|
|
167
214
|
</template>
|
|
168
215
|
```
|
|
169
216
|
|
|
170
|
-
Prefer `<Authenticated>` (or `showAuthedUi`) so SSR HTML does not flash the sign-in form. Gate private queries with `{ authenticated: true }`.
|
|
217
|
+
Prefer `<Authenticated>` (or `showAuthedUi`) so SSR HTML does not flash the sign-in form. Gate private queries with `{ authenticated: true }`. `<AuthRefreshing>` shows only while an authenticated session is refreshing a rejected token (same as React).
|
|
171
218
|
|
|
172
219
|
```ts
|
|
173
220
|
await signIn('password', { email, password, flow: 'signIn' })
|
|
@@ -214,7 +261,7 @@ export default defineNuxtRouteMiddleware(() => {
|
|
|
214
261
|
})
|
|
215
262
|
```
|
|
216
263
|
|
|
217
|
-
`useAuthToken()` returns the current JWT for authenticated HTTP calls. `useConvexGate()` exposes `{ showAuthedUi, showLoading, showSignedOut, showRefreshing }` if you prefer flags over layout components.
|
|
264
|
+
`useAuthToken()` returns the current JWT for authenticated HTTP calls. `useConvexGate()` exposes `{ showAuthedUi, showLoading, showSignedOut, showRefreshing }` if you prefer flags over layout components (`Authenticated` / `Unauthenticated` / `AuthLoading` / `AuthRefreshing`).
|
|
218
265
|
|
|
219
266
|
### Bring your own (Clerk, Auth0, custom)
|
|
220
267
|
|
|
@@ -236,7 +283,9 @@ const { data } = await useConvexQuery(api.tasks.list, {}, { authenticated: true
|
|
|
236
283
|
Optional SSR cookie (you must write the JWT after sign-in):
|
|
237
284
|
|
|
238
285
|
```ts
|
|
239
|
-
auth: {
|
|
286
|
+
auth: {
|
|
287
|
+
cookie: 'convex_jwt'
|
|
288
|
+
}
|
|
240
289
|
```
|
|
241
290
|
|
|
242
291
|
## Server routes
|
|
@@ -260,6 +309,15 @@ export default defineEventHandler(async (event) => {
|
|
|
260
309
|
})
|
|
261
310
|
```
|
|
262
311
|
|
|
312
|
+
```ts
|
|
313
|
+
// server/api/shout.post.ts
|
|
314
|
+
export default defineEventHandler(async (event) => {
|
|
315
|
+
requireConvexAuth(event)
|
|
316
|
+
const { text } = await readBody(event)
|
|
317
|
+
return await fetchAction(api.tasks.shout, { text }, { event })
|
|
318
|
+
})
|
|
319
|
+
```
|
|
320
|
+
|
|
263
321
|
`fetchAction` uses the same options. Each helper builds a fresh `ConvexHttpClient`. Override with `{ token }` when you already have a JWT. `getConvexToken(event)` reads the cookie without throwing.
|
|
264
322
|
|
|
265
323
|
## Config
|
|
@@ -280,6 +338,19 @@ convex: {
|
|
|
280
338
|
|
|
281
339
|
`url` also reads `NUXT_PUBLIC_CONVEX_URL` via `runtimeConfig.public.convex.url`, so you can change the deployment without rebuilding.
|
|
282
340
|
|
|
341
|
+
## Nuxt DevTools
|
|
342
|
+
|
|
343
|
+
In development, a **Convex** tab appears in [Nuxt DevTools](https://devtools.nuxt.com). It embeds the official [hosted dashboard](https://docs.convex.dev/platform-apis/embedded-dashboard) for `*.convex.cloud` URLs and shows module config / tips.
|
|
344
|
+
|
|
345
|
+
- **Open dashboard** deep-links to `dashboard.convex.dev` (uses your existing Convex login).
|
|
346
|
+
- **Auto-login** in the embed needs `CONVEX_DEPLOY_KEY` in the Nuxt process environment (e.g. `.env.local`). Without it, the iframe shows Convex’s credential form.
|
|
347
|
+
- A deploy key inlined into the DevTools page is visible to anyone who can reach your local `nuxt dev` server — only set it for trusted local machines.
|
|
348
|
+
- Local / self-hosted backends are not embedded; use the Open dashboard link or the CLI dashboard instead.
|
|
349
|
+
|
|
350
|
+
## Contributing
|
|
351
|
+
|
|
352
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md). Lint/format/test use **Vite+** (`vp`) — Oxlint, Oxfmt, and Vitest — while Nuxt module build/dev stay on `nuxt-module-build` / `nuxi`.
|
|
353
|
+
|
|
283
354
|
## Playground
|
|
284
355
|
|
|
285
356
|
`playground/` is a normal Nuxt app that consumes this module (workspace-linked).
|
|
@@ -287,7 +358,7 @@ convex: {
|
|
|
287
358
|
```bash
|
|
288
359
|
pnpm install
|
|
289
360
|
|
|
290
|
-
# Terminal 1 — Convex backend (writes
|
|
361
|
+
# Terminal 1 — Convex backend (writes CONVEX_URL to playground/.env.local)
|
|
291
362
|
pnpm run dev:backend
|
|
292
363
|
# first time: npx @convex-dev/auth # JWT_PRIVATE_KEY + JWKS
|
|
293
364
|
|
|
@@ -295,27 +366,36 @@ pnpm run dev:backend
|
|
|
295
366
|
pnpm run dev
|
|
296
367
|
```
|
|
297
368
|
|
|
298
|
-
| Route
|
|
299
|
-
|
|
300
|
-
| `/`
|
|
301
|
-
| `/
|
|
302
|
-
| `/
|
|
369
|
+
| Route | What it shows |
|
|
370
|
+
| --------- | ----------------------------------------------------- |
|
|
371
|
+
| `/` | Shell session: features + composable call-shape demos |
|
|
372
|
+
| `/live` | SSR snapshot + live overlay (sign up, CRUD todos) |
|
|
373
|
+
| `/server` | Nitro `fetchQuery` / `fetchMutation` / `fetchAction` |
|
|
374
|
+
| `/files` | `useConvexFileUpload` (upload, list, preview, delete) |
|
|
375
|
+
| `/extras` | `live: false`, pagination, action, connection state |
|
|
303
376
|
|
|
304
377
|
On `/server`: `GET /api/health` is public; `GET`/`POST /api/tasks` use the cookie JWT; `POST /api/shout` is a public `fetchAction` demo.
|
|
305
378
|
|
|
306
379
|
## Releasing
|
|
307
380
|
|
|
308
|
-
Releases run from `.github/workflows/release.yml` when commits land on `main
|
|
381
|
+
Releases run from `.github/workflows/release.yml` when commits that touch `src/` land on `main` (or when the workflow is run manually). Version bumps follow [conventional commits](https://www.conventionalcommits.org/):
|
|
309
382
|
|
|
310
|
-
| Commit
|
|
311
|
-
|
|
312
|
-
| `
|
|
313
|
-
| `feat:`
|
|
314
|
-
|
|
|
315
|
-
| `chore:`, `docs:`, `ci:`, … | none |
|
|
383
|
+
| Commit | Release |
|
|
384
|
+
| ------------------------------ | ------- |
|
|
385
|
+
| `feat!:` or `BREAKING CHANGE:` | major |
|
|
386
|
+
| `feat:` | minor |
|
|
387
|
+
| anything else under `src/` | patch |
|
|
316
388
|
|
|
317
389
|
semantic-release publishes `use-convex` to npm, tags `vX.Y.Z`, and opens a GitHub Release. The repo `package.json` version is not committed back.
|
|
318
390
|
|
|
391
|
+
Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC), not an `NPM_TOKEN`. On [the `use-convex` package settings](https://www.npmjs.com/package/use-convex?activeTab=settings) add a GitHub Actions trusted publisher:
|
|
392
|
+
|
|
393
|
+
- **Organization or user:** `jrmybtlr`
|
|
394
|
+
- **Repository:** `convex-nuxt`
|
|
395
|
+
- **Workflow filename:** `release.yml`
|
|
396
|
+
- **Environment:** leave empty
|
|
397
|
+
- **Allowed actions:** include **`npm publish`** (new publishers default to staged publish only)
|
|
398
|
+
|
|
319
399
|
## License
|
|
320
400
|
|
|
321
401
|
MIT
|
package/dist/module.d.mts
CHANGED
|
@@ -3,9 +3,11 @@ import * as convex_browser from 'convex/browser';
|
|
|
3
3
|
export { AuthTokens, SignInResult, UseAuthReturn } from '../dist/runtime/composables/useAuth.js';
|
|
4
4
|
export { ConvexQueryArgs, UseConvexQueryOptions, UseConvexQueryReturn } from '../dist/runtime/composables/useConvexQuery.js';
|
|
5
5
|
export { AuthTokenFetcher, UseConvexAuthReturn, UseConvexAuthSetupOptions } from '../dist/runtime/composables/useConvexAuth.js';
|
|
6
|
-
export { UseConvexMutationOptions } from '../dist/runtime/composables/useConvexMutation.js';
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
6
|
+
export { OptimisticUpdate, UseConvexMutationOptions } from '../dist/runtime/composables/useConvexMutation.js';
|
|
7
|
+
export { ConvexFileUploadExtraArgs, ConvexFileUploadMeta, UseConvexFileUploadOptions } from '../dist/runtime/composables/useConvexFileUpload.js';
|
|
8
|
+
export { ConvexR2UploadApi, ConvexR2UploadProgress } from '../dist/runtime/composables/useConvexR2Upload.js';
|
|
9
|
+
export { PaginatedQueryArgs, PaginatedQueryItem, PaginatedQueryReference, PaginationStatus, UseConvexPaginatedQueryOptions, UseConvexPaginatedQueryReturn } from '../dist/runtime/composables/useConvexPaginatedQuery.js';
|
|
10
|
+
export { ConvexQueriesRequest, ConvexQueriesResult, ConvexQueryRequestEntry } from '../dist/runtime/composables/useConvexQueries.js';
|
|
9
11
|
export { ConvexFetchOptions } from '../dist/runtime/server/fetch.js';
|
|
10
12
|
export { ConvexAuthContext, ConvexNuxtContext } from '../dist/runtime/utils/context.js';
|
|
11
13
|
export { ConnectionState } from '../dist/runtime/composables/useConvexConnectionState.js';
|
package/dist/module.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineNuxtModule, createResolver, addPlugin, addComponent, addServerScanDir, addServerHandler, addImports, addServerImports, addTypeTemplate } from '@nuxt/kit';
|
|
2
2
|
import { defu } from 'defu';
|
|
3
3
|
import { resolveAuthCookieName } from '../dist/runtime/utils/authStorage.js';
|
|
4
|
+
import { suggestedCloudUrlFromMismatch } from '../dist/runtime/utils/convexDashboard.js';
|
|
5
|
+
import { warnStaleLocalConvexUrl } from '../dist/runtime/utils/errors.js';
|
|
4
6
|
|
|
5
7
|
const module$1 = defineNuxtModule({
|
|
6
8
|
meta: {
|
|
@@ -26,6 +28,25 @@ const module$1 = defineNuxtModule({
|
|
|
26
28
|
client: options.client,
|
|
27
29
|
auth
|
|
28
30
|
});
|
|
31
|
+
if (nuxt.options.dev) {
|
|
32
|
+
const publicUrl = nuxt.options.runtimeConfig.public.convex?.url ?? "";
|
|
33
|
+
const suggestedCloudUrl = suggestedCloudUrlFromMismatch(
|
|
34
|
+
publicUrl,
|
|
35
|
+
process.env.CONVEX_DEPLOYMENT
|
|
36
|
+
);
|
|
37
|
+
if (suggestedCloudUrl) {
|
|
38
|
+
warnStaleLocalConvexUrl(publicUrl, suggestedCloudUrl);
|
|
39
|
+
}
|
|
40
|
+
nuxt.options.runtimeConfig.convexDevtools = defu(
|
|
41
|
+
nuxt.options.runtimeConfig.convexDevtools ?? {},
|
|
42
|
+
{
|
|
43
|
+
deployKey: process.env.CONVEX_DEPLOY_KEY ?? "",
|
|
44
|
+
deployment: process.env.CONVEX_DEPLOYMENT ?? "",
|
|
45
|
+
// Fallback when public.convex.url was empty at config eval time.
|
|
46
|
+
url: process.env.NUXT_PUBLIC_CONVEX_URL ?? process.env.CONVEX_URL ?? ""
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
29
50
|
const resolver = createResolver(import.meta.url);
|
|
30
51
|
const runtimeDir = resolver.resolve("./runtime");
|
|
31
52
|
const useConvexAuthProvider = auth?.provider === "convex-auth";
|
|
@@ -57,7 +78,12 @@ const module$1 = defineNuxtModule({
|
|
|
57
78
|
mode: "client"
|
|
58
79
|
});
|
|
59
80
|
}
|
|
60
|
-
for (const name of [
|
|
81
|
+
for (const name of [
|
|
82
|
+
"Authenticated",
|
|
83
|
+
"Unauthenticated",
|
|
84
|
+
"AuthLoading",
|
|
85
|
+
"AuthRefreshing"
|
|
86
|
+
]) {
|
|
61
87
|
addComponent({
|
|
62
88
|
name,
|
|
63
89
|
filePath: resolver.resolve(`./runtime/components/${name}.vue`)
|
|
@@ -103,6 +129,14 @@ const module$1 = defineNuxtModule({
|
|
|
103
129
|
name: "useConvexMutation",
|
|
104
130
|
from: resolver.resolve("./runtime/composables/useConvexMutation")
|
|
105
131
|
},
|
|
132
|
+
{
|
|
133
|
+
name: "useConvexFileUpload",
|
|
134
|
+
from: resolver.resolve("./runtime/composables/useConvexFileUpload")
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "useConvexR2Upload",
|
|
138
|
+
from: resolver.resolve("./runtime/composables/useConvexR2Upload")
|
|
139
|
+
},
|
|
106
140
|
{
|
|
107
141
|
name: "useConvexAction",
|
|
108
142
|
from: resolver.resolve("./runtime/composables/useConvexAction")
|
|
@@ -43,17 +43,17 @@ export declare function signIn(provider?: string, params?: FormData | Record<str
|
|
|
43
43
|
* Sign out and clear local tokens + SSR JWT cookie.
|
|
44
44
|
*/
|
|
45
45
|
export declare function signOut(): Promise<void>;
|
|
46
|
-
/** @internal
|
|
46
|
+
/** @internal */
|
|
47
47
|
export declare function hydrateAuthFromStorage(): void;
|
|
48
|
-
/** @internal
|
|
48
|
+
/** @internal */
|
|
49
49
|
export declare function getAuthToken({ forceRefreshToken, }: {
|
|
50
50
|
forceRefreshToken: boolean;
|
|
51
51
|
}): Promise<string | null>;
|
|
52
|
-
/** @internal
|
|
52
|
+
/** @internal */
|
|
53
53
|
export declare function hasPendingOAuthCallback(): boolean;
|
|
54
|
-
/** @internal
|
|
54
|
+
/** @internal */
|
|
55
55
|
export declare function consumeOAuthCodeFromUrl(): Promise<boolean>;
|
|
56
|
-
/** @internal
|
|
56
|
+
/** @internal */
|
|
57
57
|
export declare function useAuthProviderState(): {
|
|
58
58
|
isLoading: ComputedRef<boolean>;
|
|
59
59
|
hasSession: ComputedRef<boolean>;
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { ConvexHttpClient } from "convex/browser";
|
|
2
2
|
import { makeFunctionReference } from "convex/server";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
nextTick
|
|
6
|
-
} from "vue";
|
|
7
|
-
import {
|
|
8
|
-
useRuntimeConfig,
|
|
9
|
-
useState
|
|
10
|
-
} from "nuxt/app";
|
|
3
|
+
import { computed, nextTick } from "vue";
|
|
4
|
+
import { useRuntimeConfig, useState } from "nuxt/app";
|
|
11
5
|
import { useConvexAuth } from "./useConvexAuth.js";
|
|
6
|
+
import { missingConvexUrlError, unreachableConvexUrlError } from "../utils/errors.js";
|
|
12
7
|
import { withRefreshMutex } from "../utils/authMutex.js";
|
|
13
|
-
import {
|
|
14
|
-
useAuthJwtCookie,
|
|
15
|
-
useAuthPresentCookie
|
|
16
|
-
} from "../utils/authCookie.js";
|
|
8
|
+
import { useAuthJwtCookie, useAuthPresentCookie } from "../utils/authCookie.js";
|
|
17
9
|
import {
|
|
18
10
|
flattenSignInParams,
|
|
19
11
|
isHttpOnlyAuth,
|
|
@@ -28,9 +20,7 @@ import {
|
|
|
28
20
|
import { tryUseConvexContext } from "../utils/context.js";
|
|
29
21
|
import { parseOAuthRedirect } from "../utils/oauthRedirect.js";
|
|
30
22
|
const authSignIn = makeFunctionReference("auth:signIn");
|
|
31
|
-
const authSignOut = makeFunctionReference(
|
|
32
|
-
"auth:signOut"
|
|
33
|
-
);
|
|
23
|
+
const authSignOut = makeFunctionReference("auth:signOut");
|
|
34
24
|
const RETRY_BACKOFF = [500, 2e3];
|
|
35
25
|
const RETRY_JITTER = 100;
|
|
36
26
|
export function useAuth() {
|
|
@@ -65,7 +55,7 @@ export async function signIn(provider, params) {
|
|
|
65
55
|
const session = useAuthSession();
|
|
66
56
|
const convexUrl = session.convexUrl;
|
|
67
57
|
if (!convexUrl) {
|
|
68
|
-
throw
|
|
58
|
+
throw missingConvexUrlError("signIn");
|
|
69
59
|
}
|
|
70
60
|
session.pending.value = true;
|
|
71
61
|
session.error.value = null;
|
|
@@ -94,7 +84,7 @@ export async function signIn(provider, params) {
|
|
|
94
84
|
}
|
|
95
85
|
return { signingIn: false };
|
|
96
86
|
} catch (cause) {
|
|
97
|
-
const message = cause instanceof Error ? cause.message : "Authentication failed";
|
|
87
|
+
const message = isNetworkError(cause) && convexUrl ? unreachableConvexUrlError(convexUrl).message : cause instanceof Error ? cause.message : "Authentication failed";
|
|
98
88
|
session.error.value = message;
|
|
99
89
|
throw cause;
|
|
100
90
|
} finally {
|
|
@@ -319,13 +309,10 @@ async function fetchAuthSession() {
|
|
|
319
309
|
}
|
|
320
310
|
async function fetchAuthSessionToken() {
|
|
321
311
|
try {
|
|
322
|
-
return await $fetch(
|
|
323
|
-
|
|
324
|
-
{
|
|
325
|
-
|
|
326
|
-
body: { getToken: true }
|
|
327
|
-
}
|
|
328
|
-
);
|
|
312
|
+
return await $fetch(AUTH_SESSION_PATH, {
|
|
313
|
+
method: "POST",
|
|
314
|
+
body: { getToken: true }
|
|
315
|
+
});
|
|
329
316
|
} catch {
|
|
330
317
|
return { hasSession: false, token: null };
|
|
331
318
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { navigateTo } from "nuxt/app";
|
|
2
|
-
import {
|
|
3
|
-
computed,
|
|
4
|
-
ref,
|
|
5
|
-
watch
|
|
6
|
-
} from "vue";
|
|
2
|
+
import { computed, ref, watch } from "vue";
|
|
7
3
|
import { useConvexAuth } from "./useConvexAuth.js";
|
|
8
4
|
import { tryUseConvexContext } from "../utils/context.js";
|
|
9
5
|
export function useAuthToken() {
|
|
@@ -42,11 +38,7 @@ export function useAuthToken() {
|
|
|
42
38
|
}
|
|
43
39
|
};
|
|
44
40
|
watch(
|
|
45
|
-
[
|
|
46
|
-
() => auth.isAuthenticated.value,
|
|
47
|
-
() => auth.isRefreshing.value,
|
|
48
|
-
() => auth.isLoading.value
|
|
49
|
-
],
|
|
41
|
+
[() => auth.isAuthenticated.value, () => auth.isRefreshing.value, () => auth.isLoading.value],
|
|
50
42
|
() => {
|
|
51
43
|
void refresh();
|
|
52
44
|
},
|
|
@@ -3,7 +3,7 @@ export function useConvex() {
|
|
|
3
3
|
const ctx = useConvexContext();
|
|
4
4
|
if (!ctx.client) {
|
|
5
5
|
throw new Error(
|
|
6
|
-
"[convex
|
|
6
|
+
"[use-convex] useConvex() is only available in the browser. Use fetchQuery on the server."
|
|
7
7
|
);
|
|
8
8
|
}
|
|
9
9
|
return ctx.client;
|
|
@@ -1,30 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toValue } from "vue";
|
|
2
2
|
import { useConvexContext } from "../utils/context.js";
|
|
3
|
+
import { createPendingErrorState } from "../utils/pendingError.js";
|
|
3
4
|
export function useConvexAction(action) {
|
|
4
5
|
const ctx = useConvexContext();
|
|
5
|
-
const error =
|
|
6
|
-
const pendingCount = ref(0);
|
|
6
|
+
const { error, pending, withPending } = createPendingErrorState();
|
|
7
7
|
const run = async (args = {}) => {
|
|
8
8
|
if (import.meta.server || !ctx.client) {
|
|
9
|
-
throw new Error(
|
|
10
|
-
"[convex-nuxt] useConvexAction can only run in the browser."
|
|
11
|
-
);
|
|
12
|
-
}
|
|
13
|
-
pendingCount.value++;
|
|
14
|
-
error.value = null;
|
|
15
|
-
try {
|
|
16
|
-
return await ctx.client.action(action, toValue(args));
|
|
17
|
-
} catch (cause) {
|
|
18
|
-
const err = cause instanceof Error ? cause : new Error(String(cause));
|
|
19
|
-
error.value = err;
|
|
20
|
-
throw err;
|
|
21
|
-
} finally {
|
|
22
|
-
pendingCount.value--;
|
|
9
|
+
throw new Error("[use-convex] useConvexAction can only run in the browser.");
|
|
23
10
|
}
|
|
11
|
+
return await withPending(() => ctx.client.action(action, toValue(args)));
|
|
24
12
|
};
|
|
25
13
|
return {
|
|
26
14
|
run,
|
|
27
15
|
error,
|
|
28
|
-
pending
|
|
16
|
+
pending
|
|
29
17
|
};
|
|
30
18
|
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
computed,
|
|
3
|
-
toValue,
|
|
4
|
-
watch
|
|
5
|
-
} from "vue";
|
|
1
|
+
import { computed, toValue, watch } from "vue";
|
|
6
2
|
import { useHasSsrSessionRef } from "../utils/authCookie.js";
|
|
7
3
|
import { useConvexContext } from "../utils/context.js";
|
|
8
4
|
import { resolveConvexAuthState } from "../utils/authState.js";
|
|
@@ -10,9 +6,7 @@ export function useConvexAuth(setup) {
|
|
|
10
6
|
const ctx = useConvexContext();
|
|
11
7
|
const auth = ctx.auth;
|
|
12
8
|
const hasSsrSession = useHasSsrSessionRef();
|
|
13
|
-
const showAuthedUi = computed(
|
|
14
|
-
() => auth.isAuthenticated.value || hasSsrSession.value
|
|
15
|
-
);
|
|
9
|
+
const showAuthedUi = computed(() => auth.isAuthenticated.value || hasSsrSession.value);
|
|
16
10
|
if (setup) {
|
|
17
11
|
if (import.meta.server) {
|
|
18
12
|
return {
|
|
@@ -26,7 +20,7 @@ export function useConvexAuth(setup) {
|
|
|
26
20
|
const client = ctx.client;
|
|
27
21
|
if (!client) {
|
|
28
22
|
throw new Error(
|
|
29
|
-
"[convex
|
|
23
|
+
"[use-convex] useConvexAuth({ fetchToken }) requires ConvexClient (browser only)."
|
|
30
24
|
);
|
|
31
25
|
}
|
|
32
26
|
auth.fetchToken = setup.fetchToken;
|
|
@@ -67,10 +61,7 @@ export function useConvexAuth(setup) {
|
|
|
67
61
|
);
|
|
68
62
|
};
|
|
69
63
|
watch(
|
|
70
|
-
() => [
|
|
71
|
-
toValue(setup.isLoading) ?? false,
|
|
72
|
-
toValue(setup.isAuthenticated) ?? true
|
|
73
|
-
],
|
|
64
|
+
() => [toValue(setup.isLoading) ?? false, toValue(setup.isAuthenticated) ?? true],
|
|
74
65
|
syncFromProvider,
|
|
75
66
|
{ immediate: true }
|
|
76
67
|
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { FunctionArgs, FunctionReference, FunctionReturnType } from 'convex/server';
|
|
2
|
+
/** Built-in fields the composable always passes into `saveFile`. */
|
|
3
|
+
export type ConvexFileUploadMeta = {
|
|
4
|
+
storageId: string;
|
|
5
|
+
name: string;
|
|
6
|
+
contentType: string;
|
|
7
|
+
size: number;
|
|
8
|
+
};
|
|
9
|
+
export type ConvexFileUploadExtraArgs<SaveFile extends FunctionReference<'mutation'>> = Omit<FunctionArgs<SaveFile>, 'storageId' | 'name' | 'contentType' | 'size'>;
|
|
10
|
+
export interface UseConvexFileUploadOptions<GenerateUploadUrl extends FunctionReference<'mutation'>, SaveFile extends FunctionReference<'mutation'>> {
|
|
11
|
+
/**
|
|
12
|
+
* Mutation that returns a short-lived Convex upload URL
|
|
13
|
+
* (`ctx.storage.generateUploadUrl()`). Must enforce auth itself.
|
|
14
|
+
*/
|
|
15
|
+
generateUploadUrl: GenerateUploadUrl;
|
|
16
|
+
/**
|
|
17
|
+
* Mutation that persists `storageId` + file metadata after the POST succeeds.
|
|
18
|
+
* Must accept at least `storageId`, `name`, `contentType`, and `size`.
|
|
19
|
+
*/
|
|
20
|
+
saveFile: SaveFile;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* POST a file to a Convex-generated upload URL with upload progress.
|
|
24
|
+
* Convex expects `Content-Type` matching the file and returns `{ storageId }`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function postFileToUploadUrl(uploadUrl: string, file: File, onProgress?: (fraction: number) => void): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Browser helper for Convex's three-step file upload flow:
|
|
29
|
+
* generate upload URL → POST file → save `storageId` via mutation.
|
|
30
|
+
*
|
|
31
|
+
* Returns `{ upload, pending, error, progress }`. `progress` is `0..1` while
|
|
32
|
+
* the bytes are in flight, otherwise `null`.
|
|
33
|
+
*
|
|
34
|
+
* Safe to call during SSR setup — work only runs when `upload()` is invoked
|
|
35
|
+
* in the browser. Your Convex mutations must enforce auth; never expose an
|
|
36
|
+
* unauthenticated `generateUploadUrl`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function useConvexFileUpload<GenerateUploadUrl extends FunctionReference<'mutation'>, SaveFile extends FunctionReference<'mutation'>>(options: UseConvexFileUploadOptions<GenerateUploadUrl, SaveFile>): {
|
|
39
|
+
upload: (file: File, extra?: ConvexFileUploadExtraArgs<SaveFile> extends Record<string, never> ? undefined : ConvexFileUploadExtraArgs<SaveFile>) => Promise<FunctionReturnType<SaveFile>>;
|
|
40
|
+
error: import("vue").Ref<Error | null, Error | null>;
|
|
41
|
+
pending: import("vue").ComputedRef<boolean>;
|
|
42
|
+
progress: import("vue").ComputedRef<number | null>;
|
|
43
|
+
};
|