@better-auth-kit/convex
Version:
Convex Database Adapter for Better-Auth.
366 lines (352 loc) • 8.5 kB
text/typescript
import type {
ActionBuilder,
MutationBuilder,
PaginationOptions,
QueryBuilder,
RegisteredAction,
RegisteredMutation,
RegisteredQuery,
} from "convex/server";
import { v } from "convex/values";
import { stringToQuery } from "./helpers";
function replaceFields(key: string) {
switch (key) {
case "id":
return "_id";
case "createdAt":
return "_creationTime";
default:
return key;
}
}
const q_ = {
eq: (key: string, value: any) =>
`q.eq(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
add: (key: string, value: any) =>
`q.add(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
sub: (key: string, value: any) =>
`q.sub(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
mul: (key: string, value: any) =>
`q.mul(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
div: (key: string, value: any) =>
`q.div(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
mod: (key: string, value: any) =>
`q.mod(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
neg: (key: string, value: any) =>
`q.neg(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
gt: (key: string, value: any) =>
`q.gt(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
lt: (key: string, value: any) =>
`q.lt(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
gte: (key: string, value: any) =>
`q.gte(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
lte: (key: string, value: any) =>
`q.lte(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
in: (key: string, value: any) =>
`q.in(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
ne: (key: string, value: any) =>
`q.neq(q.field("${replaceFields(key)}"), ${JSON.stringify(value)})`,
and: (...args: any[]) => `q.and(${args.join(", ")})`,
or: (...args: any[]) => `q.or(${args.join(", ")})`,
};
export function queryBuilder(cb: (q: typeof q_) => string) {
return cb(q_);
}
export type ConvexReturnType = {
betterAuth: RegisteredAction<
"public",
{
action: string;
value: any;
},
Promise<any>
>;
query: RegisteredQuery<
"internal",
{
tableName: string;
query?: string;
order?: "asc" | "desc";
single?: boolean;
limit?: number;
offset?: number;
},
Promise<any>
>;
insert: RegisteredMutation<
"internal",
{
tableName: string;
values: Record<string, any>;
},
Promise<any>
>;
update: RegisteredMutation<
"internal",
{
tableName: string;
query: any;
update: any;
},
void
>;
count: RegisteredQuery<
"internal",
{
query?: any;
tableName: string;
},
Promise<number>
>;
delete_: RegisteredMutation<
"internal",
{
tableName: string;
query: any;
deleteAll?: boolean;
},
Promise<void>
>;
getSession: RegisteredQuery<
"internal",
{
sessionToken: string;
},
Promise<any>
>;
};
export function ConvexHandler<
Action extends ActionBuilder<any, "public"> = ActionBuilder<{}, "public">,
Query extends QueryBuilder<any, "internal"> = QueryBuilder<{}, "internal">,
Mutation extends MutationBuilder<any, "internal"> = MutationBuilder<
{},
"internal"
>,
>(
{
action,
internalQuery,
internalMutation,
internal,
}: {
action: Action;
internalQuery: Query;
internalMutation: Mutation;
internal: {
betterAuth: {
query: any;
insert: any;
update: any;
delete_: any;
count: any;
getSession: any;
};
} & Record<string, any>;
},
options?: {
/**
* If you have a custom session model, you can pass the name here.
*
* This is only useful for the `getSession` query.
*
* @default "session"
*/
sessionModelName?: string;
},
): ConvexReturnType {
const betterAuth = action({
args: { action: v.string(), value: v.any() },
handler: async (ctx, args) => {
if (args.action === "query") {
// console.log(`Query:`, args.value);
const data = (await ctx.runQuery(internal.betterAuth.query, {
query: args.value.query,
tableName: args.value.tableName,
limit: args.value.limit,
offset: args.value.offset,
order: args.value.order,
single: args.value.single,
paginationOpts: args.value.paginationOpts,
})) as unknown as any;
return data;
}
if (args.action === "insert") {
try {
const _id = await ctx.runMutation(internal.betterAuth.insert, {
tableName: args.value.tableName,
values: args.value.values,
});
return {
_id: _id,
...args.value.values,
};
} catch (error) {
return error;
}
}
if (args.action === "update") {
const res = await ctx.runMutation(internal.betterAuth.update, {
tableName: args.value.tableName,
query: args.value.query,
update: args.value.update,
});
return res;
}
if (args.action === "delete") {
const res = await ctx.runMutation(internal.betterAuth.delete_, {
tableName: args.value.tableName,
query: args.value.query,
deleteAll: args.value.deleteAll,
});
return res;
}
if (args.action === "count") {
const res = await ctx.runMutation(internal.betterAuth.count, {
tableName: args.value.tableName,
query: args.value.query,
});
return res;
}
},
});
const query = internalQuery({
args: {
tableName: v.string(),
query: v.optional(v.any()),
/**
* asc or desc
*/
order: v.optional(v.string()),
/**
* Only get the first.
*/
single: v.optional(v.boolean()),
limit: v.optional(v.number()),
paginationOpts: v.optional(
v.object({
numItems: v.optional(v.number()),
cursor: v.optional(v.string()),
}),
),
},
//@ts-ignore
handler: async (
ctx,
args: {
tableName: string;
query?: string;
order?: "asc" | "desc";
single?: boolean;
limit?: number;
paginationOpts?: { numItems: number; cursor?: string };
},
) => {
let query = ctx.db.query(args.tableName).order(args.order || "asc");
if (args.query) {
query = query.filter((q) => {
return stringToQuery(args.query!, q);
});
}
if (args.paginationOpts) {
return await query.paginate(args.paginationOpts as PaginationOptions);
}
if (args.single === true) {
return await query.first();
}
if (typeof args.limit === "number") {
return await query.take(args.limit);
}
return await query.collect();
},
});
const insert = internalMutation({
args: {
tableName: v.string(),
values: v.any(),
},
handler: async (
ctx,
args: {
tableName: string;
values: Record<string, any>;
},
) => {
return await ctx.db.insert(args.tableName, args.values);
},
});
const update = internalMutation({
args: {
tableName: v.string(),
query: v.any(),
update: v.any(),
},
async handler(ctx, { tableName, update, query }) {
const res = await ctx.db
.query(tableName)
.filter((q) => {
return stringToQuery(query, q);
})
.first();
//If no result found for that query, than there is no mutation needed.
if (!res) return;
await ctx.db.patch(res._id, update);
return Object.assign(res, update);
},
});
const delete_ = internalMutation({
args: {
tableName: v.string(),
query: v.any(),
deleteAll: v.optional(v.boolean()),
},
async handler(ctx, { tableName, query, deleteAll }) {
const r = ctx.db.query(tableName).filter((q) => {
return stringToQuery(query, q);
});
if (!deleteAll) {
const res = await r.first();
//If no result found for that query, than there is no mutation needed.
if (!res) return;
await ctx.db.delete(res._id);
return;
}
const res = await r.collect();
if (!res) return;
res.forEach((r) => {
ctx.db.delete(r._id);
});
return;
},
});
const count = internalQuery({
args: {
tableName: v.string(),
query: v.optional(v.any()),
},
handler: async (ctx, args) => {
const res = await ctx.db
.query(args.tableName)
.filter((q) => {
return stringToQuery(args.query, q);
})
.collect();
return res.length;
},
});
const getSession = internalQuery({
args: {
sessionToken: v.string(),
},
handler: async (ctx, args) => {
return (
ctx.db
//@ts-ignore
.query(options?.sessionModelName || "session")
//@ts-ignore
.filter((q) => q.eq(q.field("token"), args.sessionToken))
.first()
);
},
});
return { betterAuth, query, insert, update, delete_, count, getSession };
}