UNPKG

@proofkit/better-auth

Version:

FileMaker adapter for Better Auth

258 lines (257 loc) 8.5 kB
import { createAdapter } from "better-auth/adapters"; import { createFmOdataFetch } from "./odata/index.js"; import { z, prettifyError } from "zod/v4"; import { logger } from "better-auth"; const configSchema = z.object({ debugLogs: z.unknown().optional(), usePlural: z.boolean().optional(), odata: z.object({ serverUrl: z.url(), auth: z.union([ z.object({ username: z.string(), password: z.string() }), z.object({ apiKey: z.string() }) ]), database: z.string().endsWith(".fmp12") }) }); const defaultConfig = { debugLogs: false, usePlural: false, odata: { serverUrl: "", auth: { username: "", password: "" }, database: "" } }; function parseWhere(where) { if (!where || where.length === 0) return ""; function quoteField(field, value) { if (value === null || value instanceof Date) return field; if (field === "id" || /[\s_]/.test(field)) return `"${field}"`; return field; } function formatValue(value) { if (value === null) return "null"; if (typeof value === "boolean") return value ? "true" : "false"; if (value instanceof Date) return value.toISOString(); if (Array.isArray(value)) return `(${value.map(formatValue).join(",")})`; if (typeof value === "string") { const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/; if (isoDateRegex.test(value)) { return value; } return `'${value.replace(/'/g, "''")}'`; } return (value == null ? void 0 : value.toString()) ?? ""; } const opMap = { eq: "eq", ne: "ne", lt: "lt", lte: "le", gt: "gt", gte: "ge" }; const clauses = []; for (let i = 0; i < where.length; i++) { const cond = where[i]; if (!cond) continue; const field = quoteField(cond.field, cond.value); let clause = ""; switch (cond.operator) { case "eq": case "ne": case "lt": case "lte": case "gt": case "gte": clause = `${field} ${opMap[cond.operator]} ${formatValue(cond.value)}`; break; case "in": if (Array.isArray(cond.value)) { clause = cond.value.map((v) => `${field} eq ${formatValue(v)}`).join(" or "); clause = `(${clause})`; } break; case "contains": clause = `contains(${field}, ${formatValue(cond.value)})`; break; case "starts_with": clause = `startswith(${field}, ${formatValue(cond.value)})`; break; case "ends_with": clause = `endswith(${field}, ${formatValue(cond.value)})`; break; default: clause = `${field} eq ${formatValue(cond.value)}`; } clauses.push(clause); if (i < where.length - 1) { clauses.push((cond.connector || "and").toLowerCase()); } } return clauses.join(" "); } const FileMakerAdapter = (_config = defaultConfig) => { const parsed = configSchema.loose().safeParse(_config); if (!parsed.success) { throw new Error(`Invalid configuration: ${prettifyError(parsed.error)}`); } const config = parsed.data; const fetch = createFmOdataFetch({ ...config.odata, logging: config.debugLogs ? "verbose" : "none" }); return createAdapter({ config: { adapterId: "filemaker", adapterName: "FileMaker", usePlural: config.usePlural ?? false, // Whether the table names in the schema are plural. debugLogs: config.debugLogs ?? false, // Whether to enable debug logs. supportsJSON: false, // Whether the database supports JSON. (Default: false) supportsDates: false, // Whether the database supports dates. (Default: true) supportsBooleans: false, // Whether the database supports booleans. (Default: true) supportsNumericIds: false // Whether the database supports auto-incrementing numeric IDs. (Default: true) }, adapter: ({ options }) => { return { options: { config }, create: async ({ data, model, select }) => { if (model === "session") { console.log("session", data); } const result = await fetch(`/${model}`, { method: "POST", body: data, output: z.looseObject({ id: z.string() }) }); if (result.error) { throw new Error("Failed to create record"); } return result.data; }, count: async ({ model, where }) => { var _a; const filter = parseWhere(where); logger.debug("$filter", filter); const result = await fetch(`/${model}/$count`, { method: "GET", query: { $filter: filter }, output: z.object({ value: z.number() }) }); if (!result.data) { throw new Error("Failed to count records"); } return ((_a = result.data) == null ? void 0 : _a.value) ?? 0; }, findOne: async ({ model, where }) => { var _a, _b; const filter = parseWhere(where); logger.debug("$filter", filter); const result = await fetch(`/${model}`, { method: "GET", query: { ...filter.length > 0 ? { $filter: filter } : {}, $top: 1 }, output: z.object({ value: z.array(z.any()) }) }); if (result.error) { throw new Error("Failed to find record"); } return ((_b = (_a = result.data) == null ? void 0 : _a.value) == null ? void 0 : _b[0]) ?? null; }, findMany: async ({ model, where, limit, offset, sortBy }) => { var _a; const filter = parseWhere(where); logger.debug("$filter", filter); const rows = await fetch(`/${model}`, { method: "GET", query: { ...filter.length > 0 ? { $filter: filter } : {}, $top: limit, $skip: offset, ...sortBy ? { $orderby: `"${sortBy.field}" ${sortBy.direction ?? "asc"}` } : {} }, output: z.object({ value: z.array(z.any()) }) }); if (rows.error) { throw new Error("Failed to find records"); } return ((_a = rows.data) == null ? void 0 : _a.value) ?? []; }, delete: async ({ model, where }) => { const filter = parseWhere(where); logger.debug("$filter", filter); console.log("delete", model, where, filter); const result = await fetch(`/${model}`, { method: "DELETE", query: { ...where.length > 0 ? { $filter: filter } : {}, $top: 1 } }); if (result.error) { throw new Error("Failed to delete record"); } }, deleteMany: async ({ model, where }) => { const filter = parseWhere(where); logger.debug( where.map((o) => `typeof ${o.value} is ${typeof o.value}`).join("\n") ); logger.debug("$filter", filter); const result = await fetch(`/${model}/$count`, { method: "DELETE", query: { ...where.length > 0 ? { $filter: filter } : {} }, output: z.coerce.number() }); if (result.error) { throw new Error("Failed to delete record"); } return result.data ?? 0; }, update: async ({ model, where, update }) => { var _a, _b; const result = await fetch(`/${model}`, { method: "PATCH", query: { ...where.length > 0 ? { $filter: parseWhere(where) } : {}, $top: 1, $select: [`"id"`] }, body: update, output: z.object({ value: z.array(z.any()) }) }); return ((_b = (_a = result.data) == null ? void 0 : _a.value) == null ? void 0 : _b[0]) ?? null; }, updateMany: async ({ model, where, update }) => { const filter = parseWhere(where); const result = await fetch(`/${model}`, { method: "PATCH", query: { ...where.length > 0 ? { $filter: filter } : {} }, body: update }); return result.data; } }; } }); }; export { FileMakerAdapter, parseWhere }; //# sourceMappingURL=adapter.js.map