use-convex 0.0.2 → 1.0.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 +84 -27
- 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
|
|
@@ -59,14 +61,14 @@ const { data, pending, error, refresh } = await useConvexQuery(
|
|
|
59
61
|
)
|
|
60
62
|
```
|
|
61
63
|
|
|
62
|
-
| Option
|
|
63
|
-
|
|
64
|
-
| `key`
|
|
65
|
-
| `server`
|
|
66
|
-
| `lazy`
|
|
67
|
-
| `live`
|
|
68
|
-
| `authenticated` | `false`
|
|
69
|
-
| `token`
|
|
64
|
+
| Option | Default | Purpose |
|
|
65
|
+
| --------------- | ------------------------ | ------------------------------------------ |
|
|
66
|
+
| `key` | function name + args | Nuxt payload / cache key |
|
|
67
|
+
| `server` | `convex.server` (`true`) | SSR HttpClient snapshot |
|
|
68
|
+
| `lazy` | `false` | Non-blocking on client navigation |
|
|
69
|
+
| `live` | `true` | Subscribe after hydration |
|
|
70
|
+
| `authenticated` | `false` | Wait for Convex auth before live subscribe |
|
|
71
|
+
| `token` | cookie / none | Per-request JWT for SSR |
|
|
70
72
|
|
|
71
73
|
Args accept the query's `FunctionArgs`, `'skip'`, or a `MaybeRefOrGetter` of either.
|
|
72
74
|
|
|
@@ -85,9 +87,7 @@ prewarmQuery(api.tasks.list, {})
|
|
|
85
87
|
```ts
|
|
86
88
|
const results = useConvexQueries(() => ({
|
|
87
89
|
inbox: { query: api.messages.list, args: { channel: 'inbox' } },
|
|
88
|
-
later: selectedId.value
|
|
89
|
-
? { query: api.messages.get, args: { id: selectedId.value } }
|
|
90
|
-
: 'skip',
|
|
90
|
+
later: selectedId.value ? { query: api.messages.get, args: { id: selectedId.value } } : 'skip',
|
|
91
91
|
}))
|
|
92
92
|
```
|
|
93
93
|
|
|
@@ -135,6 +135,46 @@ Also available: `insertAtBottomIfLoaded`, `insertAtPosition`, `optimisticallyUpd
|
|
|
135
135
|
|
|
136
136
|
`useConvex()` returns the browser `ConvexClient` when you need an escape hatch. `useConvexConnectionState()` is a reactive WebSocket `ConnectionState`.
|
|
137
137
|
|
|
138
|
+
## File uploads
|
|
139
|
+
|
|
140
|
+
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`).
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const { upload, pending, error, progress } = useConvexFileUpload({
|
|
144
|
+
generateUploadUrl: api.files.generateUploadUrl,
|
|
145
|
+
saveFile: api.files.save, // ({ storageId, name, contentType, size, ... }) => Id<"files">
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
await upload(file)
|
|
149
|
+
// optional extra save args: await upload(file, { caption: '…' })
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
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.
|
|
153
|
+
|
|
154
|
+
### Cloudflare R2 (`@convex-dev/r2`)
|
|
155
|
+
|
|
156
|
+
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:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// convex/r2.ts
|
|
160
|
+
import { R2 } from '@convex-dev/r2'
|
|
161
|
+
import { components } from './_generated/api'
|
|
162
|
+
|
|
163
|
+
const r2 = new R2(components.r2)
|
|
164
|
+
export const { generateUploadUrl, syncMetadata } = r2.clientApi({
|
|
165
|
+
checkUpload: async (ctx) => {
|
|
166
|
+
/* auth */
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const { upload, pending, error, progress } = useConvexR2Upload(api.r2)
|
|
173
|
+
const key = await upload(file)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
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.
|
|
177
|
+
|
|
138
178
|
## Auth
|
|
139
179
|
|
|
140
180
|
### Convex Auth
|
|
@@ -158,6 +198,7 @@ const { signIn, signOut } = useAuth()
|
|
|
158
198
|
|
|
159
199
|
<template>
|
|
160
200
|
<AuthLoading>Resolving…</AuthLoading>
|
|
201
|
+
<AuthRefreshing>Refreshing session…</AuthRefreshing>
|
|
161
202
|
<Authenticated>
|
|
162
203
|
<!-- signed-in shell -->
|
|
163
204
|
</Authenticated>
|
|
@@ -167,7 +208,7 @@ const { signIn, signOut } = useAuth()
|
|
|
167
208
|
</template>
|
|
168
209
|
```
|
|
169
210
|
|
|
170
|
-
Prefer `<Authenticated>` (or `showAuthedUi`) so SSR HTML does not flash the sign-in form. Gate private queries with `{ authenticated: true }`.
|
|
211
|
+
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
212
|
|
|
172
213
|
```ts
|
|
173
214
|
await signIn('password', { email, password, flow: 'signIn' })
|
|
@@ -214,7 +255,7 @@ export default defineNuxtRouteMiddleware(() => {
|
|
|
214
255
|
})
|
|
215
256
|
```
|
|
216
257
|
|
|
217
|
-
`useAuthToken()` returns the current JWT for authenticated HTTP calls. `useConvexGate()` exposes `{ showAuthedUi, showLoading, showSignedOut, showRefreshing }` if you prefer flags over layout components.
|
|
258
|
+
`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
259
|
|
|
219
260
|
### Bring your own (Clerk, Auth0, custom)
|
|
220
261
|
|
|
@@ -236,7 +277,9 @@ const { data } = await useConvexQuery(api.tasks.list, {}, { authenticated: true
|
|
|
236
277
|
Optional SSR cookie (you must write the JWT after sign-in):
|
|
237
278
|
|
|
238
279
|
```ts
|
|
239
|
-
auth: {
|
|
280
|
+
auth: {
|
|
281
|
+
cookie: 'convex_jwt'
|
|
282
|
+
}
|
|
240
283
|
```
|
|
241
284
|
|
|
242
285
|
## Server routes
|
|
@@ -280,6 +323,19 @@ convex: {
|
|
|
280
323
|
|
|
281
324
|
`url` also reads `NUXT_PUBLIC_CONVEX_URL` via `runtimeConfig.public.convex.url`, so you can change the deployment without rebuilding.
|
|
282
325
|
|
|
326
|
+
## Nuxt DevTools
|
|
327
|
+
|
|
328
|
+
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.
|
|
329
|
+
|
|
330
|
+
- **Open dashboard** deep-links to `dashboard.convex.dev` (uses your existing Convex login).
|
|
331
|
+
- **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.
|
|
332
|
+
- 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.
|
|
333
|
+
- Local / self-hosted backends are not embedded; use the Open dashboard link or the CLI dashboard instead.
|
|
334
|
+
|
|
335
|
+
## Contributing
|
|
336
|
+
|
|
337
|
+
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`.
|
|
338
|
+
|
|
283
339
|
## Playground
|
|
284
340
|
|
|
285
341
|
`playground/` is a normal Nuxt app that consumes this module (workspace-linked).
|
|
@@ -287,7 +343,7 @@ convex: {
|
|
|
287
343
|
```bash
|
|
288
344
|
pnpm install
|
|
289
345
|
|
|
290
|
-
# Terminal 1 — Convex backend (writes
|
|
346
|
+
# Terminal 1 — Convex backend (writes CONVEX_URL to playground/.env.local)
|
|
291
347
|
pnpm run dev:backend
|
|
292
348
|
# first time: npx @convex-dev/auth # JWT_PRIVATE_KEY + JWKS
|
|
293
349
|
|
|
@@ -295,11 +351,12 @@ pnpm run dev:backend
|
|
|
295
351
|
pnpm run dev
|
|
296
352
|
```
|
|
297
353
|
|
|
298
|
-
| Route
|
|
299
|
-
|
|
300
|
-
| `/`
|
|
301
|
-
| `/server` | Nitro `fetchQuery` / `fetchMutation` / `fetchAction`
|
|
302
|
-
| `/
|
|
354
|
+
| Route | What it shows |
|
|
355
|
+
| --------- | ----------------------------------------------------- |
|
|
356
|
+
| `/` | SSR snapshot + live overlay (sign up, CRUD todos) |
|
|
357
|
+
| `/server` | Nitro `fetchQuery` / `fetchMutation` / `fetchAction` |
|
|
358
|
+
| `/files` | `useConvexFileUpload` (upload, list, preview, delete) |
|
|
359
|
+
| `/extras` | `live: false`, pagination, action, connection state |
|
|
303
360
|
|
|
304
361
|
On `/server`: `GET /api/health` is public; `GET`/`POST /api/tasks` use the cookie JWT; `POST /api/shout` is a public `fetchAction` demo.
|
|
305
362
|
|
|
@@ -307,12 +364,12 @@ On `/server`: `GET /api/health` is public; `GET`/`POST /api/tasks` use the cooki
|
|
|
307
364
|
|
|
308
365
|
Releases run from `.github/workflows/release.yml` when commits land on `main`. Version bumps follow [conventional commits](https://www.conventionalcommits.org/):
|
|
309
366
|
|
|
310
|
-
| Commit
|
|
311
|
-
|
|
312
|
-
| `fix:`
|
|
313
|
-
| `feat:`
|
|
314
|
-
| `feat!:` or `BREAKING CHANGE:` | major
|
|
315
|
-
| `chore:`, `docs:`, `ci:`, …
|
|
367
|
+
| Commit | Release |
|
|
368
|
+
| ------------------------------ | ------- |
|
|
369
|
+
| `fix:` | patch |
|
|
370
|
+
| `feat:` | minor |
|
|
371
|
+
| `feat!:` or `BREAKING CHANGE:` | major |
|
|
372
|
+
| `chore:`, `docs:`, `ci:`, … | none |
|
|
316
373
|
|
|
317
374
|
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
375
|
|
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
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import { useConvexContext } from "../utils/context.js";
|
|
3
|
+
function isUploadUrl(value) {
|
|
4
|
+
return typeof value === "string" && value.length > 0;
|
|
5
|
+
}
|
|
6
|
+
function parseStorageId(body) {
|
|
7
|
+
if (typeof body === "object" && body !== null && "storageId" in body && typeof body.storageId === "string" && body.storageId.length > 0) {
|
|
8
|
+
return body.storageId;
|
|
9
|
+
}
|
|
10
|
+
throw new Error("[use-convex] Upload response missing storageId.");
|
|
11
|
+
}
|
|
12
|
+
export function postFileToUploadUrl(uploadUrl, file, onProgress) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const xhr = new XMLHttpRequest();
|
|
15
|
+
xhr.open("POST", uploadUrl);
|
|
16
|
+
xhr.responseType = "json";
|
|
17
|
+
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
|
|
18
|
+
xhr.upload.onprogress = (event) => {
|
|
19
|
+
if (!event.lengthComputable || !onProgress) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (event.total > 0) {
|
|
23
|
+
onProgress(event.loaded / event.total);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
xhr.onload = () => {
|
|
27
|
+
if (xhr.status < 200 || xhr.status >= 300) {
|
|
28
|
+
reject(
|
|
29
|
+
new Error(
|
|
30
|
+
`[use-convex] File upload failed (${xhr.status} ${xhr.statusText || "error"}).`
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const body = typeof xhr.response === "string" ? JSON.parse(xhr.response) : xhr.response;
|
|
37
|
+
resolve(parseStorageId(body));
|
|
38
|
+
} catch (cause) {
|
|
39
|
+
const err = cause instanceof Error ? cause : new Error(String(cause));
|
|
40
|
+
reject(err);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
xhr.onerror = () => {
|
|
44
|
+
reject(new Error("[use-convex] File upload network error."));
|
|
45
|
+
};
|
|
46
|
+
xhr.onabort = () => {
|
|
47
|
+
reject(new Error("[use-convex] File upload aborted."));
|
|
48
|
+
};
|
|
49
|
+
xhr.send(file);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function useConvexFileUpload(options) {
|
|
53
|
+
const ctx = useConvexContext();
|
|
54
|
+
const error = ref(null);
|
|
55
|
+
const pendingCount = ref(0);
|
|
56
|
+
const progress = ref(null);
|
|
57
|
+
const upload = async (file, extra) => {
|
|
58
|
+
if (import.meta.server || !ctx.client) {
|
|
59
|
+
throw new Error("[use-convex] useConvexFileUpload can only run in the browser.");
|
|
60
|
+
}
|
|
61
|
+
if (!(file instanceof File)) {
|
|
62
|
+
throw new TypeError("[use-convex] useConvexFileUpload expects a File.");
|
|
63
|
+
}
|
|
64
|
+
pendingCount.value++;
|
|
65
|
+
error.value = null;
|
|
66
|
+
progress.value = null;
|
|
67
|
+
try {
|
|
68
|
+
const uploadUrl = await ctx.client.mutation(
|
|
69
|
+
options.generateUploadUrl,
|
|
70
|
+
{}
|
|
71
|
+
);
|
|
72
|
+
if (!isUploadUrl(uploadUrl)) {
|
|
73
|
+
throw new Error("[use-convex] generateUploadUrl must return a non-empty string URL.");
|
|
74
|
+
}
|
|
75
|
+
const storageId = await postFileToUploadUrl(uploadUrl, file, (fraction) => {
|
|
76
|
+
progress.value = fraction;
|
|
77
|
+
});
|
|
78
|
+
progress.value = 1;
|
|
79
|
+
const saveArgs = {
|
|
80
|
+
storageId,
|
|
81
|
+
name: file.name,
|
|
82
|
+
contentType: file.type || "application/octet-stream",
|
|
83
|
+
size: file.size,
|
|
84
|
+
...extra
|
|
85
|
+
};
|
|
86
|
+
return await ctx.client.mutation(options.saveFile, saveArgs);
|
|
87
|
+
} catch (cause) {
|
|
88
|
+
const err = cause instanceof Error ? cause : new Error(String(cause));
|
|
89
|
+
error.value = err;
|
|
90
|
+
throw err;
|
|
91
|
+
} finally {
|
|
92
|
+
pendingCount.value--;
|
|
93
|
+
progress.value = null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
upload,
|
|
98
|
+
error,
|
|
99
|
+
pending: computed(() => pendingCount.value > 0),
|
|
100
|
+
progress: computed(() => progress.value)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -8,14 +8,8 @@ export function useConvexGate() {
|
|
|
8
8
|
isRefreshing: auth.isRefreshing,
|
|
9
9
|
hasSsrSession: auth.hasSsrSession,
|
|
10
10
|
showAuthedUi: auth.showAuthedUi,
|
|
11
|
-
showLoading: computed(
|
|
12
|
-
|
|
13
|
-
)
|
|
14
|
-
showSignedOut: computed(
|
|
15
|
-
() => !auth.showAuthedUi.value && !auth.isLoading.value
|
|
16
|
-
),
|
|
17
|
-
showRefreshing: computed(
|
|
18
|
-
() => auth.isAuthenticated.value && auth.isRefreshing.value
|
|
19
|
-
)
|
|
11
|
+
showLoading: computed(() => auth.isLoading.value && !auth.showAuthedUi.value),
|
|
12
|
+
showSignedOut: computed(() => !auth.showAuthedUi.value && !auth.isLoading.value),
|
|
13
|
+
showRefreshing: computed(() => auth.isAuthenticated.value && auth.isRefreshing.value)
|
|
20
14
|
};
|
|
21
15
|
}
|