@proofkit/better-auth
Version:
FileMaker adapter for Better Auth
258 lines (257 loc) • 8.5 kB
JavaScript
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