@onmax/nuxt-better-auth 0.0.2-alpha.15 → 0.0.2-alpha.16
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/module.json +1 -1
- package/dist/module.mjs +164 -4
- package/dist/runtime/adapters/convex.d.ts +111 -0
- package/dist/runtime/adapters/convex.js +213 -0
- package/package.json +13 -2
- package/skills/nuxt-better-auth/SKILL.md +0 -92
- package/skills/nuxt-better-auth/references/client-auth.md +0 -153
- package/skills/nuxt-better-auth/references/client-only.md +0 -87
- package/skills/nuxt-better-auth/references/database.md +0 -115
- package/skills/nuxt-better-auth/references/installation.md +0 -143
- package/skills/nuxt-better-auth/references/plugins.md +0 -139
- package/skills/nuxt-better-auth/references/route-protection.md +0 -105
- package/skills/nuxt-better-auth/references/server-auth.md +0 -135
- package/skills/nuxt-better-auth/references/types.md +0 -144
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -8,9 +8,10 @@ import { join, dirname } from 'pathe';
|
|
|
8
8
|
import { toRouteMatcher, createRouter } from 'radix3';
|
|
9
9
|
import { isCI, isTest } from 'std-env';
|
|
10
10
|
import { generateDrizzleSchema as generateDrizzleSchema$1 } from '@better-auth/cli/api';
|
|
11
|
+
import { getAuthTables } from 'better-auth/db';
|
|
11
12
|
export { defineClientAuth, defineServerAuth } from '../dist/runtime/config.js';
|
|
12
13
|
|
|
13
|
-
const version = "0.0.2-alpha.
|
|
14
|
+
const version = "0.0.2-alpha.16";
|
|
14
15
|
|
|
15
16
|
function setupDevTools(nuxt) {
|
|
16
17
|
nuxt.hook("devtools:customTabs", (tabs) => {
|
|
@@ -56,6 +57,101 @@ async function generateDrizzleSchema(authOptions, dialect, schemaOptions) {
|
|
|
56
57
|
}
|
|
57
58
|
return result.code;
|
|
58
59
|
}
|
|
60
|
+
const convexIndexFields = {
|
|
61
|
+
account: ["accountId", ["accountId", "providerId"], ["providerId", "userId"]],
|
|
62
|
+
rateLimit: ["key"],
|
|
63
|
+
session: ["expiresAt", ["expiresAt", "userId"]],
|
|
64
|
+
verification: ["expiresAt", "identifier"],
|
|
65
|
+
user: [["email", "name"], "name", "userId"],
|
|
66
|
+
passkey: ["credentialID"],
|
|
67
|
+
oauthConsent: [["clientId", "userId"]]
|
|
68
|
+
};
|
|
69
|
+
function getConvexSpecialFields(tables) {
|
|
70
|
+
return Object.fromEntries(
|
|
71
|
+
Object.entries(tables).map(([key, table]) => {
|
|
72
|
+
const fields = Object.fromEntries(
|
|
73
|
+
Object.entries(table.fields).map(([fieldKey, field]) => [
|
|
74
|
+
field.fieldName ?? fieldKey,
|
|
75
|
+
{
|
|
76
|
+
...field.sortable ? { sortable: true } : {},
|
|
77
|
+
...field.unique ? { unique: true } : {},
|
|
78
|
+
...field.references ? { references: field.references } : {}
|
|
79
|
+
}
|
|
80
|
+
]).filter(([_key, value]) => typeof value === "object" ? Object.keys(value).length > 0 : true)
|
|
81
|
+
);
|
|
82
|
+
return [key, fields];
|
|
83
|
+
}).filter(([_key, value]) => typeof value === "object" ? Object.keys(value).length > 0 : true)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
function getMergedConvexIndexFields(tables) {
|
|
87
|
+
return Object.fromEntries(
|
|
88
|
+
Object.entries(tables).map(([key, table]) => {
|
|
89
|
+
const manualIndexes = convexIndexFields[key]?.map((index) => {
|
|
90
|
+
return typeof index === "string" ? table.fields[index]?.fieldName ?? index : index.map((i) => table.fields[i]?.fieldName ?? i);
|
|
91
|
+
}) || [];
|
|
92
|
+
const specialFieldsObj = getConvexSpecialFields(tables);
|
|
93
|
+
const specialFieldIndexes = Object.keys(specialFieldsObj[key] || {}).filter(
|
|
94
|
+
(index) => !manualIndexes.some((m) => Array.isArray(m) ? m[0] === index : m === index)
|
|
95
|
+
);
|
|
96
|
+
return [key, manualIndexes.concat(specialFieldIndexes)];
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
async function generateConvexSchema(authOptions) {
|
|
101
|
+
const tables = getAuthTables(authOptions);
|
|
102
|
+
let code = `/**
|
|
103
|
+
* Auto-generated Better Auth tables for Convex.
|
|
104
|
+
* Import these tables in your convex/schema.ts:
|
|
105
|
+
*
|
|
106
|
+
* import { defineSchema } from 'convex/server'
|
|
107
|
+
* import { authTables } from './_generated/auth-tables'
|
|
108
|
+
*
|
|
109
|
+
* export default defineSchema({ ...authTables, ...yourTables })
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
import { defineTable } from 'convex/server'
|
|
113
|
+
import { v } from 'convex/values'
|
|
114
|
+
|
|
115
|
+
export const authTables = {
|
|
116
|
+
`;
|
|
117
|
+
const getType = (_name, field) => {
|
|
118
|
+
const type = field.type;
|
|
119
|
+
const typeMap = {
|
|
120
|
+
"string": "v.string()",
|
|
121
|
+
"boolean": "v.boolean()",
|
|
122
|
+
"number": "v.number()",
|
|
123
|
+
"date": "v.number()",
|
|
124
|
+
"json": "v.string()",
|
|
125
|
+
"number[]": "v.array(v.number())",
|
|
126
|
+
"string[]": "v.array(v.string())"
|
|
127
|
+
};
|
|
128
|
+
return typeMap[type];
|
|
129
|
+
};
|
|
130
|
+
for (const tableKey in tables) {
|
|
131
|
+
const table = tables[tableKey];
|
|
132
|
+
const modelName = table.modelName;
|
|
133
|
+
const fields = Object.fromEntries(Object.entries(table.fields).filter(([key]) => key !== "id"));
|
|
134
|
+
const indexes = getMergedConvexIndexFields(tables)[tableKey]?.map((index) => {
|
|
135
|
+
const indexArray = Array.isArray(index) ? index.sort() : [index];
|
|
136
|
+
const indexName = indexArray.join("_");
|
|
137
|
+
return `.index('${indexName}', ${JSON.stringify(indexArray)})`;
|
|
138
|
+
}) || [];
|
|
139
|
+
const schema = `${modelName}: defineTable({
|
|
140
|
+
${Object.keys(fields).map((field) => {
|
|
141
|
+
const attr = fields[field];
|
|
142
|
+
const type = getType(field, attr);
|
|
143
|
+
const optional = (fieldSchema) => attr.required ? fieldSchema : `v.optional(v.union(v.null(), ${fieldSchema}))`;
|
|
144
|
+
return ` ${attr.fieldName ?? field}: ${optional(type)},`;
|
|
145
|
+
}).join("\n")}
|
|
146
|
+
})${indexes.length > 0 ? `
|
|
147
|
+
${indexes.join("\n ")}` : ""},
|
|
148
|
+
`;
|
|
149
|
+
code += ` ${schema}`;
|
|
150
|
+
}
|
|
151
|
+
code += `}
|
|
152
|
+
`;
|
|
153
|
+
return code;
|
|
154
|
+
}
|
|
59
155
|
async function loadUserAuthConfig(configPath, throwOnError = false) {
|
|
60
156
|
const { createJiti } = await import('jiti');
|
|
61
157
|
const { defineServerAuth } = await import('../dist/runtime/config.js');
|
|
@@ -105,6 +201,19 @@ function getHubCasing(hub) {
|
|
|
105
201
|
return hub.db.casing;
|
|
106
202
|
}
|
|
107
203
|
const consola = consola$1.withTag("nuxt-better-auth");
|
|
204
|
+
function resolveConvexUrl(nuxt) {
|
|
205
|
+
const convexConfig = nuxt.options.convex;
|
|
206
|
+
if (convexConfig?.url)
|
|
207
|
+
return convexConfig.url;
|
|
208
|
+
const runtimeUrl = nuxt.options.runtimeConfig.public?.convex?.url;
|
|
209
|
+
if (runtimeUrl)
|
|
210
|
+
return runtimeUrl;
|
|
211
|
+
if (process.env.CONVEX_URL)
|
|
212
|
+
return process.env.CONVEX_URL;
|
|
213
|
+
if (process.env.NUXT_PUBLIC_CONVEX_URL)
|
|
214
|
+
return process.env.NUXT_PUBLIC_CONVEX_URL;
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
108
217
|
const generateSecret = () => randomBytes(32).toString("hex");
|
|
109
218
|
function readEnvFile(rootDir) {
|
|
110
219
|
const envPath = join(rootDir, ".env");
|
|
@@ -226,6 +335,12 @@ export default defineClientAuth({})
|
|
|
226
335
|
const hasNuxtHub = hasNuxtModule("@nuxthub/core", nuxt);
|
|
227
336
|
const hub = hasNuxtHub ? nuxt.options.hub : void 0;
|
|
228
337
|
const hasHubDb = !clientOnly && hasNuxtHub && !!hub?.db;
|
|
338
|
+
const hasConvex = hasNuxtModule("nuxt-convex", nuxt);
|
|
339
|
+
const convexUrl = resolveConvexUrl(nuxt);
|
|
340
|
+
const hasConvexDb = !clientOnly && hasConvex && !!convexUrl && !hasHubDb;
|
|
341
|
+
if (hasConvexDb) {
|
|
342
|
+
consola.info("Detected Convex - using Convex HTTP adapter for Better Auth database");
|
|
343
|
+
}
|
|
229
344
|
let secondaryStorageEnabled = options.secondaryStorage ?? false;
|
|
230
345
|
if (secondaryStorageEnabled && clientOnly) {
|
|
231
346
|
consola.warn("secondaryStorage is not available in clientOnly mode. Disabling.");
|
|
@@ -237,7 +352,7 @@ export default defineClientAuth({})
|
|
|
237
352
|
nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {};
|
|
238
353
|
nuxt.options.runtimeConfig.public.auth = defu(nuxt.options.runtimeConfig.public.auth, {
|
|
239
354
|
redirects: { login: options.redirects?.login ?? "/login", guest: options.redirects?.guest ?? "/" },
|
|
240
|
-
useDatabase: hasHubDb,
|
|
355
|
+
useDatabase: hasHubDb || hasConvexDb,
|
|
241
356
|
clientOnly
|
|
242
357
|
});
|
|
243
358
|
if (clientOnly) {
|
|
@@ -285,13 +400,34 @@ export function createSecondaryStorage() {
|
|
|
285
400
|
const hubDialect = getHubDialect(hub) ?? "sqlite";
|
|
286
401
|
const usePlural = options.schema?.usePlural ?? false;
|
|
287
402
|
const camelCase = (options.schema?.casing ?? getHubCasing(hub)) !== "snake_case";
|
|
288
|
-
|
|
403
|
+
let databaseCode;
|
|
404
|
+
if (hasHubDb) {
|
|
405
|
+
databaseCode = `import { db, schema } from '../hub/db.mjs'
|
|
289
406
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
|
290
407
|
const rawDialect = '${hubDialect}'
|
|
291
408
|
const dialect = rawDialect === 'postgresql' ? 'pg' : rawDialect
|
|
292
409
|
export function createDatabase() { return drizzleAdapter(db, { provider: dialect, schema, usePlural: ${usePlural}, camelCase: ${camelCase} }) }
|
|
293
|
-
export { db }
|
|
410
|
+
export { db }`;
|
|
411
|
+
} else if (hasConvexDb) {
|
|
412
|
+
nuxt.options.runtimeConfig.betterAuth = defu(
|
|
413
|
+
nuxt.options.runtimeConfig.betterAuth || {},
|
|
414
|
+
{ convexUrl }
|
|
415
|
+
);
|
|
416
|
+
databaseCode = `import { useRuntimeConfig } from '#imports'
|
|
417
|
+
import { createConvexHttpAdapter } from '@onmax/nuxt-better-auth/adapters/convex'
|
|
418
|
+
import { api } from '#convex/api'
|
|
419
|
+
|
|
420
|
+
export function createDatabase() {
|
|
421
|
+
const config = useRuntimeConfig()
|
|
422
|
+
const convexUrl = config.betterAuth?.convexUrl || config.public?.convex?.url
|
|
423
|
+
if (!convexUrl) throw new Error('[nuxt-better-auth] CONVEX_URL not configured')
|
|
424
|
+
return createConvexHttpAdapter({ url: convexUrl, api: api.auth })
|
|
425
|
+
}
|
|
294
426
|
export const db = undefined`;
|
|
427
|
+
} else {
|
|
428
|
+
databaseCode = `export function createDatabase() { return undefined }
|
|
429
|
+
export const db = undefined`;
|
|
430
|
+
}
|
|
295
431
|
const databaseTemplate = addTemplate({ filename: "better-auth/database.mjs", getContents: () => databaseCode, write: true });
|
|
296
432
|
nuxt.options.alias["#auth/database"] = databaseTemplate.dst;
|
|
297
433
|
addTypeTemplate({
|
|
@@ -411,6 +547,9 @@ declare module '#nuxt-better-auth' {
|
|
|
411
547
|
if (hasHubDb) {
|
|
412
548
|
await setupBetterAuthSchema(nuxt, serverConfigPath, options);
|
|
413
549
|
}
|
|
550
|
+
if (hasConvexDb) {
|
|
551
|
+
await setupConvexAuthSchema(nuxt, serverConfigPath);
|
|
552
|
+
}
|
|
414
553
|
const isProduction = process.env.NODE_ENV === "production" || !nuxt.options.dev;
|
|
415
554
|
if (!isProduction && !clientOnly) {
|
|
416
555
|
if (!hasNuxtModule("@nuxt/ui"))
|
|
@@ -491,5 +630,26 @@ async function setupBetterAuthSchema(nuxt, serverConfigPath, options) {
|
|
|
491
630
|
}
|
|
492
631
|
});
|
|
493
632
|
}
|
|
633
|
+
async function setupConvexAuthSchema(nuxt, serverConfigPath) {
|
|
634
|
+
const isProduction = !nuxt.options.dev;
|
|
635
|
+
try {
|
|
636
|
+
const configFile = `${serverConfigPath}.ts`;
|
|
637
|
+
const userConfig = await loadUserAuthConfig(configFile, isProduction);
|
|
638
|
+
const authOptions = { ...userConfig };
|
|
639
|
+
const schemaCode = await generateConvexSchema(authOptions);
|
|
640
|
+
const schemaDir = join(nuxt.options.buildDir, "better-auth");
|
|
641
|
+
const schemaPath = join(schemaDir, "auth-tables.convex.ts");
|
|
642
|
+
await mkdir(schemaDir, { recursive: true });
|
|
643
|
+
await writeFile(schemaPath, schemaCode);
|
|
644
|
+
addTemplate({ filename: "better-auth/auth-tables.convex.ts", getContents: () => schemaCode, write: true });
|
|
645
|
+
nuxt.options.alias["#auth/convex-schema"] = schemaPath;
|
|
646
|
+
consola.info("Generated Convex auth schema at .nuxt/better-auth/auth-tables.convex.ts");
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (isProduction) {
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
consola.error("Failed to generate Convex schema:", error);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
494
654
|
|
|
495
655
|
export { module$1 as default };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { FunctionReference } from 'convex/server';
|
|
2
|
+
interface ConvexCleanedWhere {
|
|
3
|
+
field: string;
|
|
4
|
+
value: string | number | boolean | string[] | number[] | null;
|
|
5
|
+
operator?: 'lt' | 'lte' | 'gt' | 'gte' | 'eq' | 'in' | 'not_in' | 'ne' | 'contains' | 'starts_with' | 'ends_with';
|
|
6
|
+
connector?: 'AND' | 'OR';
|
|
7
|
+
}
|
|
8
|
+
interface PaginationResult<T> {
|
|
9
|
+
page: T[];
|
|
10
|
+
isDone: boolean;
|
|
11
|
+
continueCursor: string | null;
|
|
12
|
+
splitCursor?: string;
|
|
13
|
+
pageStatus?: 'SplitRecommended' | 'SplitRequired' | string;
|
|
14
|
+
count?: number;
|
|
15
|
+
}
|
|
16
|
+
interface ConvexAuthApi {
|
|
17
|
+
create: FunctionReference<'mutation', 'public', {
|
|
18
|
+
input: {
|
|
19
|
+
model: string;
|
|
20
|
+
data: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
select?: string[];
|
|
23
|
+
}, unknown>;
|
|
24
|
+
findOne: FunctionReference<'query', 'public', {
|
|
25
|
+
model: string;
|
|
26
|
+
where?: ConvexCleanedWhere[];
|
|
27
|
+
select?: string[];
|
|
28
|
+
}, unknown>;
|
|
29
|
+
findMany: FunctionReference<'query', 'public', {
|
|
30
|
+
model: string;
|
|
31
|
+
where?: ConvexCleanedWhere[];
|
|
32
|
+
limit?: number;
|
|
33
|
+
sortBy?: {
|
|
34
|
+
direction: 'asc' | 'desc';
|
|
35
|
+
field: string;
|
|
36
|
+
};
|
|
37
|
+
paginationOpts: {
|
|
38
|
+
numItems: number;
|
|
39
|
+
cursor: string | null;
|
|
40
|
+
};
|
|
41
|
+
}, PaginationResult<unknown>>;
|
|
42
|
+
updateOne: FunctionReference<'mutation', 'public', {
|
|
43
|
+
input: {
|
|
44
|
+
model: string;
|
|
45
|
+
where?: ConvexCleanedWhere[];
|
|
46
|
+
update: Record<string, unknown>;
|
|
47
|
+
};
|
|
48
|
+
}, unknown>;
|
|
49
|
+
updateMany: FunctionReference<'mutation', 'public', {
|
|
50
|
+
input: {
|
|
51
|
+
model: string;
|
|
52
|
+
where?: ConvexCleanedWhere[];
|
|
53
|
+
update: Record<string, unknown>;
|
|
54
|
+
};
|
|
55
|
+
paginationOpts: {
|
|
56
|
+
numItems: number;
|
|
57
|
+
cursor: string | null;
|
|
58
|
+
};
|
|
59
|
+
}, PaginationResult<unknown> & {
|
|
60
|
+
count: number;
|
|
61
|
+
}>;
|
|
62
|
+
deleteOne: FunctionReference<'mutation', 'public', {
|
|
63
|
+
input: {
|
|
64
|
+
model: string;
|
|
65
|
+
where?: ConvexCleanedWhere[];
|
|
66
|
+
};
|
|
67
|
+
}, unknown>;
|
|
68
|
+
deleteMany: FunctionReference<'mutation', 'public', {
|
|
69
|
+
input: {
|
|
70
|
+
model: string;
|
|
71
|
+
where?: ConvexCleanedWhere[];
|
|
72
|
+
};
|
|
73
|
+
paginationOpts: {
|
|
74
|
+
numItems: number;
|
|
75
|
+
cursor: string | null;
|
|
76
|
+
};
|
|
77
|
+
}, PaginationResult<unknown> & {
|
|
78
|
+
count: number;
|
|
79
|
+
}>;
|
|
80
|
+
}
|
|
81
|
+
export interface ConvexHttpAdapterOptions {
|
|
82
|
+
/** Convex deployment URL (e.g., https://your-app.convex.cloud) */
|
|
83
|
+
url: string;
|
|
84
|
+
/** Convex API functions for auth operations - import from your convex/_generated/api */
|
|
85
|
+
api: ConvexAuthApi;
|
|
86
|
+
/** Enable debug logging for adapter operations */
|
|
87
|
+
debugLogs?: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Creates a Better Auth adapter that communicates with Convex via HTTP.
|
|
91
|
+
* Uses ConvexHttpClient for server-side auth operations.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* import { api } from '~/convex/_generated/api'
|
|
96
|
+
*
|
|
97
|
+
* export default defineServerAuth(() => ({
|
|
98
|
+
* database: createConvexHttpAdapter({
|
|
99
|
+
* url: process.env.CONVEX_URL!,
|
|
100
|
+
* api: api.auth,
|
|
101
|
+
* }),
|
|
102
|
+
* }))
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @limitations
|
|
106
|
+
* - `update()` only supports AND-connected where clauses (no OR support)
|
|
107
|
+
* - `count()` fetches all documents client-side (Convex limitation)
|
|
108
|
+
* - `offset` pagination not supported in `findMany()`
|
|
109
|
+
*/
|
|
110
|
+
export declare function createConvexHttpAdapter(options: ConvexHttpAdapterOptions): import("better-auth/adapters").AdapterFactory;
|
|
111
|
+
export {};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createAdapterFactory } from "better-auth/adapters";
|
|
2
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
3
|
+
function parseWhere(where) {
|
|
4
|
+
if (!where)
|
|
5
|
+
return [];
|
|
6
|
+
const whereArray = Array.isArray(where) ? where : [where];
|
|
7
|
+
return whereArray.map((w) => {
|
|
8
|
+
if (w.value instanceof Date)
|
|
9
|
+
return { ...w, value: w.value.getTime() };
|
|
10
|
+
return w;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
async function handlePagination(next, { limit } = {}) {
|
|
14
|
+
const state = { isDone: false, cursor: null, docs: [], count: 0 };
|
|
15
|
+
const onResult = (result) => {
|
|
16
|
+
state.cursor = result.pageStatus === "SplitRecommended" || result.pageStatus === "SplitRequired" ? result.splitCursor ?? result.continueCursor : result.continueCursor;
|
|
17
|
+
if (result.page) {
|
|
18
|
+
state.docs.push(...result.page);
|
|
19
|
+
state.isDone = limit && state.docs.length >= limit || result.isDone;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (result.count) {
|
|
23
|
+
state.count += result.count;
|
|
24
|
+
state.isDone = limit && state.count >= limit || result.isDone;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
state.isDone = result.isDone;
|
|
28
|
+
};
|
|
29
|
+
do {
|
|
30
|
+
const result = await next({
|
|
31
|
+
paginationOpts: {
|
|
32
|
+
numItems: Math.min(200, (limit ?? 200) - state.docs.length, 200),
|
|
33
|
+
cursor: state.cursor
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
onResult(result);
|
|
37
|
+
} while (!state.isDone);
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
export function createConvexHttpAdapter(options) {
|
|
41
|
+
if (!options.url.startsWith("https://") || !options.url.includes(".convex.")) {
|
|
42
|
+
throw new Error(`Invalid Convex URL: ${options.url}. Expected format: https://your-app.convex.cloud`);
|
|
43
|
+
}
|
|
44
|
+
const client = new ConvexHttpClient(options.url);
|
|
45
|
+
return createAdapterFactory({
|
|
46
|
+
config: {
|
|
47
|
+
adapterId: "convex-http",
|
|
48
|
+
adapterName: "Convex HTTP Adapter",
|
|
49
|
+
debugLogs: options.debugLogs ?? false,
|
|
50
|
+
disableIdGeneration: true,
|
|
51
|
+
transaction: false,
|
|
52
|
+
supportsNumericIds: false,
|
|
53
|
+
supportsJSON: false,
|
|
54
|
+
supportsDates: false,
|
|
55
|
+
supportsArrays: true,
|
|
56
|
+
usePlural: false,
|
|
57
|
+
mapKeysTransformInput: { id: "_id" },
|
|
58
|
+
mapKeysTransformOutput: { _id: "id" },
|
|
59
|
+
customTransformInput: ({ data, fieldAttributes }) => {
|
|
60
|
+
if (data && fieldAttributes.type === "date")
|
|
61
|
+
return new Date(data).getTime();
|
|
62
|
+
return data;
|
|
63
|
+
},
|
|
64
|
+
customTransformOutput: ({ data, fieldAttributes }) => {
|
|
65
|
+
if (data && fieldAttributes.type === "date")
|
|
66
|
+
return new Date(data).getTime();
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
adapter: ({ options: authOptions }) => {
|
|
71
|
+
authOptions.telemetry = { enabled: false };
|
|
72
|
+
return {
|
|
73
|
+
id: "convex-http",
|
|
74
|
+
create: async ({ model, data, select }) => {
|
|
75
|
+
return client.mutation(options.api.create, {
|
|
76
|
+
input: { model, data },
|
|
77
|
+
select
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
findOne: async (data) => {
|
|
81
|
+
if (data.where?.every((w) => w.connector === "OR")) {
|
|
82
|
+
for (const w of data.where) {
|
|
83
|
+
const result = await client.query(options.api.findOne, {
|
|
84
|
+
...data,
|
|
85
|
+
model: data.model,
|
|
86
|
+
where: parseWhere(w)
|
|
87
|
+
});
|
|
88
|
+
if (result)
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return client.query(options.api.findOne, {
|
|
94
|
+
...data,
|
|
95
|
+
model: data.model,
|
|
96
|
+
where: parseWhere(data.where)
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
findMany: async (data) => {
|
|
100
|
+
if (data.offset)
|
|
101
|
+
throw new Error("offset not supported");
|
|
102
|
+
if (data.where?.some((w) => w.connector === "OR")) {
|
|
103
|
+
const results = await Promise.all(
|
|
104
|
+
data.where.map(
|
|
105
|
+
async (w) => handlePagination(async ({ paginationOpts }) => {
|
|
106
|
+
return client.query(options.api.findMany, {
|
|
107
|
+
...data,
|
|
108
|
+
model: data.model,
|
|
109
|
+
where: parseWhere(w),
|
|
110
|
+
paginationOpts
|
|
111
|
+
});
|
|
112
|
+
}, { limit: data.limit })
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
const allDocs = results.flatMap((r) => r.docs);
|
|
116
|
+
const uniqueDocs = [...new Map(allDocs.map((d) => [d._id, d])).values()];
|
|
117
|
+
if (data.sortBy) {
|
|
118
|
+
return uniqueDocs.sort((a, b) => {
|
|
119
|
+
const aVal = a[data.sortBy.field];
|
|
120
|
+
const bVal = b[data.sortBy.field];
|
|
121
|
+
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
122
|
+
return data.sortBy.direction === "asc" ? cmp : -cmp;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return uniqueDocs;
|
|
126
|
+
}
|
|
127
|
+
const result = await handlePagination(
|
|
128
|
+
async ({ paginationOpts }) => client.query(options.api.findMany, {
|
|
129
|
+
...data,
|
|
130
|
+
model: data.model,
|
|
131
|
+
where: parseWhere(data.where),
|
|
132
|
+
paginationOpts
|
|
133
|
+
}),
|
|
134
|
+
{ limit: data.limit }
|
|
135
|
+
);
|
|
136
|
+
return result.docs;
|
|
137
|
+
},
|
|
138
|
+
// Note: Convex doesn't have a native count query, so we fetch all docs and count client-side.
|
|
139
|
+
// This is inefficient for large datasets but acceptable for auth tables (typically small).
|
|
140
|
+
count: async (data) => {
|
|
141
|
+
if (data.where?.some((w) => w.connector === "OR")) {
|
|
142
|
+
const results = await Promise.all(
|
|
143
|
+
data.where.map(
|
|
144
|
+
async (w) => handlePagination(async ({ paginationOpts }) => {
|
|
145
|
+
return client.query(options.api.findMany, {
|
|
146
|
+
...data,
|
|
147
|
+
model: data.model,
|
|
148
|
+
where: parseWhere(w),
|
|
149
|
+
paginationOpts
|
|
150
|
+
});
|
|
151
|
+
})
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
const allDocs = results.flatMap((r) => r.docs);
|
|
155
|
+
const uniqueDocs = [...new Map(allDocs.map((d) => [d._id, d])).values()];
|
|
156
|
+
return uniqueDocs.length;
|
|
157
|
+
}
|
|
158
|
+
const result = await handlePagination(async ({ paginationOpts }) => client.query(options.api.findMany, {
|
|
159
|
+
...data,
|
|
160
|
+
model: data.model,
|
|
161
|
+
where: parseWhere(data.where),
|
|
162
|
+
paginationOpts
|
|
163
|
+
}));
|
|
164
|
+
return result.docs.length;
|
|
165
|
+
},
|
|
166
|
+
// Supports single eq or multiple AND-connected conditions (Better Auth's common patterns)
|
|
167
|
+
update: async (data) => {
|
|
168
|
+
const hasOrConnector = data.where?.some((w) => w.connector === "OR");
|
|
169
|
+
if (hasOrConnector) {
|
|
170
|
+
throw new Error("update() does not support OR conditions - use updateMany() or split into multiple calls");
|
|
171
|
+
}
|
|
172
|
+
return client.mutation(options.api.updateOne, {
|
|
173
|
+
input: {
|
|
174
|
+
model: data.model,
|
|
175
|
+
where: parseWhere(data.where),
|
|
176
|
+
update: data.update
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
updateMany: async (data) => {
|
|
181
|
+
const result = await handlePagination(async ({ paginationOpts }) => client.mutation(options.api.updateMany, {
|
|
182
|
+
input: {
|
|
183
|
+
...data,
|
|
184
|
+
model: data.model,
|
|
185
|
+
where: parseWhere(data.where)
|
|
186
|
+
},
|
|
187
|
+
paginationOpts
|
|
188
|
+
}));
|
|
189
|
+
return result.count;
|
|
190
|
+
},
|
|
191
|
+
delete: async (data) => {
|
|
192
|
+
await client.mutation(options.api.deleteOne, {
|
|
193
|
+
input: {
|
|
194
|
+
model: data.model,
|
|
195
|
+
where: parseWhere(data.where)
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
deleteMany: async (data) => {
|
|
200
|
+
const result = await handlePagination(async ({ paginationOpts }) => client.mutation(options.api.deleteMany, {
|
|
201
|
+
input: {
|
|
202
|
+
...data,
|
|
203
|
+
model: data.model,
|
|
204
|
+
where: parseWhere(data.where)
|
|
205
|
+
},
|
|
206
|
+
paginationOpts
|
|
207
|
+
}));
|
|
208
|
+
return result.count;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onmax/nuxt-better-auth",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.2-alpha.
|
|
4
|
+
"version": "0.0.2-alpha.16",
|
|
5
5
|
"packageManager": "pnpm@10.15.1",
|
|
6
6
|
"description": "Nuxt module for Better Auth integration with NuxtHub, route protection, session management, and role-based access",
|
|
7
7
|
"author": "onmax",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"./config": {
|
|
27
27
|
"types": "./dist/runtime/config.d.ts",
|
|
28
28
|
"import": "./dist/runtime/config.js"
|
|
29
|
+
},
|
|
30
|
+
"./adapters/convex": {
|
|
31
|
+
"types": "./dist/runtime/adapters/convex.d.ts",
|
|
32
|
+
"import": "./dist/runtime/adapters/convex.js"
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"main": "./dist/module.mjs",
|
|
@@ -36,6 +40,9 @@
|
|
|
36
40
|
],
|
|
37
41
|
"config": [
|
|
38
42
|
"./dist/runtime/config.d.ts"
|
|
43
|
+
],
|
|
44
|
+
"adapters/convex": [
|
|
45
|
+
"./dist/runtime/adapters/convex.d.ts"
|
|
39
46
|
]
|
|
40
47
|
}
|
|
41
48
|
},
|
|
@@ -60,11 +67,15 @@
|
|
|
60
67
|
},
|
|
61
68
|
"peerDependencies": {
|
|
62
69
|
"@nuxthub/core": ">=0.10.0",
|
|
63
|
-
"better-auth": ">=1.0.0"
|
|
70
|
+
"better-auth": ">=1.0.0",
|
|
71
|
+
"convex": ">=1.25.0"
|
|
64
72
|
},
|
|
65
73
|
"peerDependenciesMeta": {
|
|
66
74
|
"@nuxthub/core": {
|
|
67
75
|
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"convex": {
|
|
78
|
+
"optional": true
|
|
68
79
|
}
|
|
69
80
|
},
|
|
70
81
|
"dependencies": {
|