nuxt-convex-module 0.12.3 → 0.14.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/dist/devtools-client/_nuxt/C-J9qZjr.js +3 -0
- package/dist/devtools-client/_nuxt/CB1p6iYX.js +1 -0
- package/dist/devtools-client/_nuxt/{COXexnyN.js → CtC_CmOY.js} +2 -2
- package/dist/devtools-client/_nuxt/{entry.DsZySRVW.css → entry.jOWrhpYd.css} +1 -1
- package/dist/devtools-client/index.html +1 -1
- package/dist/module.d.mts +18 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +129 -38
- package/dist/runtime/better-auth/nuxt/middleware.js +8 -2
- package/dist/runtime/better-auth/nuxt/no-session-vary.d.ts +38 -0
- package/dist/runtime/better-auth/nuxt/no-session-vary.js +21 -0
- package/dist/runtime/better-auth/nuxt/server.js +4 -1
- package/dist/runtime/better-auth/nuxt/shared-render.d.ts +9 -0
- package/dist/runtime/better-auth/nuxt/shared-render.js +17 -0
- package/dist/runtime/better-auth/vue/auth-client-loader.d.ts +17 -0
- package/dist/runtime/better-auth/vue/auth-client-loader.js +186 -0
- package/dist/runtime/better-auth/vue/client.d.ts +3 -3
- package/dist/runtime/better-auth/vue/cross-domain.d.ts +26 -0
- package/dist/runtime/better-auth/vue/cross-domain.js +52 -35
- package/dist/runtime/better-auth/vue/index.d.ts +2 -1
- package/dist/runtime/better-auth/vue/index.js +1 -0
- package/dist/runtime/better-auth/vue/plugin.client.js +21 -2
- package/dist/runtime/better-auth/vue/plugin.server.js +23 -3
- package/dist/runtime/better-auth/vue/token-store.js +25 -1
- package/dist/runtime/better-auth/vue/use-better-auth.d.ts +11 -5
- package/dist/runtime/better-auth/vue/use-better-auth.js +135 -32
- package/dist/runtime/nuxt/composables/use-async-paginated-query.d.ts +1 -1
- package/dist/runtime/nuxt/composables/use-async-paginated-query.js +5 -3
- package/dist/runtime/nuxt/composables/use-async-query.d.ts +4 -1
- package/dist/runtime/nuxt/composables/use-async-query.js +6 -3
- package/dist/runtime/nuxt/csp.d.ts +2 -1
- package/dist/runtime/nuxt/csp.js +9 -3
- package/dist/runtime/nuxt/function-name.d.ts +9 -0
- package/dist/runtime/nuxt/function-name.js +38 -0
- package/dist/runtime/nuxt/index.js +4 -3
- package/dist/runtime/nuxt/testing.d.ts +21 -0
- package/dist/runtime/nuxt/testing.js +1 -0
- package/dist/runtime/vue/auth/index.d.ts +2 -1
- package/dist/runtime/vue/auth/index.js +62 -13
- package/dist/runtime/vue/composables/use-upload-queue.d.ts +70 -3
- package/dist/runtime/vue/composables/use-upload-queue.js +74 -19
- package/dist/runtime/vue/index.d.ts +1 -1
- package/package.json +19 -12
- package/dist/devtools-client/_nuxt/AqohFEu1.js +0 -1
- package/dist/devtools-client/_nuxt/BPtiIkWa.js +0 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { computed, effectScope, hasInjectionContext, inject, shallowRef } from "vue";
|
|
2
|
-
import {
|
|
3
|
-
import { BetterAuthTokenStoreKey, createBetterAuthTokenStore, PENDING_AUTH_VERSION, seedInitialToken } from "./token-store.js";
|
|
1
|
+
import { computed, effectScope, hasInjectionContext, inject, shallowRef, triggerRef, watch } from "vue";
|
|
2
|
+
import { __resetAuthClientLoaderForTests, isAuthClientDeferred, lazyAuthClient, loadAuthClient, loadedAuthClient, onAuthClient, retryAuthClientLoad } from "./auth-client-loader.js";
|
|
3
|
+
import { BetterAuthTokenStoreKey, clearToken, createBetterAuthTokenStore, PENDING_AUTH_VERSION, readTokenClaims, seedInitialToken } from "./token-store.js";
|
|
4
4
|
const browserStore = createBetterAuthTokenStore();
|
|
5
5
|
function useTokenStore() {
|
|
6
6
|
if (!import.meta.server) return browserStore;
|
|
@@ -16,64 +16,163 @@ const serverSession = () => shallowRef({
|
|
|
16
16
|
});
|
|
17
17
|
let browserSession;
|
|
18
18
|
let browserSessionScope;
|
|
19
|
+
let settledWithoutClient = false;
|
|
20
|
+
const clientAnswered = shallowRef(false);
|
|
21
|
+
const pendingSessionState = () => {
|
|
22
|
+
const refetch = async (...args) => {
|
|
23
|
+
await loadAuthClient();
|
|
24
|
+
const next = browserSession.value.refetch;
|
|
25
|
+
if (next === refetch) return;
|
|
26
|
+
return next(...args);
|
|
27
|
+
};
|
|
28
|
+
return { data: null, error: null, isPending: true, isRefetching: false, refetch };
|
|
29
|
+
};
|
|
19
30
|
const useClientSession = () => {
|
|
20
31
|
if (import.meta.server) return serverSession();
|
|
21
32
|
browserSessionScope ??= effectScope(true);
|
|
22
|
-
|
|
33
|
+
const scope = browserSessionScope;
|
|
34
|
+
return browserSession ??= scope.run(() => {
|
|
35
|
+
const session = shallowRef(pendingSessionState());
|
|
36
|
+
onAuthClient((client) => scope.active && scope.run(() => {
|
|
37
|
+
watch(client.useSession(), (state) => {
|
|
38
|
+
if (settledWithoutClient && state.isPending) {
|
|
39
|
+
if (session.value.refetch !== state.refetch) session.value = { ...session.value, refetch: state.refetch };
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
settledWithoutClient = false;
|
|
43
|
+
session.value = state;
|
|
44
|
+
if (!state.isPending) clientAnswered.value = true;
|
|
45
|
+
}, { immediate: true, flush: "sync" });
|
|
46
|
+
}));
|
|
47
|
+
if (!isAuthClientDeferred()) void loadAtStartup();
|
|
48
|
+
watch(session, ({ data, isPending }) => {
|
|
49
|
+
if (!data && !isPending) clearToken(browserStore);
|
|
50
|
+
}, { flush: "sync" });
|
|
51
|
+
return session;
|
|
52
|
+
});
|
|
23
53
|
};
|
|
54
|
+
async function loadAtStartup() {
|
|
55
|
+
try {
|
|
56
|
+
await loadAuthClient().catch(() => loadAuthClient());
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn("[nuxt-convex-module] Failed to load the Better Auth client:", error);
|
|
59
|
+
if (!coveredByServerToken()) settle(error);
|
|
60
|
+
retryAuthClientLoad();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function coveredByServerToken() {
|
|
64
|
+
return Boolean(browserStore.cachedToken.value) && browserStore.cachedTokenVersion === PENDING_AUTH_VERSION;
|
|
65
|
+
}
|
|
66
|
+
export async function clientSessionSettled() {
|
|
67
|
+
useClientSession();
|
|
68
|
+
try {
|
|
69
|
+
await loadAuthClient().catch(() => loadAuthClient());
|
|
70
|
+
} catch {
|
|
71
|
+
if (!coveredByServerToken()) settle();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (clientAnswered.value) return;
|
|
75
|
+
await new Promise((resolve) => {
|
|
76
|
+
const stop = watch(clientAnswered, (answered) => {
|
|
77
|
+
if (!answered) return;
|
|
78
|
+
stop();
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function settle(error = null) {
|
|
84
|
+
const session = useClientSession();
|
|
85
|
+
if (loadedAuthClient() || !session.value.isPending) return;
|
|
86
|
+
settledWithoutClient = true;
|
|
87
|
+
session.value = { ...session.value, isPending: false, error };
|
|
88
|
+
}
|
|
89
|
+
export function settleSessionWithoutClient() {
|
|
90
|
+
settle();
|
|
91
|
+
}
|
|
24
92
|
export function useBetterAuth(initialToken) {
|
|
25
93
|
const store = useTokenStore();
|
|
26
94
|
seedInitialToken(store, initialToken);
|
|
27
95
|
const { cachedToken } = store;
|
|
28
96
|
const session = useClientSession();
|
|
29
|
-
const client =
|
|
97
|
+
const client = loadedAuthClient() ?? lazyAuthClient;
|
|
30
98
|
const authVersion = computed(() => {
|
|
31
99
|
const data = session.value.data;
|
|
32
|
-
|
|
100
|
+
const id = data?.session?.id ?? data?.user?.id ?? null;
|
|
101
|
+
return id === null && session.value.isPending ? void 0 : id;
|
|
33
102
|
});
|
|
34
|
-
const currentAuthVersion = () => authVersion.value
|
|
103
|
+
const currentAuthVersion = () => authVersion.value === void 0 ? PENDING_AUTH_VERSION : authVersion.value;
|
|
104
|
+
const holdsAnotherSessionsToken = computed(() => {
|
|
105
|
+
const data = session.value.data;
|
|
106
|
+
if (!data || !cachedToken.value || store.cachedTokenVersion !== PENDING_AUTH_VERSION) return false;
|
|
107
|
+
if (store.initialTokenSessionId && data.session?.id) return store.initialTokenSessionId !== data.session.id;
|
|
108
|
+
const subject = readTokenClaims(cachedToken.value)?.sub;
|
|
109
|
+
return !!data.user?.id && subject !== void 0 && subject !== data.user.id;
|
|
110
|
+
});
|
|
111
|
+
const isServerTokenActive = () => session.value.isPending && cachedToken.value !== null && store.cachedTokenVersion === PENDING_AUTH_VERSION;
|
|
112
|
+
const signedInSince = async (forceRefreshToken) => {
|
|
113
|
+
if (!forceRefreshToken || import.meta.server || clientAnswered.value) return false;
|
|
114
|
+
await clientSessionSettled();
|
|
115
|
+
return currentAuthVersion() !== null;
|
|
116
|
+
};
|
|
35
117
|
const fetchAccessToken = async ({
|
|
36
118
|
forceRefreshToken = false
|
|
37
119
|
} = {}) => {
|
|
38
120
|
const version = currentAuthVersion();
|
|
39
121
|
if (version === null) {
|
|
40
|
-
|
|
41
|
-
store
|
|
42
|
-
store.pendingToken = null;
|
|
43
|
-
store.pendingTokenVersion = null;
|
|
122
|
+
if (await signedInSince(forceRefreshToken)) return fetchAccessToken({ forceRefreshToken });
|
|
123
|
+
clearToken(store);
|
|
44
124
|
return null;
|
|
45
125
|
}
|
|
46
|
-
if (
|
|
47
|
-
|
|
126
|
+
if (store.cachedTokenVersion === PENDING_AUTH_VERSION && store.initialTokenSessionId === version) {
|
|
127
|
+
store.cachedTokenVersion = version;
|
|
48
128
|
}
|
|
49
|
-
if (!forceRefreshToken
|
|
50
|
-
|
|
129
|
+
if (!forceRefreshToken) {
|
|
130
|
+
if (store.pendingToken && store.pendingTokenVersion === version) {
|
|
131
|
+
return store.pendingToken;
|
|
132
|
+
}
|
|
133
|
+
if (cachedToken.value && store.cachedTokenVersion === version) {
|
|
134
|
+
return cachedToken.value;
|
|
135
|
+
}
|
|
51
136
|
}
|
|
137
|
+
const afterFailure = () => {
|
|
138
|
+
if (cachedToken.value && !holdsAnotherSessionsToken.value && (store.cachedTokenVersion === version || store.cachedTokenVersion === PENDING_AUTH_VERSION)) {
|
|
139
|
+
return cachedToken.value;
|
|
140
|
+
}
|
|
141
|
+
if (isLatest()) {
|
|
142
|
+
cachedToken.value = null;
|
|
143
|
+
store.cachedTokenVersion = version;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
52
147
|
store.pendingTokenVersion = version;
|
|
53
|
-
const
|
|
148
|
+
const isLatest = () => store.pendingToken === pending;
|
|
149
|
+
const pending = store.pendingToken = loadAuthClient().then((client2) => client2.convex.token({ fetchOptions: { throw: false } })).then(({ data, error }) => {
|
|
150
|
+
if (error && error.status !== 401) {
|
|
151
|
+
return afterFailure();
|
|
152
|
+
}
|
|
54
153
|
const token = data?.token || null;
|
|
55
|
-
|
|
56
|
-
|
|
154
|
+
if (isLatest()) {
|
|
155
|
+
store.cachedTokenVersion = version;
|
|
156
|
+
cachedToken.value = token;
|
|
157
|
+
triggerRef(cachedToken);
|
|
158
|
+
}
|
|
57
159
|
return token;
|
|
58
|
-
}).catch(() => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
store.pendingToken = null;
|
|
64
|
-
store.pendingTokenVersion = null;
|
|
160
|
+
}).catch(afterFailure).finally(() => {
|
|
161
|
+
if (isLatest()) {
|
|
162
|
+
store.pendingToken = null;
|
|
163
|
+
store.pendingTokenVersion = null;
|
|
164
|
+
}
|
|
65
165
|
});
|
|
66
166
|
return pending;
|
|
67
167
|
};
|
|
68
168
|
return {
|
|
69
|
-
isLoading: computed(() => session.value.isPending && !
|
|
169
|
+
isLoading: computed(() => session.value.isPending && !isServerTokenActive() || holdsAnotherSessionsToken.value),
|
|
70
170
|
// PARITY: D-03
|
|
71
|
-
// Diverges from upstream's `Boolean(session?.session) ||
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// session data without a nested `session` still counts as signed in.
|
|
171
|
+
// Diverges from upstream's `Boolean(session?.session) || isSsrTokenActive`:
|
|
172
|
+
// Better Auth session data without a nested `session` still counts as
|
|
173
|
+
// signed in.
|
|
75
174
|
isAuthenticated: computed(
|
|
76
|
-
() => !!session.value.data ||
|
|
175
|
+
() => !!session.value.data || isServerTokenActive()
|
|
77
176
|
),
|
|
78
177
|
fetchAccessToken,
|
|
79
178
|
// PARITY: A-06 — Vue-only service extensions.
|
|
@@ -86,10 +185,14 @@ export function useBetterAuth(initialToken) {
|
|
|
86
185
|
authVersion
|
|
87
186
|
};
|
|
88
187
|
}
|
|
89
|
-
export function
|
|
188
|
+
export function resetBetterAuthState() {
|
|
90
189
|
Object.assign(browserStore, createBetterAuthTokenStore(), { cachedToken: browserStore.cachedToken });
|
|
91
190
|
browserStore.cachedToken.value = null;
|
|
92
191
|
browserSessionScope?.stop();
|
|
93
192
|
browserSessionScope = void 0;
|
|
94
193
|
browserSession = void 0;
|
|
194
|
+
settledWithoutClient = false;
|
|
195
|
+
clientAnswered.value = false;
|
|
196
|
+
__resetAuthClientLoaderForTests();
|
|
95
197
|
}
|
|
198
|
+
export const __resetUseBetterAuthForTests = resetBetterAuthState;
|
|
@@ -32,7 +32,7 @@ export interface AsyncPaginatedQueryData<Item> {
|
|
|
32
32
|
status: ComputedRef<PaginationStatus>;
|
|
33
33
|
/** `true` while no page — server or live — is available yet, or more rows are loading. */
|
|
34
34
|
isLoading: ComputedRef<boolean>;
|
|
35
|
-
/** The SSR fetch error
|
|
35
|
+
/** The live subscription's error, or the SSR fetch error until the live query has a page; `null` when none. Never thrown. */
|
|
36
36
|
error: ComputedRef<Error | null>;
|
|
37
37
|
/**
|
|
38
38
|
* Fetch `numItems` more rows. Takes effect once the live subscription has
|
|
@@ -70,9 +70,11 @@ export function useAsyncPaginatedQuery(query, args, options) {
|
|
|
70
70
|
return "LoadingFirstPage";
|
|
71
71
|
});
|
|
72
72
|
const isLoading = computed(() => status.value === "LoadingFirstPage" || status.value === "LoadingMore");
|
|
73
|
-
const error = computed(
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
const error = computed(() => {
|
|
74
|
+
const liveError = liveState.value?.error ?? clientError.value;
|
|
75
|
+
if (liveError) return liveError;
|
|
76
|
+
return liveHasPage.value ? null : asyncData.error.value ?? null;
|
|
77
|
+
});
|
|
76
78
|
const loadMore = (numItems) => {
|
|
77
79
|
live?.loadMore(numItems);
|
|
78
80
|
};
|
|
@@ -66,7 +66,10 @@ export interface AsyncQueryReturn<T> extends PromiseLike<AsyncQueryData<T>> {
|
|
|
66
66
|
* nothing has loaded (or while skipped).
|
|
67
67
|
*/
|
|
68
68
|
data: ComputedRef<T | undefined>;
|
|
69
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* The initial-fetch or live-subscription error, `null` when none. A failed
|
|
71
|
+
* initial fetch stops counting once the live subscription has a value.
|
|
72
|
+
*/
|
|
70
73
|
error: ComputedRef<Error | null>;
|
|
71
74
|
/**
|
|
72
75
|
* Initial-fetch status. Live pushes don't churn it: once data exists it
|
|
@@ -75,9 +75,12 @@ export function useAsyncQuery(query, args, options = {}) {
|
|
|
75
75
|
const data = computed(
|
|
76
76
|
() => liveValue.value !== void 0 ? liveValue.value : payloadValue.value
|
|
77
77
|
);
|
|
78
|
-
const error = computed(
|
|
79
|
-
(
|
|
80
|
-
|
|
78
|
+
const error = computed(() => {
|
|
79
|
+
if (liveError.value) {
|
|
80
|
+
return liveError.value;
|
|
81
|
+
}
|
|
82
|
+
return liveValue.value !== void 0 ? null : asyncData.error.value ?? null;
|
|
83
|
+
});
|
|
81
84
|
const status = computed(() => {
|
|
82
85
|
if (liveError.value) {
|
|
83
86
|
return "error";
|
|
@@ -37,7 +37,8 @@ export declare function convexResourceSrc(url?: string, siteUrl?: string): strin
|
|
|
37
37
|
*
|
|
38
38
|
* For each directive we keep any value the user already set and *append* the
|
|
39
39
|
* Convex origins, so consumers extend (add their own third parties) rather than
|
|
40
|
-
* fight the defaults, and the Convex origins are always present.
|
|
40
|
+
* fight the defaults, and the Convex origins are always present. That covers a
|
|
41
|
+
* directive the user left to `default-src`, whose places carry over. A user who sets
|
|
41
42
|
* `contentSecurityPolicy: false` (CSP disabled) is left untouched, as is a rule
|
|
42
43
|
* set with nothing to add (no Convex URL).
|
|
43
44
|
*/
|
package/dist/runtime/nuxt/csp.js
CHANGED
|
@@ -39,8 +39,14 @@ export function applyConvexSecurityDefaults(rules, connectSrc, resourceSrc) {
|
|
|
39
39
|
tightenDirective(csp, "media-src", ["'self'"], resourceSrc);
|
|
40
40
|
}
|
|
41
41
|
function tightenDirective(csp, name, baseline, additions) {
|
|
42
|
-
if (additions.length === 0) return;
|
|
43
|
-
const
|
|
44
|
-
const existing = Array.isArray(current) ? current : typeof current === "string" ? current.split(/\s+/).filter(Boolean) : baseline;
|
|
42
|
+
if (additions.length === 0 || csp[name] === false) return;
|
|
43
|
+
const existing = sourcesOf(csp[name]) ?? [...baseline, ...inheritedSources(csp)];
|
|
45
44
|
csp[name] = uniq([...existing, ...additions]);
|
|
46
45
|
}
|
|
46
|
+
function sourcesOf(value) {
|
|
47
|
+
if (Array.isArray(value)) return value;
|
|
48
|
+
return typeof value === "string" ? value.split(/\s+/).filter(Boolean) : void 0;
|
|
49
|
+
}
|
|
50
|
+
function inheritedSources(csp) {
|
|
51
|
+
return (sourcesOf(csp["default-src"]) ?? []).filter((source) => source === "'self'" || !source.startsWith("'"));
|
|
52
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getFunctionName } from 'convex/server';
|
|
2
|
+
declare const SOURCE_PREFIX: {
|
|
3
|
+
readonly query: "Q";
|
|
4
|
+
readonly mutation: "M";
|
|
5
|
+
readonly action: "A";
|
|
6
|
+
};
|
|
7
|
+
/** `call`, with the function's name put in front of the message of the error it rejects with. */
|
|
8
|
+
export declare function withFunctionName<T>(source: keyof typeof SOURCE_PREFIX, reference: Parameters<typeof getFunctionName>[0], call: Promise<T>): Promise<T>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getFunctionName } from "convex/server";
|
|
2
|
+
const SOURCE_PREFIX = { query: "Q", mutation: "M", action: "A" };
|
|
3
|
+
const unnamed = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
export async function withFunctionName(source, reference, call) {
|
|
5
|
+
try {
|
|
6
|
+
return await call;
|
|
7
|
+
} catch (error) {
|
|
8
|
+
if (error instanceof Error && !error.message.startsWith("[CONVEX ")) name(error, `[CONVEX ${SOURCE_PREFIX[source]}(${getFunctionName(reference)})] `);
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function name(error, prefix) {
|
|
13
|
+
const original = error.message;
|
|
14
|
+
const stack = error.stack;
|
|
15
|
+
try {
|
|
16
|
+
error.message = `${prefix}${original}`;
|
|
17
|
+
} catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (error.message !== `${prefix}${original}`) return;
|
|
21
|
+
unnamed.set(error, { message: original, stack });
|
|
22
|
+
const header = `${error.name}: ${original}`;
|
|
23
|
+
if (original && stack?.startsWith(header)) error.stack = `${error.name}: ${error.message}${stack.slice(header.length)}`;
|
|
24
|
+
}
|
|
25
|
+
export function withoutFunctionName(error, read) {
|
|
26
|
+
const before = error instanceof Error ? unnamed.get(error) : void 0;
|
|
27
|
+
if (!before) return read(error);
|
|
28
|
+
const named = error;
|
|
29
|
+
const { message, stack } = named;
|
|
30
|
+
named.message = before.message;
|
|
31
|
+
named.stack = before.stack;
|
|
32
|
+
try {
|
|
33
|
+
return read(error);
|
|
34
|
+
} finally {
|
|
35
|
+
named.message = message;
|
|
36
|
+
named.stack = stack;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -2,6 +2,7 @@ import { ConvexHttpClient } from "convex/browser";
|
|
|
2
2
|
import { getFunctionName } from "convex/server";
|
|
3
3
|
import { convexToJson, jsonToConvex } from "convex/values";
|
|
4
4
|
import { getConvexRuntimeConfig } from "./config.js";
|
|
5
|
+
import { withFunctionName } from "./function-name.js";
|
|
5
6
|
export async function preloadQuery(query, ...args) {
|
|
6
7
|
const value = await fetchQuery(query, ...args);
|
|
7
8
|
const preloaded = {
|
|
@@ -17,17 +18,17 @@ export function preloadedQueryResult(preloaded) {
|
|
|
17
18
|
export async function fetchQuery(query, ...args) {
|
|
18
19
|
const [fnArgs, options] = args;
|
|
19
20
|
const client = setupClient(options ?? {});
|
|
20
|
-
return client.query(query, fnArgs || {});
|
|
21
|
+
return withFunctionName("query", query, client.query(query, fnArgs || {}));
|
|
21
22
|
}
|
|
22
23
|
export async function fetchMutation(mutation, ...args) {
|
|
23
24
|
const [fnArgs, options] = args;
|
|
24
25
|
const client = setupClient(options ?? {});
|
|
25
|
-
return client.mutation(mutation, fnArgs || {});
|
|
26
|
+
return withFunctionName("mutation", mutation, client.mutation(mutation, fnArgs || {}));
|
|
26
27
|
}
|
|
27
28
|
export async function fetchAction(action, ...args) {
|
|
28
29
|
const [fnArgs, options] = args;
|
|
29
30
|
const client = setupClient(options ?? {});
|
|
30
|
-
return client.action(action, fnArgs || {});
|
|
31
|
+
return withFunctionName("action", action, client.action(action, fnArgs || {}));
|
|
31
32
|
}
|
|
32
33
|
function setupClient(options) {
|
|
33
34
|
if ("url" in options && options.url === void 0) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for an app's own tests, kept off the runtime entries.
|
|
3
|
+
*
|
|
4
|
+
* The module keeps some state for the app's lifetime, as upstream's provider
|
|
5
|
+
* keeps it for the provider's: a test that signs in leaves it behind for the
|
|
6
|
+
* next. Reset it here instead of reaching into the module's files by path.
|
|
7
|
+
*
|
|
8
|
+
* Load it where the app's runtime loads, in `@nuxt/test-utils`' `nuxt`
|
|
9
|
+
* environment: it imports `#convex/auth-client`, the alias the module (or
|
|
10
|
+
* your test config) registers.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { afterEach } from 'vitest'
|
|
14
|
+
* import { resetBetterAuthState } from 'nuxt-convex-module/testing'
|
|
15
|
+
*
|
|
16
|
+
* afterEach(() => resetBetterAuthState())
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @module testing
|
|
20
|
+
*/
|
|
21
|
+
export { resetBetterAuthState } from '../better-auth/vue/use-better-auth.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { resetBetterAuthState } from "../better-auth/vue/use-better-auth.js";
|
|
@@ -66,7 +66,8 @@ export interface ConvexAuthProviderOptions {
|
|
|
66
66
|
* Alternative re-authentication trigger for providers that hand out a
|
|
67
67
|
* stable fetcher function: when this value changes, auth state transitions
|
|
68
68
|
* back to loading, `clearAuth` runs, and `fetchAccessToken` is called
|
|
69
|
-
* again against the new auth context.
|
|
69
|
+
* again against the new auth context. `undefined` means the provider does
|
|
70
|
+
* not know the context yet, so the first value it learns is no change.
|
|
70
71
|
*/
|
|
71
72
|
authVersion?: MaybeRefOrGetter<unknown>;
|
|
72
73
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, effectScope, inject, provide, ref, toValue, unref, watch
|
|
1
|
+
import { computed, effectScope, inject, nextTick, provide, ref, toValue, unref, watch } from "vue";
|
|
2
2
|
export const ConvexAuthStateKey = Symbol("ConvexAuthState");
|
|
3
3
|
export function useConvexAuth() {
|
|
4
4
|
const authContext = inject(ConvexAuthStateKey);
|
|
@@ -9,6 +9,27 @@ export function useConvexAuth() {
|
|
|
9
9
|
}
|
|
10
10
|
return authContext;
|
|
11
11
|
}
|
|
12
|
+
const pendingClears = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
function clearAuthLast(client, ended) {
|
|
14
|
+
const pending = pendingClears.get(client);
|
|
15
|
+
if (pending) {
|
|
16
|
+
if (pending.start === ended) {
|
|
17
|
+
pending.start = null;
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const clear = { start: null };
|
|
22
|
+
pendingClears.set(client, clear);
|
|
23
|
+
void nextTick(() => {
|
|
24
|
+
pendingClears.delete(client);
|
|
25
|
+
try {
|
|
26
|
+
client.clearAuth();
|
|
27
|
+
} catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
clear.start?.();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
12
33
|
export function provideConvexAuth(options) {
|
|
13
34
|
const authState = createConvexAuthState(options);
|
|
14
35
|
provide(ConvexAuthStateKey, authState);
|
|
@@ -43,16 +64,37 @@ export function createConvexAuthState(options, scope) {
|
|
|
43
64
|
},
|
|
44
65
|
{ immediate: true }
|
|
45
66
|
);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
const authContext = ref(0);
|
|
68
|
+
watch(
|
|
69
|
+
() => toValue(authVersion),
|
|
70
|
+
(_version, previous) => {
|
|
71
|
+
if (previous !== void 0) authContext.value++;
|
|
49
72
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
);
|
|
74
|
+
watch(
|
|
75
|
+
[
|
|
76
|
+
// The loading flag is here only so auth re-registers when it flips,
|
|
77
|
+
// mirroring upstream's effect deps. It must not gate `setAuth`: a
|
|
78
|
+
// provider may report a cached session (authenticated) while still
|
|
79
|
+
// revalidating (loading), and upstream authenticates the client
|
|
80
|
+
// immediately in that window.
|
|
81
|
+
() => toValue(authProviderLoading),
|
|
82
|
+
authContext,
|
|
83
|
+
// Unwrapping here tracks the ref, so a new fetcher identity
|
|
84
|
+
// re-registers auth: upstream's effect re-running when
|
|
85
|
+
// `fetchAccessToken` changes between renders.
|
|
86
|
+
() => unref(fetchAccessToken),
|
|
87
|
+
() => toValue(authProviderAuthenticated)
|
|
88
|
+
],
|
|
89
|
+
([, , fetchToken, authenticated], _previous, onCleanup) => {
|
|
90
|
+
if (import.meta.server) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!authenticated) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let isThisEffectRelevant = true;
|
|
97
|
+
const start = () => client.setAuth(
|
|
56
98
|
fetchToken,
|
|
57
99
|
(convexReportsIsAuthenticated) => {
|
|
58
100
|
if (isThisEffectRelevant) {
|
|
@@ -67,14 +109,21 @@ export function createConvexAuthState(options, scope) {
|
|
|
67
109
|
}
|
|
68
110
|
}
|
|
69
111
|
);
|
|
112
|
+
const pending = pendingClears.get(client);
|
|
113
|
+
if (pending) {
|
|
114
|
+
pending.start = start;
|
|
115
|
+
} else {
|
|
116
|
+
start();
|
|
117
|
+
}
|
|
70
118
|
onCleanup(() => {
|
|
71
119
|
isThisEffectRelevant = false;
|
|
72
|
-
client.clearAuth();
|
|
73
120
|
isConvexAuthenticated.value = null;
|
|
74
121
|
isRefreshing.value = false;
|
|
122
|
+
clearAuthLast(client, start);
|
|
75
123
|
});
|
|
76
|
-
}
|
|
77
|
-
|
|
124
|
+
},
|
|
125
|
+
{ immediate: true }
|
|
126
|
+
);
|
|
78
127
|
};
|
|
79
128
|
if (scope) {
|
|
80
129
|
scope.run(register);
|
|
@@ -6,6 +6,13 @@ import { type GenerateUploadUrl, type StorageId } from './use-upload.js';
|
|
|
6
6
|
* @public
|
|
7
7
|
*/
|
|
8
8
|
export type UploadItemStatus = 'pending' | 'uploading' | 'success' | 'error';
|
|
9
|
+
/**
|
|
10
|
+
* Which step an `uploading` item is on: its `prepare` hook, the transfer, or
|
|
11
|
+
* its `after` hook. `null` outside `uploading`.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export type UploadItemStage = 'prepare' | 'upload' | 'after';
|
|
9
16
|
/**
|
|
10
17
|
* A single entry in the queue managed by {@link useUploadQueue}.
|
|
11
18
|
*
|
|
@@ -20,10 +27,27 @@ export interface UploadQueueItem {
|
|
|
20
27
|
status: UploadItemStatus;
|
|
21
28
|
/** Upload progress for this item, from `0` to `1`. */
|
|
22
29
|
progress: number;
|
|
23
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* The storage id once the file is stored, or `null`. An item that failed
|
|
32
|
+
* after that keeps it (see `after` and `onItemSuccess`).
|
|
33
|
+
*/
|
|
24
34
|
storageId: StorageId | null;
|
|
25
35
|
/** The error if this item failed, or `null`. */
|
|
26
36
|
error: Error | null;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the error is a refusal rather than a failure: the app said no
|
|
39
|
+
* (`isRefusal`; by default a `ConvexError`), so the same file would be
|
|
40
|
+
* turned away again. `false` while the item has no error.
|
|
41
|
+
*/
|
|
42
|
+
refused: boolean;
|
|
43
|
+
/** The step an `uploading` item is on, or `null`. */
|
|
44
|
+
stage: UploadItemStage | null;
|
|
45
|
+
/**
|
|
46
|
+
* What `prepare` returned, which is what gets sent; `null` until it runs,
|
|
47
|
+
* or without a `prepare` hook (the file itself is sent). Kept for a retry
|
|
48
|
+
* and for a preview.
|
|
49
|
+
*/
|
|
50
|
+
prepared: Blob | null;
|
|
27
51
|
}
|
|
28
52
|
/**
|
|
29
53
|
* Options for {@link useUploadQueue}.
|
|
@@ -33,9 +57,39 @@ export interface UploadQueueItem {
|
|
|
33
57
|
export interface UseUploadQueueOptions {
|
|
34
58
|
/** Maximum number of uploads to run at once. Defaults to `3`. */
|
|
35
59
|
concurrency?: number;
|
|
36
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Turn a queued file into what is sent, before an upload URL is asked
|
|
62
|
+
* for: resize, re-encode, strip metadata. Throw to turn the file away; the
|
|
63
|
+
* item ends `error`, refused when `isRefusal` says so. Runs once per
|
|
64
|
+
* item: a retry reuses what it returned.
|
|
65
|
+
*/
|
|
66
|
+
prepare?: (file: Blob, item: UploadQueueItem, signal: AbortSignal) => Blob | Promise<Blob>;
|
|
67
|
+
/**
|
|
68
|
+
* Runs once the file is stored, before the item counts as a success:
|
|
69
|
+
* attach the storage id to a document, run a check on it. The item stays
|
|
70
|
+
* `uploading` (stage `'after'`) until it settles. A throw ends the item
|
|
71
|
+
* `error`; a retry then runs only this hook again, with the same storage
|
|
72
|
+
* id, unless the throw was a refusal, which sends the file again. After a
|
|
73
|
+
* refusal `item.storageId` still holds the stored file while the item is
|
|
74
|
+
* in error, so `onItemError` can delete it; the queue never deletes files,
|
|
75
|
+
* and a retry forgets it before uploading again.
|
|
76
|
+
*/
|
|
77
|
+
after?: (storageId: StorageId, item: UploadQueueItem, signal: AbortSignal) => void | Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Whether an error is a refusal (the app turned the file away) rather than
|
|
80
|
+
* a failure (the network, the deployment, an abort). Defaults to: the
|
|
81
|
+
* error is a `ConvexError`, which a Convex function throws on purpose.
|
|
82
|
+
* An aborted item is never refused, and neither is an error this check
|
|
83
|
+
* throws on.
|
|
84
|
+
*/
|
|
85
|
+
isRefusal?: (error: Error, item: UploadQueueItem) => boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Called when an individual item finishes uploading (after `after`). A
|
|
88
|
+
* throw ends the item `error` with its storage id kept; a retry then runs
|
|
89
|
+
* only this callback again, never `after` a second time.
|
|
90
|
+
*/
|
|
37
91
|
onItemSuccess?: (storageId: StorageId, item: UploadQueueItem) => void | Promise<void>;
|
|
38
|
-
/** Called when an individual item fails. */
|
|
92
|
+
/** Called when an individual item fails or is refused (`item.refused` says which). */
|
|
39
93
|
onItemError?: (error: Error, item: UploadQueueItem) => void;
|
|
40
94
|
/** Called once every queued item has settled (succeeded or failed). */
|
|
41
95
|
onComplete?: (items: UploadQueueItem[]) => void;
|
|
@@ -66,6 +120,14 @@ export interface VueUploadQueue {
|
|
|
66
120
|
remove: (id: string) => void;
|
|
67
121
|
/** Abort everything and empty the queue. */
|
|
68
122
|
clear: () => void;
|
|
123
|
+
/**
|
|
124
|
+
* Queue an errored item again, or every errored item without an id. It
|
|
125
|
+
* picks up where it stopped: `onItemSuccess` alone when `after` already
|
|
126
|
+
* finished, `after` alone when the file is stored, the transfer when
|
|
127
|
+
* `prepare` already ran. Whether a refused item is worth
|
|
128
|
+
* retrying is the app's call; nothing here stops it.
|
|
129
|
+
*/
|
|
130
|
+
retry: (id?: string) => void;
|
|
69
131
|
}
|
|
70
132
|
/**
|
|
71
133
|
* Upload many files to Convex file storage with bounded concurrency and
|
|
@@ -76,6 +138,11 @@ export interface VueUploadQueue {
|
|
|
76
138
|
* a multi-file `<input>`) and the queue uploads up to `concurrency` at a time,
|
|
77
139
|
* exposing per-item and aggregate progress.
|
|
78
140
|
*
|
|
141
|
+
* Options `prepare` and `after` add the app's own steps on either side of
|
|
142
|
+
* the transfer (shrink a photo first; attach it once stored), and a failed
|
|
143
|
+
* item says whether the app `refused` it or it failed, so the UI can show a
|
|
144
|
+
* reason for one and {@link VueUploadQueue.retry} the other.
|
|
145
|
+
*
|
|
79
146
|
* @example
|
|
80
147
|
* ```vue
|
|
81
148
|
* <script setup lang="ts">
|