UNPKG

kitcn

Version:

kitcn - React Query integration and CLI tools for Convex

1,182 lines (1,179 loc) 60.1 kB
import { n as asyncMap } from "../upstream-BR6sBLg3.js"; import { r as partial } from "../validators-C7LelqTN.js"; import { i as defineAuth, n as createDisabledAuthRuntime, r as getGeneratedAuthDisabledReason, t as DEFAULT_AUTH_DEFINITION_PATH } from "../generated-contract-disabled-BXaz7JCE.js"; import { n as createGeneratedFunctionReference, o as isQueryCtx, s as isRunMutationCtx } from "../api-entry-N3nBOlI2.js"; import { n as customCtx, r as customMutation } from "../customFunctions-DxEEO4Dq.js"; import { a as mergedStream, l as unsetToken, o as stream, t as getByIdWithOrmQueryFallback, v as eq } from "../query-context-ydn9kb6P.js"; import { n as convex } from "../convex-plugin-BHVCqWTH.js"; import { v } from "convex/values"; import { internalActionGeneric, internalMutationGeneric, internalQueryGeneric, paginationOptsValidator } from "convex/server"; import { createAdapterFactory } from "better-auth/adapters"; import { getAuthTables } from "better-auth/db"; import { prop, sortBy } from "remeda"; import { stripIndent } from "common-tags"; import { betterAuth } from "better-auth/minimal"; //#region src/auth/adapter-utils.ts const adapterWhereValidator = v.object({ connector: v.optional(v.union(v.literal("AND"), v.literal("OR"))), field: v.string(), mode: v.optional(v.union(v.literal("sensitive"), v.literal("insensitive"))), operator: v.optional(v.union(v.literal("lt"), v.literal("lte"), v.literal("gt"), v.literal("gte"), v.literal("eq"), v.literal("in"), v.literal("not_in"), v.literal("ne"), v.literal("contains"), v.literal("starts_with"), v.literal("ends_with"))), value: v.union(v.string(), v.number(), v.boolean(), v.array(v.string()), v.array(v.number()), v.null()) }); const adapterArgsValidator = v.object({ limit: v.optional(v.number()), model: v.string(), offset: v.optional(v.number()), select: v.optional(v.array(v.string())), sortBy: v.optional(v.object({ direction: v.union(v.literal("asc"), v.literal("desc")), field: v.string() })), where: v.optional(v.array(adapterWhereValidator)) }); const isUniqueField = (betterAuthSchema, model, field) => { const modelSchema = betterAuthSchema[Object.keys(betterAuthSchema).find((key) => betterAuthSchema[key].modelName === model) || model]; if (!modelSchema?.fields) return false; return Object.entries(modelSchema.fields).filter(([, value]) => value.unique).map(([key]) => key).includes(field); }; const hasUniqueFields = (betterAuthSchema, model, input) => { for (const field of Object.keys(input)) if (isUniqueField(betterAuthSchema, model, field)) return true; return false; }; const findIndex = (schema, args) => { if ((args.where?.length ?? 0) > 1 && args.where?.some((w) => w.connector === "OR")) throw new Error(`OR connector not supported with multiple where statements in findIndex, split up the where statements before calling findIndex: ${JSON.stringify(args.where)}`); const where = args.where?.filter((w) => w.mode !== "insensitive" && (!w.operator || [ "eq", "gt", "gte", "in", "lt", "lte", "not_in" ].includes(w.operator)) && w.field !== "_id"); if (!where?.length && !args.sortBy) return; const lowerBounds = where?.filter((w) => w.operator === "lt" || w.operator === "lte") ?? []; if (lowerBounds.length > 1) throw new Error(`cannot have more than one lower bound where clause: ${JSON.stringify(where)}`); const upperBounds = where?.filter((w) => w.operator === "gt" || w.operator === "gte") ?? []; if (upperBounds.length > 1) throw new Error(`cannot have more than one upper bound where clause: ${JSON.stringify(where)}`); const lowerBound = lowerBounds[0]; const upperBound = upperBounds[0]; if (lowerBound && upperBound && lowerBound.field !== upperBound.field) throw new Error(`lower bound and upper bound must have the same field: ${JSON.stringify(where)}`); const boundField = lowerBound?.field || upperBound?.field; if (boundField && where?.some((w) => w.field === boundField && w !== lowerBound && w !== upperBound)) throw new Error(`too many where clauses on the bound field: ${JSON.stringify(where)}`); const indexEqFields = where?.filter((w) => !w.operator || w.operator === "eq").sort((a, b) => a.field.localeCompare(b.field)).map((w) => [w.field, w.value]) ?? []; if (!indexEqFields?.length && !boundField && !args.sortBy) return; const table = schema.tables[args.model]; if (!table) throw new Error(`Table ${args.model} not found`); const indexes = table[" indexes"] ? table[" indexes"]() : table.export().indexes; const sortField = args.sortBy?.field; const indexFields = indexEqFields.map(([field]) => field).concat(boundField && boundField !== "createdAt" ? boundField : "").concat(sortField && sortField !== "createdAt" && boundField !== sortField ? sortField : "").filter(Boolean); if (indexFields.length === 0 && !boundField && !sortField) return; const index = indexFields.length === 0 ? { fields: [], indexDescriptor: "by_creation_time" } : indexes.find(({ fields }) => { const fieldsMatch = indexFields.every((field, idx) => field === fields[idx]); const boundFieldMatch = boundField === "createdAt" || sortField === "createdAt" ? indexFields.length === fields.length : true; return fieldsMatch && boundFieldMatch; }); if (!index) return { indexFields }; return { boundField, index: { fields: [...index.fields, "_creationTime"], indexDescriptor: index.indexDescriptor }, sortField, values: { eq: indexEqFields.map(([, value]) => value), gt: upperBound?.operator === "gt" ? upperBound.value : void 0, gte: upperBound?.operator === "gte" ? upperBound.value : void 0, lt: lowerBound?.operator === "lt" ? lowerBound.value : void 0, lte: lowerBound?.operator === "lte" ? lowerBound.value : void 0 } }; }; const checkUniqueFields = async (ctx, schema, betterAuthSchema, table, input, doc) => { if (!hasUniqueFields(betterAuthSchema, table, input)) return; for (const field of Object.keys(input)) { if (!isUniqueField(betterAuthSchema, table, field)) continue; const { index } = findIndex(schema, { model: table, where: [{ field, operator: "eq", value: input[field] }] }) || {}; if (!index) throw new Error(`No index found for ${table}${field}`); const existingDoc = await ctx.db.query(table).withIndex(index.indexDescriptor, (q) => q.eq(field, input[field])).unique(); if (existingDoc && existingDoc._id !== doc?._id) throw new Error(`${table} ${field} already exists`); } }; const selectFields = (doc, select) => { if (!doc) return null; if (!select?.length) return doc; return select.reduce((acc, field) => { const sourceField = field === "id" && "_id" in doc ? "_id" : field; acc[sourceField] = doc[sourceField]; return acc; }, {}); }; const filterByWhere = (doc, where, filterWhere) => { if (!doc) return false; for (const w of where ?? []) { if (filterWhere && !filterWhere(w)) continue; const value = doc[w.field]; const normalizeString = (input) => w.mode === "insensitive" ? input.toLowerCase() : input; const normalizeComparable = (input) => typeof input === "string" ? normalizeString(input) : input; const isLessThan = (val, wVal) => { if (wVal === void 0 || wVal === null) return false; if (val === void 0 || val === null) return true; return normalizeComparable(val) < normalizeComparable(wVal); }; const isGreaterThan = (val, wVal) => { if (val === void 0 || val === null) return false; if (wVal === void 0 || wVal === null) return true; return normalizeComparable(val) > normalizeComparable(wVal); }; const filter = (w) => { const comparableValue = normalizeComparable(value); const comparableWhereValue = normalizeComparable(w.value); switch (w.operator) { case "contains": return typeof comparableValue === "string" && typeof comparableWhereValue === "string" && comparableValue.includes(comparableWhereValue); case "ends_with": return typeof comparableValue === "string" && typeof comparableWhereValue === "string" && comparableValue.endsWith(comparableWhereValue); case "eq": case void 0: return comparableValue === comparableWhereValue; case "gt": return isGreaterThan(value, w.value); case "gte": return comparableValue === comparableWhereValue || isGreaterThan(value, w.value); case "in": return Array.isArray(w.value) && w.value.some((candidate) => normalizeComparable(candidate) === comparableValue); case "lt": return isLessThan(value, w.value); case "lte": return comparableValue === comparableWhereValue || isLessThan(value, w.value); case "ne": return comparableValue !== comparableWhereValue; case "not_in": return Array.isArray(w.value) && !w.value.some((candidate) => normalizeComparable(candidate) === comparableValue); case "starts_with": return typeof comparableValue === "string" && typeof comparableWhereValue === "string" && comparableValue.startsWith(comparableWhereValue); } }; if (!filter(w)) return false; } return true; }; const generateQuery = (ctx, schema, args) => { const { boundField, index, indexFields, values } = findIndex(schema, args) ?? {}; const usableIndex = index?.indexDescriptor === "by_creation_time" ? void 0 : index; const query = stream(ctx.db, schema).query(args.model); const hasValues = (values?.eq?.length ?? 0) > 0 || values?.lt !== void 0 || values?.lte !== void 0 || values?.gt !== void 0 || values?.gte !== void 0; const indexedQuery = usableIndex ? query.withIndex(usableIndex.indexDescriptor, hasValues ? (q) => { let query = q; for (const [idx, value] of (values?.eq ?? []).entries()) query = query.eq(usableIndex.fields[idx], value); if (values?.lt !== void 0) query = query.lt(boundField, values.lt); if (values?.lte !== void 0) query = query.lte(boundField, values.lte); if (values?.gt !== void 0) query = query.gt(boundField, values.gt); if (values?.gte !== void 0) query = query.gte(boundField, values.gte); return query; } : void 0) : query; const orderedQuery = args.sortBy ? indexedQuery.order(args.sortBy.direction === "desc" ? "desc" : "asc") : indexedQuery; if (!usableIndex && indexFields?.length) console.warn(stripIndent` Querying without an index on table "${args.model}". This can cause performance issues, and may hit the document read limit. To fix, add an index that begins with the following fields in order: [${indexFields.join(", ")}] `); return orderedQuery.filterWith(async (doc) => { if (!usableIndex) return filterByWhere(doc, args.where); return filterByWhere(doc, args.where, (w) => w.mode === "insensitive" || w.operator && [ "contains", "ends_with", "ne", "not_in", "starts_with" ].includes(w.operator)); }); }; const paginate = async (ctx, schema, betterAuthSchema, args) => { if (args.offset) throw new Error(`offset not supported: ${JSON.stringify(args.offset)}`); if (args.where?.some((w) => w.connector === "OR") && args.where?.length > 1) throw new Error(`OR connector not supported with multiple where statements in paginate, split up the where statements before calling paginate: ${JSON.stringify(args.where)}`); if (args.where?.some((w) => w.field === "_id" && w.operator && ![ "eq", "in", "ne", "not_in" ].includes(w.operator))) throw new Error(`id can only be used with eq, in, not_in, or ne operator: ${JSON.stringify(args.where)}`); const uniqueWhere = args.where?.find((w) => w.mode !== "insensitive" && (!w.operator || w.operator === "eq") && (isUniqueField(betterAuthSchema, args.model, w.field) || w.field === "_id")); if (uniqueWhere) { const { index } = findIndex(schema, { model: args.model, where: [uniqueWhere] }) || {}; const doc = uniqueWhere.field === "_id" ? await ctx.db.get(uniqueWhere.value) : await ctx.db.query(args.model).withIndex(index?.indexDescriptor, (q) => q.eq(index?.fields[0], uniqueWhere.value)).unique(); if (filterByWhere(doc, args.where, (w) => w !== uniqueWhere)) return { continueCursor: "", isDone: true, page: [selectFields(doc, args.select)].filter(Boolean) }; return { continueCursor: "", isDone: true, page: [] }; } const paginationLimit = args.paginationOpts.numItems ?? args.limit ?? 200; const paginationMaxScan = Math.max(args.paginationOpts.maximumRowsRead ?? 0, paginationLimit + 1, 200); const paginationOpts = { cursor: args.paginationOpts.cursor, endCursor: args.paginationOpts.endCursor, limit: paginationLimit, maxScan: paginationMaxScan }; const inWhere = args.where?.find((w) => w.operator === "in"); if (inWhere) { if (!Array.isArray(inWhere.value)) throw new TypeError("in clause value must be an array"); if (inWhere.field === "_id") return { continueCursor: "", isDone: true, page: (await asyncMap(inWhere.value, async (value) => ctx.db.get(value))).flatMap((doc) => doc ? [doc] : []).filter((doc) => filterByWhere(doc, args.where, (w) => w !== inWhere)).sort((a, b) => { if (args.sortBy?.field === "createdAt") return args.sortBy.direction === "asc" ? a._creationTime - b._creationTime : b._creationTime - a._creationTime; if (args.sortBy) { const aValue = a[args.sortBy.field]; const bValue = b[args.sortBy.field]; if (aValue === bValue) return 0; return args.sortBy.direction === "asc" ? aValue > bValue ? 1 : -1 : aValue > bValue ? -1 : 1; } return 0; }).map((doc) => selectFields(doc, args.select)).flatMap((doc) => doc ? [doc] : []) }; const result = await mergedStream(inWhere.value.map((value) => generateQuery(ctx, schema, { ...args, where: args.where?.map((w) => { if (w === inWhere) return { ...w, operator: "eq", value }; return w; }) })), [args.sortBy?.field !== "createdAt" && args.sortBy?.field, "_creationTime"].flatMap((f) => f ? [f] : [])).paginate(paginationOpts); return { ...result, page: result.page.map((doc) => selectFields(doc, args.select)) }; } const notInWhere = args.where?.find((w) => w.operator === "not_in"); if (notInWhere) { if (!Array.isArray(notInWhere.value)) throw new TypeError("not_in clause value must be an array"); const result = await generateQuery(ctx, schema, { ...args, where: args.where?.filter((w) => w !== notInWhere) }).paginate(paginationOpts); const filteredPage = result.page.filter((doc) => filterByWhere(doc, [notInWhere])); return { ...result, page: filteredPage.map((doc) => selectFields(doc, args.select)) }; } const result = await generateQuery(ctx, schema, args).paginate(paginationOpts); return { ...result, page: result.page.map((doc) => selectFields(doc, args.select)) }; }; const listOne = async (ctx, schema, betterAuthSchema, args) => (await paginate(ctx, schema, betterAuthSchema, { ...args, paginationOpts: { cursor: null, numItems: 1 } })).page[0]; //#endregion //#region src/auth/create-api.ts const AUTH_TABLE_TRIGGER_KEYS = new Set([ "create", "update", "delete", "change" ]); const LEGACY_AUTH_TRIGGER_KEYS = new Set([ "beforeCreate", "beforeDelete", "beforeUpdate", "onCreate", "onDelete", "onUpdate" ]); const whereValidator = (schema, tableName) => v.object({ connector: v.optional(v.union(v.literal("AND"), v.literal("OR"))), field: v.union(...Object.keys(schema.tables[tableName].validator.fields).map((field) => v.literal(field)), v.literal("_id")), operator: v.optional(v.union(v.literal("lt"), v.literal("lte"), v.literal("gt"), v.literal("gte"), v.literal("eq"), v.literal("in"), v.literal("not_in"), v.literal("ne"), v.literal("contains"), v.literal("starts_with"), v.literal("ends_with"))), value: v.union(v.string(), v.number(), v.boolean(), v.array(v.string()), v.array(v.number()), v.null()) }); const resolveSchemaTableName = (schema, betterAuthSchema, model) => { if (schema.tables[model]) return model; const modelConfig = betterAuthSchema?.[model]; if (modelConfig?.modelName && schema.tables[modelConfig.modelName]) return modelConfig.modelName; for (const [key, value] of Object.entries(betterAuthSchema ?? {})) { if (value?.modelName !== model) continue; if (schema.tables[key]) return key; if (schema.tables[value.modelName]) return value.modelName; } }; const AUTH_TIMESTAMP_FIELDS = ["createdAt", "updatedAt"]; const resolveBetterAuthModel = (betterAuthSchema, model) => { if (betterAuthSchema?.[model]) return betterAuthSchema[model]; return Object.values(betterAuthSchema ?? {}).find((value) => value?.modelName === model); }; const resolveWriteFields = (schema, betterAuthSchema, model) => { const tableName = resolveSchemaTableName(schema, betterAuthSchema, model); const validatorFields = tableName ? schema.tables[tableName]?.validator?.fields : void 0; if (validatorFields) return new Set(Object.keys(validatorFields)); const modelFields = resolveBetterAuthModel(betterAuthSchema, model)?.fields; if (!modelFields) return; const fields = /* @__PURE__ */ new Set(); for (const [field, config] of Object.entries(modelFields)) { fields.add(field); if (typeof config?.fieldName === "string") fields.add(config.fieldName); } return fields; }; const stripUnsupportedAuthTimestamps = (data, schema, betterAuthSchema, model) => { const writeFields = resolveWriteFields(schema, betterAuthSchema, model); if (!writeFields) return data; let result; for (const field of AUTH_TIMESTAMP_FIELDS) if (field in data && !writeFields.has(field)) { result ??= { ...data }; delete result[field]; } return result ?? data; }; const resolveOrmTable = (ctx, schema, betterAuthSchema, model) => { if (!ctx?.orm || typeof ctx.orm.insert !== "function" || typeof ctx.orm.update !== "function" || typeof ctx.orm.delete !== "function") return; const tableName = resolveSchemaTableName(schema, betterAuthSchema, model); if (!tableName) return; const table = schema.tables[tableName]; if (!table?._id) return; return { table, tableName }; }; const normalizeUpdateForOrm = (update) => Object.fromEntries(Object.entries(update).map(([key, value]) => [key, value === void 0 ? unsetToken : value])); const ormInsert = async (ctx, table, data) => (await ctx.orm.insert(table).values(data).returning())[0]; const ormUpdate = async (ctx, table, id, update) => (await ctx.orm.update(table).set(normalizeUpdateForOrm(update)).returning().where(eq(table._id, id)))[0]; const ormDelete = async (ctx, table, id) => { await ctx.orm.delete(table).where(eq(table._id, id)); }; const withBothIdFields = (doc) => { if (!doc || typeof doc !== "object" || Array.isArray(doc)) return doc; const record = doc; const existingUnderscoreId = record._id; const existingId = record.id; const id = existingUnderscoreId ?? existingId; if (!id) return doc; return { ...record, _id: existingUnderscoreId ?? id, id: existingId ?? id }; }; const isPlainObject = (value) => !!value && typeof value === "object" && !Array.isArray(value); const isBeforeDataResult = (value) => isPlainObject(value) && "data" in value && isPlainObject(value.data); const ensureRuntimeTableTriggers = (model, value) => { if (value === void 0) return; if (!isPlainObject(value)) throw new Error(`Invalid auth triggers for '${model}'. Expected an object with create/update/delete/change keys.`); for (const key of Object.keys(value)) { if (LEGACY_AUTH_TRIGGER_KEYS.has(key)) throw new Error(`Invalid auth trigger key '${key}' for '${model}'. Auth triggers now use { create, update, delete, change } shape.`); if (!AUTH_TABLE_TRIGGER_KEYS.has(key)) throw new Error(`Invalid auth trigger key '${key}' for '${model}'. Allowed keys: create, update, delete, change.`); } const create = value.create; const update = value.update; const del = value.delete; const change = value.change; const validateOperationHook = (operation, operationHooks) => { if (operationHooks === void 0) return; if (!isPlainObject(operationHooks)) throw new Error(`Invalid auth trigger '${operation}' for '${model}'. Expected an object with optional before/after handlers.`); if (operationHooks.before !== void 0 && typeof operationHooks.before !== "function") throw new Error(`Invalid auth trigger '${operation}.before' for '${model}'. Expected a function.`); if (operationHooks.after !== void 0 && typeof operationHooks.after !== "function") throw new Error(`Invalid auth trigger '${operation}.after' for '${model}'. Expected a function.`); }; validateOperationHook("create", create); validateOperationHook("update", update); validateOperationHook("delete", del); if (change !== void 0 && typeof change !== "function") throw new Error(`Invalid auth trigger 'change' for '${model}'. Expected a function.`); return { ...create ? { create } : {}, ...update ? { update } : {}, ...del ? { delete: del } : {}, ...change ? { change } : {} }; }; const applyBeforeHook = async (model, operation, data, beforeHook, triggerCtx) => { if (!beforeHook) return data; const result = await beforeHook(data, triggerCtx); if (result === false) throw new Error(`Auth trigger cancelled ${operation} on '${model}'.`); if (isBeforeDataResult(result)) return { ...data, ...result.data }; return data; }; const getDocId$1 = (doc) => doc._id ?? doc.id; const serializeDatesForConvex = (value) => { if (value instanceof Date) return value.getTime(); if (Array.isArray(value)) { let result; for (let index = 0; index < value.length; index += 1) { const entry = value[index]; const serialized = serializeDatesForConvex(entry); if (serialized !== entry) { if (!result) result = value.slice(); result[index] = serialized; } } return result ?? value; } if (!isPlainObject(value)) return value; let serialized; for (const key in value) { if (!Object.hasOwn(value, key)) continue; const nested = value[key]; const encoded = serializeDatesForConvex(nested); if (encoded !== nested) { if (!serialized) serialized = { ...value }; serialized[key] = encoded; } } return serialized ?? value; }; const toConvexSafe = (value) => serializeDatesForConvex(value); const withAuthTimestamps = (data, schema, betterAuthSchema, model) => { const writeFields = resolveWriteFields(schema, betterAuthSchema, model); if (data.createdAt !== void 0) return stripUnsupportedAuthTimestamps(data, schema, betterAuthSchema, model); const supportsCreatedAt = !writeFields || writeFields.has("createdAt"); const supportsUpdatedAt = !writeFields || writeFields.has("updatedAt"); if (!(supportsCreatedAt || supportsUpdatedAt)) return data; const now = Date.now(); return stripUnsupportedAuthTimestamps({ ...data, ...supportsCreatedAt ? { createdAt: now } : {}, ...supportsUpdatedAt && data.updatedAt === void 0 ? { updatedAt: now } : {} }, schema, betterAuthSchema, model); }; const createHandler = async (ctx, args, schema, betterAuthSchema) => { const triggerCtx = args.triggerCtx ?? ctx; const tableTriggers = args.tableTriggers; const data = serializeDatesForConvex(withAuthTimestamps(await applyBeforeHook(args.input.model, "create", args.input.data, tableTriggers?.create?.before, triggerCtx), schema, betterAuthSchema, args.input.model)); await checkUniqueFields(ctx, schema, betterAuthSchema, args.input.model, data); const ormTable = resolveOrmTable(ctx, schema, betterAuthSchema, args.input.model); const doc = ormTable ? await ormInsert(ctx, ormTable.table, data) : await (async () => { const id = await ctx.db.insert(args.input.model, data); return ctx.db.get(id); })(); if (!doc) throw new Error(`Failed to create ${args.input.model}`); const normalizedDoc = withBothIdFields(doc); const result = await selectFields(normalizedDoc, args.select); const hookDoc = serializeDatesForConvex(normalizedDoc); const id = getDocId$1(hookDoc); await tableTriggers?.create?.after?.(hookDoc, triggerCtx); await tableTriggers?.change?.({ id, newDoc: hookDoc, oldDoc: null, operation: "insert" }, triggerCtx); return toConvexSafe(result); }; const findOneHandler = async (ctx, args, schema, betterAuthSchema) => toConvexSafe(await listOne(ctx, schema, betterAuthSchema, args)); const findManyHandler = async (ctx, args, schema, betterAuthSchema) => toConvexSafe(await paginate(ctx, schema, betterAuthSchema, args)); const updateOneHandler = async (ctx, args, schema, betterAuthSchema) => { const triggerCtx = args.triggerCtx ?? ctx; const tableTriggers = args.tableTriggers; const doc = await listOne(ctx, schema, betterAuthSchema, args.input); if (!doc) throw new Error(`Failed to update ${args.input.model}`); const normalizedDoc = withBothIdFields(doc); const update = stripUnsupportedAuthTimestamps(serializeDatesForConvex(await applyBeforeHook(args.input.model, "update", args.input.update, tableTriggers?.update?.before, triggerCtx)), schema, betterAuthSchema, args.input.model); await checkUniqueFields(ctx, schema, betterAuthSchema, args.input.model, update, normalizedDoc); const ormTable = resolveOrmTable(ctx, schema, betterAuthSchema, args.input.model); const updatedDoc = ormTable ? await ormUpdate(ctx, ormTable.table, normalizedDoc._id, update) : await (async () => { await ctx.db.patch(normalizedDoc._id, update); return ctx.db.get(normalizedDoc._id); })(); if (!updatedDoc) throw new Error(`Failed to update ${args.input.model}`); const normalizedUpdatedDoc = withBothIdFields(updatedDoc); const hookNewDoc = serializeDatesForConvex(normalizedUpdatedDoc); const hookOldDoc = serializeDatesForConvex(normalizedDoc); const id = getDocId$1(hookNewDoc); await tableTriggers?.update?.after?.(hookNewDoc, triggerCtx); await tableTriggers?.change?.({ id, newDoc: hookNewDoc, oldDoc: hookOldDoc, operation: "update" }, triggerCtx); return toConvexSafe(normalizedUpdatedDoc); }; const updateManyHandler = async (ctx, args, schema, betterAuthSchema) => { const triggerCtx = args.triggerCtx ?? ctx; const tableTriggers = args.tableTriggers; const { page, ...result } = await paginate(ctx, schema, betterAuthSchema, { ...args.input, paginationOpts: args.paginationOpts }); const ormTable = resolveOrmTable(ctx, schema, betterAuthSchema, args.input.model); if (args.input.update) { if (hasUniqueFields(betterAuthSchema, args.input.model, args.input.update ?? {}) && page.length > 1) throw new Error(`Attempted to set unique fields in multiple documents in ${args.input.model} with the same value. Fields: ${Object.keys(args.input.update ?? {}).join(", ")}`); await asyncMap(page, async (doc) => { const normalizedDoc = withBothIdFields(doc); const update = stripUnsupportedAuthTimestamps(serializeDatesForConvex(await applyBeforeHook(args.input.model, "update", args.input.update ?? {}, tableTriggers?.update?.before, triggerCtx)), schema, betterAuthSchema, args.input.model); await checkUniqueFields(ctx, schema, betterAuthSchema, args.input.model, update ?? {}, normalizedDoc); const hookNewDoc = serializeDatesForConvex(withBothIdFields(ormTable ? await ormUpdate(ctx, ormTable.table, normalizedDoc._id, update ?? {}) : await (async () => { await ctx.db.patch(normalizedDoc._id, update); return ctx.db.get(normalizedDoc._id); })())); const hookOldDoc = serializeDatesForConvex(normalizedDoc); const id = getDocId$1(hookNewDoc); await tableTriggers?.update?.after?.(hookNewDoc, triggerCtx); await tableTriggers?.change?.({ id, newDoc: hookNewDoc, oldDoc: hookOldDoc, operation: "update" }, triggerCtx); }); } return toConvexSafe({ ...result, count: page.length, ids: page.map((doc) => withBothIdFields(doc)._id) }); }; const deleteOneHandler = async (ctx, args, schema, betterAuthSchema) => { const triggerCtx = args.triggerCtx ?? ctx; const tableTriggers = args.tableTriggers; const doc = await listOne(ctx, schema, betterAuthSchema, args.input); if (!doc) return; const normalizedDoc = withBothIdFields(doc); const hookDoc = withBothIdFields(serializeDatesForConvex(await applyBeforeHook(args.input.model, "delete", normalizedDoc, tableTriggers?.delete?.before, triggerCtx))); const id = getDocId$1(normalizedDoc); const ormTable = resolveOrmTable(ctx, schema, betterAuthSchema, args.input.model); if (ormTable) await ormDelete(ctx, ormTable.table, normalizedDoc._id); else await ctx.db.delete(normalizedDoc._id); await tableTriggers?.delete?.after?.(hookDoc, triggerCtx); await tableTriggers?.change?.({ id, newDoc: null, oldDoc: hookDoc, operation: "delete" }, triggerCtx); return toConvexSafe(withBothIdFields(hookDoc)); }; const deleteManyHandler = async (ctx, args, schema, betterAuthSchema) => { const triggerCtx = args.triggerCtx ?? ctx; const tableTriggers = args.tableTriggers; const { page, ...result } = await paginate(ctx, schema, betterAuthSchema, { ...args.input, paginationOpts: args.paginationOpts }); const ormTable = resolveOrmTable(ctx, schema, betterAuthSchema, args.input.model); await asyncMap(page, async (doc) => { const normalizedDoc = withBothIdFields(doc); const hookDoc = withBothIdFields(serializeDatesForConvex(await applyBeforeHook(args.input.model, "delete", normalizedDoc, tableTriggers?.delete?.before, triggerCtx))); const id = getDocId$1(normalizedDoc); if (ormTable) await ormDelete(ctx, ormTable.table, normalizedDoc._id); else await ctx.db.delete(normalizedDoc._id); await tableTriggers?.delete?.after?.(hookDoc, triggerCtx); await tableTriggers?.change?.({ id, newDoc: null, oldDoc: hookDoc, operation: "delete" }, triggerCtx); }); return toConvexSafe({ ...result, count: page.length, ids: page.map((doc) => withBothIdFields(doc)._id) }); }; const createApi = (schema, getAuth, options) => { const { internalMutation, validateInput = false, context, triggers } = options ?? {}; let betterAuthSchema; const getBetterAuthSchema = () => { betterAuthSchema ??= getAuthTables(getAuth({}).options); return betterAuthSchema; }; const mutationBuilderBase = internalMutation ?? internalMutationGeneric; const mutationBuilder = context ? customMutation(mutationBuilderBase, customCtx(async (ctx) => await context?.(ctx) ?? ctx)) : mutationBuilderBase; const resolveTableTriggers = (model, triggerCtx) => { const tableTriggers = (typeof triggers === "function" ? triggers(triggerCtx) : triggers)?.[model]; return ensureRuntimeTableTriggers(model, tableTriggers); }; const anyInput = v.object({ data: v.any(), model: v.string() }); const anyInputWithWhere = v.object({ model: v.string(), where: v.optional(v.array(v.any())) }); const anyInputWithUpdate = v.object({ model: v.string(), update: v.any(), where: v.optional(v.array(v.any())) }); const authSchemaForValidation = validateInput ? getBetterAuthSchema() : {}; const authTableNames = new Set(Object.keys(authSchemaForValidation)); const authTables = Object.entries(schema.tables).filter(([name]) => authTableNames.has(name)); const authTableKeys = authTables.map(([name]) => name); const createInput = validateInput ? v.union(...authTables.map(([model, table]) => { const fields = partial(table.validator.fields); return v.object({ data: v.object(fields), model: v.literal(model) }); })) : anyInput; const deleteInput = validateInput ? v.union(...authTableKeys.map((tableName) => v.object({ model: v.literal(tableName), where: v.optional(v.array(whereValidator(schema, tableName))) }))) : anyInputWithWhere; const modelValidator = validateInput ? v.union(...authTableKeys.map((model) => v.literal(model))) : v.string(); const updateInput = validateInput ? v.union(...authTables.map(([tableName, table]) => { const fields = partial(table.validator.fields); return v.object({ model: v.literal(tableName), update: v.object(fields), where: v.optional(v.array(whereValidator(schema, tableName))) }); })) : anyInputWithUpdate; return { create: mutationBuilder({ args: { input: createInput, select: v.optional(v.array(v.string())) }, handler: async (ctx, args) => { const triggerCtx = ctx; return createHandler(ctx, { input: args.input, select: args.select, tableTriggers: resolveTableTriggers(args.input.model, triggerCtx), triggerCtx }, schema, getBetterAuthSchema()); } }), deleteMany: mutationBuilder({ args: { input: deleteInput, paginationOpts: paginationOptsValidator }, handler: async (ctx, args) => { const triggerCtx = ctx; return deleteManyHandler(ctx, { input: args.input, paginationOpts: args.paginationOpts, tableTriggers: resolveTableTriggers(args.input.model, triggerCtx), triggerCtx }, schema, getBetterAuthSchema()); } }), deleteOne: mutationBuilder({ args: { input: deleteInput }, handler: async (ctx, args) => { const triggerCtx = ctx; return deleteOneHandler(ctx, { input: args.input, tableTriggers: resolveTableTriggers(args.input.model, triggerCtx), triggerCtx }, schema, getBetterAuthSchema()); } }), findMany: internalQueryGeneric({ args: { join: v.optional(v.any()), limit: v.optional(v.number()), model: modelValidator, offset: v.optional(v.number()), paginationOpts: paginationOptsValidator, sortBy: v.optional(v.object({ direction: v.union(v.literal("asc"), v.literal("desc")), field: v.string() })), where: v.optional(v.array(adapterWhereValidator)) }, handler: async (ctx, args) => findManyHandler(ctx, args, schema, getBetterAuthSchema()) }), findOne: internalQueryGeneric({ args: { join: v.optional(v.any()), model: modelValidator, select: v.optional(v.array(v.string())), where: v.optional(v.array(adapterWhereValidator)) }, handler: async (ctx, args) => findOneHandler(ctx, args, schema, getBetterAuthSchema()) }), getLatestJwks: internalActionGeneric({ args: {}, handler: async (ctx) => { return getAuth(ctx).api.getLatestJwks(); } }), rotateKeys: internalActionGeneric({ args: {}, handler: async (ctx) => { return getAuth(ctx).api.rotateKeys(); } }), updateMany: mutationBuilder({ args: { input: updateInput, paginationOpts: paginationOptsValidator }, handler: async (ctx, args) => { const triggerCtx = ctx; return updateManyHandler(ctx, { input: args.input, paginationOpts: args.paginationOpts, tableTriggers: resolveTableTriggers(args.input.model, triggerCtx), triggerCtx }, schema, getBetterAuthSchema()); } }), updateOne: mutationBuilder({ args: { input: updateInput }, handler: async (ctx, args) => { const triggerCtx = ctx; return updateOneHandler(ctx, { input: args.input, tableTriggers: resolveTableTriggers(args.input.model, triggerCtx), triggerCtx }, schema, getBetterAuthSchema()); } }) }; }; //#endregion //#region src/auth/adapter.ts let didWarnExperimentalJoinsUnsupported = false; const handlePagination = async (next, { limit, numItems } = {}) => { const state = { count: 0, cursor: null, docs: [], isDone: false }; const onResult = (result) => { state.cursor = result.pageStatus === "SplitRecommended" || result.pageStatus === "SplitRequired" ? result.splitCursor ?? result.continueCursor : result.continueCursor; if (result.page) { state.docs.push(...result.page); state.isDone = limit && state.docs.length >= limit || result.isDone; return; } if (result.count) { state.count += result.count; state.isDone = limit && state.count >= limit || result.isDone; return; } state.isDone = result.isDone; }; do onResult(await next({ paginationOpts: { cursor: state.cursor, numItems: Math.min(numItems ?? 200, (limit ?? 200) - state.docs.length, 200) } })); while (!state.isDone); return state; }; const hasOrWhere = (where) => where?.some((clause) => clause.connector === "OR") ?? false; const assertSupportedBulkOrWhere = (where, operation) => { if (hasOrWhere(where) && !where?.every((clause) => clause.connector === "OR")) throw new Error(`Mixed OR/AND where clauses are not supported for ${operation}`); }; const parseWhere = (where) => { if (!where) return []; return (Array.isArray(where) ? where : [where]).map((w) => { if (w.value instanceof Date) return { ...w, value: w.value.getTime() }; return w; }); }; const getDocId = (doc) => { if (doc?._id !== void 0 && doc?._id !== null) return String(doc._id); if (doc?.id !== void 0 && doc?.id !== null) return String(doc.id); }; const dedupeDocsById = (docs) => { const seen = /* @__PURE__ */ new Set(); const deduped = []; for (const doc of docs) { const id = getDocId(doc); if (!id) { deduped.push(doc); continue; } if (seen.has(id)) continue; seen.add(id); deduped.push(doc); } return deduped; }; const selectDocFields = (doc, select) => { if (!select?.length) return doc; return select.reduce((acc, field) => { const sourceField = field === "id" && "_id" in doc ? "_id" : field; acc[sourceField] = doc[sourceField]; return acc; }, {}); }; const adapterConfig = { adapterId: "convex", adapterName: "Convex Adapter", debugLogs: false, disableIdGeneration: true, mapKeysTransformInput: { id: "_id" }, mapKeysTransformOutput: { _id: "id" }, supportsJSON: false, supportsNumericIds: false, supportsDates: false, supportsArrays: true, transaction: false, usePlural: false, customTransformInput: ({ data, fieldAttributes }) => { if (data && fieldAttributes.type === "date") return new Date(data).getTime(); return data; }, customTransformOutput: ({ data, fieldAttributes }) => { if (data && fieldAttributes.type === "date") return new Date(data).getTime(); return data; } }; const ORM_SCHEMA_OPTIONS = Symbol.for("kitcn:OrmSchemaOptions"); const hasOrmSchemaMetadata = (schema) => !!schema && typeof schema === "object" && ORM_SCHEMA_OPTIONS in schema; const createAuthSchema = async ({ file, schema, tables }) => { if (hasOrmSchemaMetadata(schema)) { const { createSchemaOrm } = await import("../create-schema-orm-B3f2Kc8O.js"); return createSchemaOrm({ file, tables }); } const { createSchema } = await import("../create-schema-BXrKE2YY.js"); return createSchema({ file, tables }); }; const httpAdapter = (ctx, { authFunctions, debugLogs, schema }) => { return createAdapterFactory({ config: { ...adapterConfig, debugLogs: debugLogs || false }, adapter: ({ options }) => { options.telemetry = { enabled: false }; if (options.experimental?.joins) { options.experimental = { ...options.experimental, joins: false }; if (!didWarnExperimentalJoinsUnsupported) { didWarnExperimentalJoinsUnsupported = true; console.warn("[kitcn] Better Auth experimental.joins is not supported by the Convex adapter yet. Forcing experimental.joins = false."); } } const collectIdsForOrWhere = async (data) => { return dedupeDocsById((await asyncMap(data.where, async (w) => handlePagination(async ({ paginationOpts }) => await ctx.runQuery(authFunctions.findMany, { model: data.model, paginationOpts, where: parseWhere(w) })))).flatMap((result) => result.docs)).map((doc) => getDocId(doc)).flatMap((id) => id ? [id] : []); }; return { id: "convex", options: { isRunMutationCtx: isRunMutationCtx(ctx) }, count: async (data) => { if (hasOrWhere(data.where)) return dedupeDocsById((await asyncMap(data.where, async (w) => handlePagination(async ({ paginationOpts }) => await ctx.runQuery(authFunctions.findMany, { ...data, paginationOpts, where: parseWhere(w) })))).flatMap((r) => r.docs)).length; return (await handlePagination(async ({ paginationOpts }) => await ctx.runQuery(authFunctions.findMany, { ...data, paginationOpts, where: parseWhere(data.where) }))).docs.length; }, create: async ({ data, model, select }) => { if (!("runMutation" in ctx)) throw new Error("ctx is not a mutation ctx"); return await ctx.runMutation(authFunctions.create, { input: { data, model }, select }); }, createSchema: async ({ file, tables }) => createAuthSchema({ file, schema, tables }), delete: async (data) => { if (!("runMutation" in ctx)) throw new Error("ctx is not a mutation ctx"); await ctx.runMutation(authFunctions.deleteOne, { input: { model: data.model, where: parseWhere(data.where) } }); }, deleteMany: async (data) => { if (!("runMutation" in ctx)) throw new Error("ctx is not a mutation ctx"); assertSupportedBulkOrWhere(data.where, "deleteMany"); if (hasOrWhere(data.where)) { const ids = await collectIdsForOrWhere({ model: data.model, where: data.where }); await asyncMap(ids, async (id) => { await ctx.runMutation(authFunctions.deleteOne, { input: { model: data.model, where: [{ field: "_id", operator: "eq", value: id }] } }); }); return ids.length; } return (await handlePagination(async ({ paginationOpts }) => await ctx.runMutation(authFunctions.deleteMany, { input: { ...data, where: parseWhere(data.where) }, paginationOpts }))).count; }, findMany: async (data) => { if (data.offset) throw new Error("offset not supported"); if (hasOrWhere(data.where)) { const { select: _ignoredSelect, ...queryData } = data; let docs = dedupeDocsById((await asyncMap(data.where, async (w) => handlePagination(async ({ paginationOpts }) => await ctx.runQuery(authFunctions.findMany, { ...queryData, paginationOpts, where: parseWhere(w) }), { limit: data.limit }))).flatMap((r) => r.docs)); if (data.sortBy) docs = sortBy(docs, [prop(data.sortBy.field), data.sortBy.direction]); if (data.limit !== void 0) docs = docs.slice(0, data.limit); return docs.map((doc) => selectDocFields(doc, data.select)); } return (await handlePagination(async ({ paginationOpts }) => await ctx.runQuery(authFunctions.findMany, { ...data, paginationOpts, where: parseWhere(data.where) }), { limit: data.limit })).docs; }, findOne: async (data) => { const parsedWhere = parseWhere(data.where); if (data.where?.every((w) => w.connector === "OR")) for (const w of data.where) { const result = await ctx.runQuery(authFunctions.findOne, { ...data, where: parseWhere(w) }); if (result) return result; } return await ctx.runQuery(authFunctions.findOne, { ...data, where: parsedWhere }); }, update: async (data) => { if (!("runMutation" in ctx)) throw new Error("ctx is not a mutation ctx"); if (data.where?.length && data.where.every((w) => (w.operator === "eq" || w.operator === void 0) && w.connector !== "OR")) { const countResult = await handlePagination(async ({ paginationOpts }) => await ctx.runQuery(authFunctions.findMany, { model: data.model, paginationOpts, where: parseWhere(data.where) }), { limit: 2 }); if (countResult.docs.length === 0) throw new Error(`No ${data.model} found matching criteria`); if (countResult.docs.length > 1) throw new Error(`Multiple ${data.model} found matching criteria. Expected exactly 1.`); return await ctx.runMutation(authFunctions.updateOne, { input: { model: data.model, update: data.update, where: parseWhere(data.where) } }); } throw new Error("where clause not supported"); }, updateMany: async (data) => { if (!("runMutation" in ctx)) throw new Error("ctx is not a mutation ctx"); assertSupportedBulkOrWhere(data.where, "updateMany"); if (hasOrWhere(data.where)) { const ids = await collectIdsForOrWhere({ model: data.model, where: data.where }); if (!ids.length) return 0; return (await handlePagination(async ({ paginationOpts }) => await ctx.runMutation(authFunctions.updateMany, { input: { ...data, where: [{ field: "_id", operator: "in", value: ids }] }, paginationOpts }), { limit: ids.length })).count; } return (await handlePagination(async ({ paginationOpts }) => await ctx.runMutation(authFunctions.updateMany, { input: { ...data, where: parseWhere(data.where) }, paginationOpts }))).count; } }; } }); }; const dbAdapter = (ctx, getAuthOptions, { authFunctions, debugLogs, schema }) => { const betterAuthSchema = getAuthTables(getAuthOptions({})); return createAdapterFactory({ config: { ...adapterConfig, debugLogs: debugLogs || false }, adapter: ({ options }) => { options.telemetry = { enabled: false }; if (options.experimental?.joins) { options.experimental = { ...options.experimental, joins: false }; if (!didWarnExperimentalJoinsUnsupported) { didWarnExperimentalJoinsUnsupported = true; console.warn("[kitcn] Better Auth experimental.joins is not supported by the Convex adapter yet. Forcing experimental.joins = false."); } } const collectIdsForOrWhere = async (data) => { return dedupeDocsById((await asyncMap(data.where, async (w) => handlePagination(async ({ paginationOpts }) => await findManyHandler(ctx, { model: data.model, paginationOpts, where: parseWhere(w) }, schema, betterAuthSchema)))).flatMap((result) => result.docs)).map((doc) => getDocId(doc)).flatMap((id) => id ? [id] : []); }; return { id: "convex", options: { isRunMutationCtx: isRunMutationCtx(ctx) }, count: async (data) => { if (hasOrWhere(data.where)) return dedupeDocsById((await asyncMap(data.where, async (w) => handlePagination(async ({ paginationOpts }) => await findManyHandler(ctx, { ...data, paginationOpts, where: parseWhere(w) }, schema, betterAuthSchema)))).flatMap((r) => r.docs)).length; return (await handlePagination(async ({ paginationOpts }) => await findManyHandler(ctx, { ...data, paginationOpts, where: parseWhere(data.where) }, schema, betterAuthSchema))).docs.length; }, create: async ({ data, model, select }) => { if (!("runMutation" in ctx)) throw new Error("ctx is not a mutation ctx"); return await ctx.runMutation(authFunctions.create, { input: { data, model }, select }); }, createSchema: async ({ file, tables }) => createAuthSchema({ file, schema, tables }), delete: async (data) => { if (!("runMutation" in ctx)) throw new Error("ctx is not a mutation ctx"); await ctx.runMutation(authFunctions.deleteOne, { input: { model: data.model, where: parseWhere(data.where) } }); }, deleteMany: async (data) => { if (!("runMutation" in ctx)) throw new Error("ctx is not a mutation ctx"); assertSupportedBulkOrWhere(data.where, "deleteMany"); if (hasOrWhere(data.where)) { const ids = await collectIdsForOrWhere({ model: data.model, where: data.where }); await asyncMap(ids, async (id) => { await ctx.runMutation(authFunctions.deleteOne, { input: { model: data.model, where: [{ field: "_id", operator: "eq", value: id }] } }); }); return ids.length; } return (await handlePagination(async ({ paginationOpts }) => await ctx.runMutation(authFunctions.deleteMany, { input: { ...data, where: parseWhere(data.where) }, paginationOpts }))).count; }, findMany: async (data) => { if (data.offset) throw new Error("offset not supported"); if (hasOrWhere(data.where)) { const { select: _ignoredSelect, ...queryData } = data; let docs = dedupeDocsById((await asyncMap(data.where, async (w) => handlePagination(async ({ paginationOpts }) => await findManyHandler(ctx, { ...queryData, paginationOpts, where: parseWhere(w) }, schema, betterAuthSchema), { limit: data.limit }))).flatMap((r) => r.docs)); if (data.sortBy) docs = sortBy(docs, [prop(data.sortBy.field), data.sortBy.direction]); if (data.limit !== void 0) docs = docs.slice(0, data.limit); return docs.map((doc) => selectDocFields(doc, data.select)); } return (await handlePagination(async ({ paginationOpts }) => await findManyHandler(ctx, { ...data, paginationOpts, where: parseWhere(data.where) }, schema, betterAuthSchema), { limit: data.limit })).docs; }, findOne: async (data) => { if (data.where?.every((w) => w.connector === "OR")) for (const w of data.where) { const result = await findOneHandler(ctx, { ...data, where: parseWhere(w) }, schema, betterAuthSchema); if (result) return result; } return await findOneHandler(ctx, { ...data, where: parseWhere(data.where) }, schema, betterAuthSchema); }, update: async (data) => { if (data.where?.length && data.where.every((w) => (w.operator === "eq" || w.operator === void 0) && w.connector !== "OR")) { const countResult = await handlePagination(async ({ paginationOpts }) => await findManyHandler(ctx, { model: data.model, paginationOpts, where: parseWhere(data.where) }, schema, betterAuthSchema), { limit: 2 }); if (countResult.docs.length === 0) throw new Error(`No ${data.model} found matching criteria`); if (countResult.docs.length > 1) throw new Error(`Multiple ${data.model} found matching criteria. Expected exactly 1.`); if (!("runMutation" in ctx)) throw new Error("ctx is not a mutation ctx"); return await ctx.runMutation(authFunctions.updateOne, { input: { model: data.model, update: data.update, where: parseWhere(data.where) } }); } throw new Error("where clause not supported"); }, updateMany: async (data) => { if (!("runMutation