use-convex 0.0.2
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/LICENSE +2 -0
- package/README.md +321 -0
- package/dist/module.d.mts +97 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +232 -0
- package/dist/runtime/components/AuthLoading.d.vue.ts +13 -0
- package/dist/runtime/components/AuthLoading.vue +8 -0
- package/dist/runtime/components/AuthLoading.vue.d.ts +13 -0
- package/dist/runtime/components/AuthRefreshing.d.vue.ts +13 -0
- package/dist/runtime/components/AuthRefreshing.vue +8 -0
- package/dist/runtime/components/AuthRefreshing.vue.d.ts +13 -0
- package/dist/runtime/components/Authenticated.d.vue.ts +13 -0
- package/dist/runtime/components/Authenticated.vue +8 -0
- package/dist/runtime/components/Authenticated.vue.d.ts +13 -0
- package/dist/runtime/components/Unauthenticated.d.vue.ts +13 -0
- package/dist/runtime/components/Unauthenticated.vue +8 -0
- package/dist/runtime/components/Unauthenticated.vue.d.ts +13 -0
- package/dist/runtime/composables/prewarmQuery.d.ts +9 -0
- package/dist/runtime/composables/prewarmQuery.js +14 -0
- package/dist/runtime/composables/useAuth.d.ts +60 -0
- package/dist/runtime/composables/useAuth.js +426 -0
- package/dist/runtime/composables/useAuthToken.d.ts +24 -0
- package/dist/runtime/composables/useAuthToken.js +71 -0
- package/dist/runtime/composables/useConvex.d.ts +5 -0
- package/dist/runtime/composables/useConvex.js +10 -0
- package/dist/runtime/composables/useConvexAction.d.ts +13 -0
- package/dist/runtime/composables/useConvexAction.js +30 -0
- package/dist/runtime/composables/useConvexAuth.d.ts +51 -0
- package/dist/runtime/composables/useConvexAuth.js +96 -0
- package/dist/runtime/composables/useConvexConnectionState.d.ts +8 -0
- package/dist/runtime/composables/useConvexConnectionState.js +21 -0
- package/dist/runtime/composables/useConvexGate.d.ts +20 -0
- package/dist/runtime/composables/useConvexGate.js +21 -0
- package/dist/runtime/composables/useConvexMutation.d.ts +25 -0
- package/dist/runtime/composables/useConvexMutation.js +35 -0
- package/dist/runtime/composables/useConvexPaginatedQuery.d.ts +43 -0
- package/dist/runtime/composables/useConvexPaginatedQuery.js +188 -0
- package/dist/runtime/composables/useConvexQueries.d.ts +27 -0
- package/dist/runtime/composables/useConvexQueries.js +84 -0
- package/dist/runtime/composables/useConvexQuery.d.ts +56 -0
- package/dist/runtime/composables/useConvexQuery.js +147 -0
- package/dist/runtime/plugin.auth.client.d.ts +8 -0
- package/dist/runtime/plugin.auth.client.js +41 -0
- package/dist/runtime/plugin.client.d.ts +7 -0
- package/dist/runtime/plugin.client.js +40 -0
- package/dist/runtime/plugin.devtools.client.d.ts +6 -0
- package/dist/runtime/plugin.devtools.client.js +16 -0
- package/dist/runtime/plugin.server.d.ts +7 -0
- package/dist/runtime/plugin.server.js +44 -0
- package/dist/runtime/server/api/convex/auth/session.d.ts +36 -0
- package/dist/runtime/server/api/convex/auth/session.js +95 -0
- package/dist/runtime/server/auth.d.ts +19 -0
- package/dist/runtime/server/auth.js +32 -0
- package/dist/runtime/server/authCookies.d.ts +9 -0
- package/dist/runtime/server/authCookies.js +86 -0
- package/dist/runtime/server/fetch.d.ts +43 -0
- package/dist/runtime/server/fetch.js +63 -0
- package/dist/runtime/server/routes/__convex_devtools.get.d.ts +6 -0
- package/dist/runtime/server/routes/__convex_devtools.get.js +32 -0
- package/dist/runtime/server/sameOrigin.d.ts +14 -0
- package/dist/runtime/server/sameOrigin.js +33 -0
- package/dist/runtime/utils/authCookie.d.ts +21 -0
- package/dist/runtime/utils/authCookie.js +47 -0
- package/dist/runtime/utils/authGate.d.ts +11 -0
- package/dist/runtime/utils/authGate.js +9 -0
- package/dist/runtime/utils/authMutex.d.ts +15 -0
- package/dist/runtime/utils/authMutex.js +47 -0
- package/dist/runtime/utils/authState.d.ts +31 -0
- package/dist/runtime/utils/authState.js +35 -0
- package/dist/runtime/utils/authStorage.d.ts +55 -0
- package/dist/runtime/utils/authStorage.js +85 -0
- package/dist/runtime/utils/context.d.ts +53 -0
- package/dist/runtime/utils/context.js +41 -0
- package/dist/runtime/utils/fetchToken.d.ts +9 -0
- package/dist/runtime/utils/fetchToken.js +6 -0
- package/dist/runtime/utils/http.d.ts +10 -0
- package/dist/runtime/utils/http.js +13 -0
- package/dist/runtime/utils/oauthRedirect.d.ts +5 -0
- package/dist/runtime/utils/oauthRedirect.js +12 -0
- package/dist/runtime/utils/overlay.d.ts +18 -0
- package/dist/runtime/utils/overlay.js +48 -0
- package/dist/runtime/utils/paginatedOptimistic.d.ts +37 -0
- package/dist/runtime/utils/paginatedOptimistic.js +178 -0
- package/dist/runtime/utils/queryKey.d.ts +8 -0
- package/dist/runtime/utils/queryKey.js +11 -0
- package/dist/types.d.mts +29 -0
- package/package.json +66 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { navigateTo } from "nuxt/app";
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
ref,
|
|
5
|
+
watch
|
|
6
|
+
} from "vue";
|
|
7
|
+
import { useConvexAuth } from "./useConvexAuth.js";
|
|
8
|
+
import { tryUseConvexContext } from "../utils/context.js";
|
|
9
|
+
export function useAuthToken() {
|
|
10
|
+
const token = ref(null);
|
|
11
|
+
const ctx = tryUseConvexContext();
|
|
12
|
+
if (!ctx) {
|
|
13
|
+
return computed(() => null);
|
|
14
|
+
}
|
|
15
|
+
const auth = useConvexAuth();
|
|
16
|
+
const refresh = async () => {
|
|
17
|
+
if (!ctx.client) {
|
|
18
|
+
token.value = null;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const claims = ctx.client.getAuth();
|
|
23
|
+
if (claims?.token) {
|
|
24
|
+
token.value = claims.token;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
if (!auth.isAuthenticated.value) {
|
|
30
|
+
token.value = null;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const fetchToken = ctx.auth.fetchToken;
|
|
34
|
+
if (!fetchToken) {
|
|
35
|
+
token.value = null;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
token.value = await fetchToken({ forceRefreshToken: false }) ?? null;
|
|
40
|
+
} catch {
|
|
41
|
+
token.value = null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
watch(
|
|
45
|
+
[
|
|
46
|
+
() => auth.isAuthenticated.value,
|
|
47
|
+
() => auth.isRefreshing.value,
|
|
48
|
+
() => auth.isLoading.value
|
|
49
|
+
],
|
|
50
|
+
() => {
|
|
51
|
+
void refresh();
|
|
52
|
+
},
|
|
53
|
+
{ immediate: true }
|
|
54
|
+
);
|
|
55
|
+
return computed(() => token.value);
|
|
56
|
+
}
|
|
57
|
+
export function requireConvexAuthMiddleware(options = {}) {
|
|
58
|
+
const redirectTo = options.redirectTo ?? "/login";
|
|
59
|
+
const ctx = tryUseConvexContext();
|
|
60
|
+
if (!ctx) {
|
|
61
|
+
return navigateTo(redirectTo);
|
|
62
|
+
}
|
|
63
|
+
const auth = useConvexAuth();
|
|
64
|
+
if (auth.showAuthedUi.value) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (auth.isLoading.value) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
return navigateTo(redirectTo);
|
|
71
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useConvexContext } from "../utils/context.js";
|
|
2
|
+
export function useConvex() {
|
|
3
|
+
const ctx = useConvexContext();
|
|
4
|
+
if (!ctx.client) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"[convex-nuxt] useConvex() is only available in the browser. Use fetchQuery on the server."
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
return ctx.client;
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FunctionArgs, FunctionReference, FunctionReturnType } from 'convex/server';
|
|
2
|
+
import { type MaybeRefOrGetter } from 'vue';
|
|
3
|
+
/**
|
|
4
|
+
* Browser action helper. Returns `{ run, error, pending }`.
|
|
5
|
+
*
|
|
6
|
+
* Safe to call during SSR setup — the ConvexClient is only touched when
|
|
7
|
+
* `run()` runs in the browser.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useConvexAction<Action extends FunctionReference<'action'>>(action: Action): {
|
|
10
|
+
run: (args?: MaybeRefOrGetter<FunctionArgs<Action>>) => Promise<FunctionReturnType<Action>>;
|
|
11
|
+
error: import("vue").Ref<Error | null, Error | null>;
|
|
12
|
+
pending: import("vue").ComputedRef<boolean>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { computed, ref, toValue } from "vue";
|
|
2
|
+
import { useConvexContext } from "../utils/context.js";
|
|
3
|
+
export function useConvexAction(action) {
|
|
4
|
+
const ctx = useConvexContext();
|
|
5
|
+
const error = ref(null);
|
|
6
|
+
const pendingCount = ref(0);
|
|
7
|
+
const run = async (args = {}) => {
|
|
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--;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
run,
|
|
27
|
+
error,
|
|
28
|
+
pending: computed(() => pendingCount.value > 0)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AuthTokenFetcher } from 'convex/browser';
|
|
2
|
+
import { type ComputedRef, type MaybeRefOrGetter } from 'vue';
|
|
3
|
+
export type { AuthTokenFetcher };
|
|
4
|
+
export interface UseConvexAuthSetupOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Async function returning a JWT (or null). Called by ConvexClient when
|
|
7
|
+
* the current token expires or is rejected. Pass `{ forceRefreshToken: true }`
|
|
8
|
+
* when a fresh token is required.
|
|
9
|
+
*/
|
|
10
|
+
fetchToken: AuthTokenFetcher;
|
|
11
|
+
/**
|
|
12
|
+
* Auth provider is still resolving its initial session.
|
|
13
|
+
* @default false
|
|
14
|
+
*/
|
|
15
|
+
isLoading?: MaybeRefOrGetter<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Auth provider believes a user is signed in (has a session / tokens).
|
|
18
|
+
* Defaults to `true` so a bare `fetchToken` is enough for simple demos —
|
|
19
|
+
* Convex confirmation still gates `isAuthenticated`.
|
|
20
|
+
* @default true
|
|
21
|
+
*/
|
|
22
|
+
isAuthenticated?: MaybeRefOrGetter<boolean>;
|
|
23
|
+
}
|
|
24
|
+
export interface UseConvexAuthReturn {
|
|
25
|
+
isLoading: ComputedRef<boolean>;
|
|
26
|
+
isAuthenticated: ComputedRef<boolean>;
|
|
27
|
+
isRefreshing: ComputedRef<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* JWT cookie is present. Use with `isAuthenticated` to keep an SSR-gated
|
|
30
|
+
* shell mounted while Convex confirms — do not live-subscribe on this alone.
|
|
31
|
+
*/
|
|
32
|
+
hasSsrSession: ComputedRef<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Mount the signed-in shell when the SSR cookie is present **or** Convex
|
|
35
|
+
* has confirmed auth. Prefer this over `isAuthenticated` alone so SSR HTML
|
|
36
|
+
* does not flash the signed-out gate during client confirmation.
|
|
37
|
+
*
|
|
38
|
+
* Live subscriptions must still gate on `isAuthenticated` (use `'skip'`).
|
|
39
|
+
*/
|
|
40
|
+
showAuthedUi: ComputedRef<boolean>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Provider-agnostic Convex auth state.
|
|
44
|
+
*
|
|
45
|
+
* Call once with `{ fetchToken }` (typically in a client plugin) to wire
|
|
46
|
+
* `ConvexClient.setAuth` + token refresh. Call without args anywhere to read
|
|
47
|
+
* `{ isLoading, isAuthenticated, isRefreshing, hasSsrSession }`.
|
|
48
|
+
*
|
|
49
|
+
* Same contract as React `useConvexAuth` / `ConvexProviderWithAuth`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function useConvexAuth(setup?: UseConvexAuthSetupOptions): UseConvexAuthReturn;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
toValue,
|
|
4
|
+
watch
|
|
5
|
+
} from "vue";
|
|
6
|
+
import { useHasSsrSessionRef } from "../utils/authCookie.js";
|
|
7
|
+
import { useConvexContext } from "../utils/context.js";
|
|
8
|
+
import { resolveConvexAuthState } from "../utils/authState.js";
|
|
9
|
+
export function useConvexAuth(setup) {
|
|
10
|
+
const ctx = useConvexContext();
|
|
11
|
+
const auth = ctx.auth;
|
|
12
|
+
const hasSsrSession = useHasSsrSessionRef();
|
|
13
|
+
const showAuthedUi = computed(
|
|
14
|
+
() => auth.isAuthenticated.value || hasSsrSession.value
|
|
15
|
+
);
|
|
16
|
+
if (setup) {
|
|
17
|
+
if (import.meta.server) {
|
|
18
|
+
return {
|
|
19
|
+
isLoading: computed(() => auth.isLoading.value),
|
|
20
|
+
isAuthenticated: computed(() => auth.isAuthenticated.value),
|
|
21
|
+
isRefreshing: computed(() => auth.isRefreshing.value),
|
|
22
|
+
hasSsrSession,
|
|
23
|
+
showAuthedUi
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const client = ctx.client;
|
|
27
|
+
if (!client) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"[convex-nuxt] useConvexAuth({ fetchToken }) requires ConvexClient (browser only)."
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
auth.fetchToken = setup.fetchToken;
|
|
33
|
+
auth.configured = true;
|
|
34
|
+
const syncFromProvider = () => {
|
|
35
|
+
const providerLoading = toValue(setup.isLoading) ?? false;
|
|
36
|
+
const providerAuthenticated = toValue(setup.isAuthenticated) ?? true;
|
|
37
|
+
auth.providerLoading.value = providerLoading;
|
|
38
|
+
auth.providerAuthenticated.value = providerAuthenticated;
|
|
39
|
+
if (providerLoading) {
|
|
40
|
+
auth.isConvexAuthenticated.value = null;
|
|
41
|
+
auth.isRefreshingRaw.value = false;
|
|
42
|
+
applyAuthState(auth);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!providerAuthenticated) {
|
|
46
|
+
auth.isConvexAuthenticated.value = false;
|
|
47
|
+
auth.isRefreshingRaw.value = false;
|
|
48
|
+
try {
|
|
49
|
+
client.client.clearAuth();
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
applyAuthState(auth);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
auth.isConvexAuthenticated.value = null;
|
|
56
|
+
applyAuthState(auth);
|
|
57
|
+
client.client.setAuth(
|
|
58
|
+
setup.fetchToken,
|
|
59
|
+
(backendReportsIsAuthenticated) => {
|
|
60
|
+
auth.isConvexAuthenticated.value = backendReportsIsAuthenticated;
|
|
61
|
+
applyAuthState(auth);
|
|
62
|
+
},
|
|
63
|
+
(isRefreshing) => {
|
|
64
|
+
auth.isRefreshingRaw.value = isRefreshing;
|
|
65
|
+
applyAuthState(auth);
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
watch(
|
|
70
|
+
() => [
|
|
71
|
+
toValue(setup.isLoading) ?? false,
|
|
72
|
+
toValue(setup.isAuthenticated) ?? true
|
|
73
|
+
],
|
|
74
|
+
syncFromProvider,
|
|
75
|
+
{ immediate: true }
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
isLoading: computed(() => auth.isLoading.value),
|
|
80
|
+
isAuthenticated: computed(() => auth.isAuthenticated.value),
|
|
81
|
+
isRefreshing: computed(() => auth.isRefreshing.value),
|
|
82
|
+
hasSsrSession,
|
|
83
|
+
showAuthedUi
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function applyAuthState(auth) {
|
|
87
|
+
const next = resolveConvexAuthState({
|
|
88
|
+
authProviderLoading: auth.providerLoading.value,
|
|
89
|
+
authProviderAuthenticated: auth.providerAuthenticated.value,
|
|
90
|
+
isConvexAuthenticated: auth.isConvexAuthenticated.value,
|
|
91
|
+
isRefreshing: auth.isRefreshingRaw.value
|
|
92
|
+
});
|
|
93
|
+
auth.isLoading.value = next.isLoading;
|
|
94
|
+
auth.isAuthenticated.value = next.isAuthenticated;
|
|
95
|
+
auth.isRefreshing.value = next.isRefreshing;
|
|
96
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ConnectionState } from 'convex/browser';
|
|
2
|
+
import { type ShallowRef } from 'vue';
|
|
3
|
+
export type { ConnectionState };
|
|
4
|
+
/**
|
|
5
|
+
* Reactive WebSocket {@link ConnectionState} for the browser ConvexClient.
|
|
6
|
+
* Returns `null` during SSR or when the client is unavailable.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useConvexConnectionState(): ShallowRef<ConnectionState | null>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { onScopeDispose, shallowRef } from "vue";
|
|
2
|
+
import { useConvexContext } from "../utils/context.js";
|
|
3
|
+
export function useConvexConnectionState() {
|
|
4
|
+
const state = shallowRef(null);
|
|
5
|
+
if (import.meta.server) {
|
|
6
|
+
return state;
|
|
7
|
+
}
|
|
8
|
+
const ctx = useConvexContext();
|
|
9
|
+
const client = ctx.client;
|
|
10
|
+
if (!client) {
|
|
11
|
+
return state;
|
|
12
|
+
}
|
|
13
|
+
state.value = client.connectionState();
|
|
14
|
+
const unsubscribe = client.subscribeToConnectionState((next) => {
|
|
15
|
+
state.value = next;
|
|
16
|
+
});
|
|
17
|
+
onScopeDispose(() => {
|
|
18
|
+
unsubscribe();
|
|
19
|
+
});
|
|
20
|
+
return state;
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ComputedRef } from 'vue';
|
|
2
|
+
export interface ConvexGate {
|
|
3
|
+
isLoading: ComputedRef<boolean>;
|
|
4
|
+
isAuthenticated: ComputedRef<boolean>;
|
|
5
|
+
isRefreshing: ComputedRef<boolean>;
|
|
6
|
+
hasSsrSession: ComputedRef<boolean>;
|
|
7
|
+
showAuthedUi: ComputedRef<boolean>;
|
|
8
|
+
/** True while resolving auth and the authed shell is not yet shown. */
|
|
9
|
+
showLoading: ComputedRef<boolean>;
|
|
10
|
+
/** True when signed out (no cookie, not authenticated, not loading). */
|
|
11
|
+
showSignedOut: ComputedRef<boolean>;
|
|
12
|
+
/** True while refreshing a rejected token for an authenticated session. */
|
|
13
|
+
showRefreshing: ComputedRef<boolean>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Convenience gate flags for auth UI. Same values as the
|
|
17
|
+
* `Authenticated` / `Unauthenticated` / `AuthLoading` / `AuthRefreshing`
|
|
18
|
+
* components.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useConvexGate(): ConvexGate;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useConvexAuth } from "../composables/useConvexAuth.js";
|
|
3
|
+
export function useConvexGate() {
|
|
4
|
+
const auth = useConvexAuth();
|
|
5
|
+
return {
|
|
6
|
+
isLoading: auth.isLoading,
|
|
7
|
+
isAuthenticated: auth.isAuthenticated,
|
|
8
|
+
isRefreshing: auth.isRefreshing,
|
|
9
|
+
hasSsrSession: auth.hasSsrSession,
|
|
10
|
+
showAuthedUi: auth.showAuthedUi,
|
|
11
|
+
showLoading: computed(
|
|
12
|
+
() => auth.isLoading.value && !auth.showAuthedUi.value
|
|
13
|
+
),
|
|
14
|
+
showSignedOut: computed(
|
|
15
|
+
() => !auth.showAuthedUi.value && !auth.isLoading.value
|
|
16
|
+
),
|
|
17
|
+
showRefreshing: computed(
|
|
18
|
+
() => auth.isAuthenticated.value && auth.isRefreshing.value
|
|
19
|
+
)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FunctionArgs, FunctionReference, FunctionReturnType } from 'convex/server';
|
|
2
|
+
import type { OptimisticUpdate } from 'convex/browser';
|
|
3
|
+
import { type MaybeRefOrGetter } from 'vue';
|
|
4
|
+
export type { OptimisticUpdate };
|
|
5
|
+
export interface UseConvexMutationOptions<Mutation extends FunctionReference<'mutation'>> {
|
|
6
|
+
/**
|
|
7
|
+
* Local query update applied while the mutation is in flight.
|
|
8
|
+
* Passed through to `ConvexClient.mutation` → `BaseConvexClient`.
|
|
9
|
+
*/
|
|
10
|
+
optimisticUpdate?: OptimisticUpdate<FunctionArgs<Mutation>>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Browser mutation helper. Returns `{ mutate, error, pending }`.
|
|
14
|
+
*
|
|
15
|
+
* Safe to call during SSR setup — the ConvexClient is only touched when
|
|
16
|
+
* `mutate()` runs in the browser.
|
|
17
|
+
*
|
|
18
|
+
* Pass `{ optimisticUpdate }` to apply temporary local query results while
|
|
19
|
+
* the mutation is in flight (same contract as React `withOptimisticUpdate`).
|
|
20
|
+
*/
|
|
21
|
+
export declare function useConvexMutation<Mutation extends FunctionReference<'mutation'>>(mutation: Mutation, options?: UseConvexMutationOptions<Mutation>): {
|
|
22
|
+
mutate: (args?: MaybeRefOrGetter<FunctionArgs<Mutation>>) => Promise<FunctionReturnType<Mutation>>;
|
|
23
|
+
error: import("vue").Ref<Error | null, Error | null>;
|
|
24
|
+
pending: import("vue").ComputedRef<boolean>;
|
|
25
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { computed, ref, toValue } from "vue";
|
|
2
|
+
import { useConvexContext } from "../utils/context.js";
|
|
3
|
+
export function useConvexMutation(mutation, options = {}) {
|
|
4
|
+
const ctx = useConvexContext();
|
|
5
|
+
const error = ref(null);
|
|
6
|
+
const pendingCount = ref(0);
|
|
7
|
+
const mutate = async (args = {}) => {
|
|
8
|
+
if (import.meta.server || !ctx.client) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"[convex-nuxt] useConvexMutation can only run in the browser."
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
pendingCount.value++;
|
|
14
|
+
error.value = null;
|
|
15
|
+
try {
|
|
16
|
+
const resolved = toValue(args);
|
|
17
|
+
return await ctx.client.mutation(
|
|
18
|
+
mutation,
|
|
19
|
+
resolved,
|
|
20
|
+
options.optimisticUpdate ? { optimisticUpdate: options.optimisticUpdate } : void 0
|
|
21
|
+
);
|
|
22
|
+
} catch (cause) {
|
|
23
|
+
const err = cause instanceof Error ? cause : new Error(String(cause));
|
|
24
|
+
error.value = err;
|
|
25
|
+
throw err;
|
|
26
|
+
} finally {
|
|
27
|
+
pendingCount.value--;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
mutate,
|
|
32
|
+
error,
|
|
33
|
+
pending: computed(() => pendingCount.value > 0)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { FunctionArgs, FunctionReference, FunctionReturnType, PaginationOptions, PaginationResult } from 'convex/server';
|
|
2
|
+
import { type ComputedRef, type MaybeRefOrGetter, type Ref } from 'vue';
|
|
3
|
+
/**
|
|
4
|
+
* A query usable with {@link useConvexPaginatedQuery}.
|
|
5
|
+
* Must accept `paginationOpts` and return {@link PaginationResult}.
|
|
6
|
+
*/
|
|
7
|
+
export type PaginatedQueryReference = FunctionReference<'query', 'public', {
|
|
8
|
+
paginationOpts: PaginationOptions;
|
|
9
|
+
}, PaginationResult<any>>;
|
|
10
|
+
export type PaginatedQueryArgs<Query extends PaginatedQueryReference> = Omit<FunctionArgs<Query>, 'paginationOpts'>;
|
|
11
|
+
export type PaginatedQueryItem<Query extends PaginatedQueryReference> = FunctionReturnType<Query>['page'][number];
|
|
12
|
+
export type PaginationStatus = 'LoadingFirstPage' | 'CanLoadMore' | 'LoadingMore' | 'Exhausted';
|
|
13
|
+
export interface UseConvexPaginatedQueryOptions {
|
|
14
|
+
/** Items to load on the first page (and default for `loadMore`). */
|
|
15
|
+
initialNumItems: number;
|
|
16
|
+
/** Explicit Nuxt payload key for the first page. */
|
|
17
|
+
key?: string;
|
|
18
|
+
/** SSR the first page via HttpClient. Defaults to `convex.server`. */
|
|
19
|
+
server?: boolean;
|
|
20
|
+
/** JWT for the SSR / HttpClient first page. */
|
|
21
|
+
token?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Skip first-page HttpClient and live paginated subscribe until Convex
|
|
24
|
+
* confirms auth. Same contract as `useConvexQuery({ authenticated: true })`.
|
|
25
|
+
* @default false
|
|
26
|
+
*/
|
|
27
|
+
authenticated?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export type UseConvexPaginatedQueryReturn<Query extends PaginatedQueryReference> = {
|
|
30
|
+
results: ComputedRef<Array<PaginatedQueryItem<Query>>>;
|
|
31
|
+
status: ComputedRef<PaginationStatus>;
|
|
32
|
+
isLoading: ComputedRef<boolean>;
|
|
33
|
+
loadMore: (numItems?: number) => void;
|
|
34
|
+
error: Ref<Error | null>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Paginated Convex query with SSR for the first page and live multi-page updates.
|
|
38
|
+
*
|
|
39
|
+
* On the browser this uses `ConvexClient.onPaginatedUpdate_experimental` so
|
|
40
|
+
* every loaded page stays reactive (React `usePaginatedQuery` parity). The SSR
|
|
41
|
+
* snapshot hydrates the first page until the live subscription is ready.
|
|
42
|
+
*/
|
|
43
|
+
export declare function useConvexPaginatedQuery<Query extends PaginatedQueryReference>(query: Query, args: MaybeRefOrGetter<PaginatedQueryArgs<Query> | "skip"> | undefined, options: UseConvexPaginatedQueryOptions): Promise<UseConvexPaginatedQueryReturn<Query>>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { convexToJson, jsonToConvex } from "convex/values";
|
|
2
|
+
import { useAsyncData, useNuxtApp, useRuntimeConfig } from "nuxt/app";
|
|
3
|
+
import {
|
|
4
|
+
computed,
|
|
5
|
+
onScopeDispose,
|
|
6
|
+
ref,
|
|
7
|
+
shallowRef,
|
|
8
|
+
toValue,
|
|
9
|
+
watch
|
|
10
|
+
} from "vue";
|
|
11
|
+
import { resolveAuthGatedArgs } from "../utils/authGate.js";
|
|
12
|
+
import { useConvexContext } from "../utils/context.js";
|
|
13
|
+
import { convexQueryKey } from "../utils/queryKey.js";
|
|
14
|
+
function mapLiveStatus(status) {
|
|
15
|
+
switch (status) {
|
|
16
|
+
case "LoadingFirstPage":
|
|
17
|
+
return "LoadingFirstPage";
|
|
18
|
+
case "LoadingMore":
|
|
19
|
+
return "LoadingMore";
|
|
20
|
+
case "Exhausted":
|
|
21
|
+
return "Exhausted";
|
|
22
|
+
case "CanLoadMore":
|
|
23
|
+
return "CanLoadMore";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function useConvexPaginatedQuery(query, args = {}, options) {
|
|
27
|
+
if (options.initialNumItems <= 0) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"[convex-nuxt] useConvexPaginatedQuery initialNumItems must be > 0"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const runtimeConfig = useRuntimeConfig();
|
|
33
|
+
const defaultServer = runtimeConfig.public.convex?.server;
|
|
34
|
+
const server = options.server ?? defaultServer ?? true;
|
|
35
|
+
const requireAuth = options.authenticated ?? false;
|
|
36
|
+
const ctx = useConvexContext();
|
|
37
|
+
const resolveRawArgs = () => toValue(args);
|
|
38
|
+
const resolveEffectiveArgs = () => resolveAuthGatedArgs(resolveRawArgs(), {
|
|
39
|
+
authenticated: requireAuth,
|
|
40
|
+
isAuthenticated: ctx.auth.isAuthenticated.value
|
|
41
|
+
});
|
|
42
|
+
const firstPageArgsFrom = (base) => {
|
|
43
|
+
if (base === "skip") {
|
|
44
|
+
return "skip";
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
...base,
|
|
48
|
+
paginationOpts: {
|
|
49
|
+
numItems: options.initialNumItems,
|
|
50
|
+
cursor: null
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const key = computed(
|
|
55
|
+
() => convexQueryKey(query, firstPageArgsFrom(resolveRawArgs()), options.key)
|
|
56
|
+
);
|
|
57
|
+
const asyncData = await useAsyncData(
|
|
58
|
+
key,
|
|
59
|
+
async () => {
|
|
60
|
+
const pageArgs = firstPageArgsFrom(resolveEffectiveArgs());
|
|
61
|
+
if (pageArgs === "skip") {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const token = options.token ?? ctx.ssrToken.value;
|
|
65
|
+
if (requireAuth && import.meta.client && !token) {
|
|
66
|
+
const nuxtApp = useNuxtApp();
|
|
67
|
+
const cached = nuxtApp.payload.data[toValue(key)];
|
|
68
|
+
return cached ?? null;
|
|
69
|
+
}
|
|
70
|
+
const http = ctx.createHttpClient({ token });
|
|
71
|
+
const result = await http.query(
|
|
72
|
+
query,
|
|
73
|
+
pageArgs
|
|
74
|
+
);
|
|
75
|
+
return jsonToConvex(convexToJson(result));
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
server,
|
|
79
|
+
watch: [
|
|
80
|
+
() => toValue(args),
|
|
81
|
+
() => options.token ?? ctx.ssrToken.value,
|
|
82
|
+
() => requireAuth && ctx.auth.isAuthenticated.value && !!(options.token ?? ctx.ssrToken.value)
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
const live = shallowRef(null);
|
|
87
|
+
const liveReady = ref(false);
|
|
88
|
+
const error = ref(null);
|
|
89
|
+
const liveLoadMore = shallowRef(null);
|
|
90
|
+
const results = computed(() => {
|
|
91
|
+
if (resolveEffectiveArgs() === "skip") {
|
|
92
|
+
return asyncData.data.value?.page ?? [];
|
|
93
|
+
}
|
|
94
|
+
if (liveReady.value && live.value) {
|
|
95
|
+
return live.value.results;
|
|
96
|
+
}
|
|
97
|
+
return asyncData.data.value?.page ?? [];
|
|
98
|
+
});
|
|
99
|
+
const status = computed(() => {
|
|
100
|
+
if (resolveEffectiveArgs() === "skip") {
|
|
101
|
+
if (asyncData.data.value) {
|
|
102
|
+
return asyncData.data.value.isDone ? "Exhausted" : "CanLoadMore";
|
|
103
|
+
}
|
|
104
|
+
return "Exhausted";
|
|
105
|
+
}
|
|
106
|
+
if (liveReady.value && live.value) {
|
|
107
|
+
return mapLiveStatus(live.value.status);
|
|
108
|
+
}
|
|
109
|
+
if (asyncData.pending.value || import.meta.client && !liveReady.value) {
|
|
110
|
+
if (asyncData.data.value) {
|
|
111
|
+
return asyncData.data.value.isDone ? "Exhausted" : "CanLoadMore";
|
|
112
|
+
}
|
|
113
|
+
return "LoadingFirstPage";
|
|
114
|
+
}
|
|
115
|
+
if (asyncData.data.value) {
|
|
116
|
+
return asyncData.data.value.isDone ? "Exhausted" : "CanLoadMore";
|
|
117
|
+
}
|
|
118
|
+
return "LoadingFirstPage";
|
|
119
|
+
});
|
|
120
|
+
const isLoading = computed(
|
|
121
|
+
() => status.value === "LoadingFirstPage" || status.value === "LoadingMore"
|
|
122
|
+
);
|
|
123
|
+
if (import.meta.client && ctx.client) {
|
|
124
|
+
const client = ctx.client;
|
|
125
|
+
let cancel;
|
|
126
|
+
const subscribe = () => {
|
|
127
|
+
cancel?.();
|
|
128
|
+
cancel = void 0;
|
|
129
|
+
liveReady.value = false;
|
|
130
|
+
live.value = null;
|
|
131
|
+
liveLoadMore.value = null;
|
|
132
|
+
error.value = null;
|
|
133
|
+
const base = resolveEffectiveArgs();
|
|
134
|
+
if (base === "skip") {
|
|
135
|
+
liveReady.value = true;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
cancel = client.onPaginatedUpdate_experimental(
|
|
139
|
+
query,
|
|
140
|
+
base,
|
|
141
|
+
{ initialNumItems: options.initialNumItems },
|
|
142
|
+
(result) => {
|
|
143
|
+
const page = result;
|
|
144
|
+
live.value = page;
|
|
145
|
+
liveLoadMore.value = page.loadMore;
|
|
146
|
+
liveReady.value = true;
|
|
147
|
+
error.value = null;
|
|
148
|
+
},
|
|
149
|
+
(err) => {
|
|
150
|
+
error.value = err;
|
|
151
|
+
liveReady.value = true;
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
watch(
|
|
156
|
+
() => [
|
|
157
|
+
toValue(args),
|
|
158
|
+
options.initialNumItems,
|
|
159
|
+
ctx.auth.isAuthenticated.value
|
|
160
|
+
],
|
|
161
|
+
subscribe,
|
|
162
|
+
{ immediate: true }
|
|
163
|
+
);
|
|
164
|
+
onScopeDispose(() => {
|
|
165
|
+
cancel?.();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const loadMore = (numItems = options.initialNumItems) => {
|
|
169
|
+
if (import.meta.server || !ctx.client) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (status.value !== "CanLoadMore") {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const fn = liveLoadMore.value;
|
|
176
|
+
if (!fn) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
fn(numItems);
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
results,
|
|
183
|
+
status,
|
|
184
|
+
isLoading,
|
|
185
|
+
loadMore,
|
|
186
|
+
error
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FunctionReference, FunctionReturnType } from 'convex/server';
|
|
2
|
+
import type { Value } from 'convex/values';
|
|
3
|
+
import { type ComputedRef, type MaybeRefOrGetter } from 'vue';
|
|
4
|
+
export type ConvexQueriesRequest = Record<string, {
|
|
5
|
+
query: FunctionReference<'query'>;
|
|
6
|
+
args: Record<string, Value>;
|
|
7
|
+
} | 'skip'>;
|
|
8
|
+
export type ConvexQueriesResult<Request extends ConvexQueriesRequest> = {
|
|
9
|
+
[K in keyof Request]: Request[K] extends {
|
|
10
|
+
query: infer Query;
|
|
11
|
+
args: any;
|
|
12
|
+
} ? Query extends FunctionReference<'query'> ? FunctionReturnType<Query> | undefined | Error : undefined : undefined;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Subscribe to a dynamic map of Convex queries (React `useQueries` parity).
|
|
16
|
+
*
|
|
17
|
+
* Pass a reactive object whose values are `{ query, args }` or `'skip'`.
|
|
18
|
+
* Returns a computed map of the same keys → result | `undefined` (loading) |
|
|
19
|
+
* `Error`.
|
|
20
|
+
*
|
|
21
|
+
* Browser-only live subscriptions. During SSR every entry is `undefined`
|
|
22
|
+
* (combine with `useConvexQuery` when you need SSR for known queries).
|
|
23
|
+
*
|
|
24
|
+
* Unchanged keys (same function name + args) keep their existing subscription
|
|
25
|
+
* so deep reactive churn does not tear down WebSocket watches.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useConvexQueries<Request extends ConvexQueriesRequest>(queries: MaybeRefOrGetter<Request>): ComputedRef<ConvexQueriesResult<Request>>;
|