kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
1,182 lines (1,179 loc) • 60.1 kB
JavaScript
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