@proofkit/better-auth
Version:
FileMaker adapter for Better Auth
288 lines (287 loc) • 9.62 kB
JavaScript
import { logger } from "better-auth";
import { createAdapterFactory } from "better-auth/adapters";
const FIELD_SPECIAL_CHARS_REGEX = /[\s_]/;
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/;
function parseWhere(where) {
if (!where || where.length === 0) {
return "";
}
function quoteField(field, value) {
if (value === null || value instanceof Date) {
return field;
}
if (field === "id" || FIELD_SPECIAL_CHARS_REGEX.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") {
if (ISO_DATE_REGEX.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(" ");
}
function buildQueryString(params) {
var _a;
const parts = [];
if (params.top !== void 0) {
parts.push(`$top=${params.top}`);
}
if (params.skip !== void 0) {
parts.push(`$skip=${params.skip}`);
}
if (params.filter) {
parts.push(`$filter=${encodeURIComponent(params.filter)}`);
}
if (params.orderBy) {
parts.push(`$orderby=${encodeURIComponent(params.orderBy)}`);
}
if ((_a = params.select) == null ? void 0 : _a.length) {
parts.push(`$select=${params.select.map(encodeURIComponent).join(",")}`);
}
return parts.length > 0 ? `?${parts.join("&")}` : "";
}
const FileMakerAdapter = (config) => {
if (!config.database || typeof config.database !== "object") {
throw new Error("FileMakerAdapter requires a `database` (fmodata Database instance).");
}
const db = config.database;
const adapterFactory = createAdapterFactory({
config: {
adapterId: "filemaker",
adapterName: "FileMaker",
usePlural: config.usePlural ?? false,
debugLogs: config.debugLogs ?? false,
supportsJSON: false,
supportsDates: false,
supportsBooleans: false,
supportsNumericIds: false
},
adapter: () => {
return {
create: async ({ data, model }) => {
const result = await db._makeRequest(`/${model}`, {
method: "POST",
body: JSON.stringify(data)
});
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 query = buildQueryString({
filter: filter.length > 0 ? filter : void 0
});
const result = await db._makeRequest(`/${model}/$count${query}`);
if (result.error) {
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 query = buildQueryString({
top: 1,
filter: filter.length > 0 ? filter : void 0
});
const result = await db._makeRequest(`/${model}${query}`);
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("FIND MANY", { where, filter });
const query = buildQueryString({
top: limit,
skip: offset,
orderBy: sortBy ? `${sortBy.field} ${sortBy.direction ?? "asc"}` : void 0,
filter: filter.length > 0 ? filter : void 0
});
logger.debug("QUERY", query);
const result = await db._makeRequest(`/${model}${query}`);
logger.debug("RESULT", result);
if (result.error) {
throw new Error("Failed to find records");
}
return ((_a = result.data) == null ? void 0 : _a.value) ?? [];
},
delete: async ({ model, where }) => {
var _a, _b, _c;
const filter = parseWhere(where);
logger.debug("$filter", filter);
const query = buildQueryString({
top: 1,
select: [`"id"`],
filter: filter.length > 0 ? filter : void 0
});
const toDelete = await db._makeRequest(`/${model}${query}`);
const id = (_c = (_b = (_a = toDelete.data) == null ? void 0 : _a.value) == null ? void 0 : _b[0]) == null ? void 0 : _c.id;
if (!id) {
return;
}
const result = await db._makeRequest(`/${model}('${id}')`, {
method: "DELETE"
});
if (result.error) {
throw new Error("Failed to delete record");
}
},
deleteMany: async ({ model, where }) => {
var _a, _b;
const filter = parseWhere(where);
const query = buildQueryString({
select: [`"id"`],
filter: filter.length > 0 ? filter : void 0
});
const rows = await db._makeRequest(`/${model}${query}`);
const ids = ((_b = (_a = rows.data) == null ? void 0 : _a.value) == null ? void 0 : _b.map((r) => r.id)) ?? [];
let deleted = 0;
for (const id of ids) {
const res = await db._makeRequest(`/${model}('${id}')`, {
method: "DELETE"
});
if (!res.error) {
deleted++;
}
}
return deleted;
},
update: async ({ model, where, update }) => {
var _a, _b, _c;
const filter = parseWhere(where);
logger.debug("UPDATE", { model, where, update });
logger.debug("$filter", filter);
const query = buildQueryString({
select: [`"id"`],
filter: filter.length > 0 ? filter : void 0
});
const existing = await db._makeRequest(`/${model}${query}`);
logger.debug("EXISTING", existing.data);
const id = (_c = (_b = (_a = existing.data) == null ? void 0 : _a.value) == null ? void 0 : _b[0]) == null ? void 0 : _c.id;
if (!id) {
return null;
}
const patchRes = await db._makeRequest(`/${model}('${id}')`, {
method: "PATCH",
body: JSON.stringify(update)
});
logger.debug("PATCH RES", patchRes.data);
if (patchRes.error) {
return null;
}
const readBack = await db._makeRequest(`/${model}('${id}')`);
logger.debug("READ BACK", readBack.data);
return readBack.data ?? null;
},
updateMany: async ({ model, where, update }) => {
var _a, _b;
const filter = parseWhere(where);
const query = buildQueryString({
select: [`"id"`],
filter: filter.length > 0 ? filter : void 0
});
const rows = await db._makeRequest(`/${model}${query}`);
const ids = ((_b = (_a = rows.data) == null ? void 0 : _a.value) == null ? void 0 : _b.map((r) => r.id)) ?? [];
let updated = 0;
for (const id of ids) {
const res = await db._makeRequest(`/${model}('${id}')`, {
method: "PATCH",
body: JSON.stringify(update)
});
if (!res.error) {
updated++;
}
}
return updated;
}
};
}
});
const originalFactory = adapterFactory;
const wrappedFactory = ((options) => {
const adapter = originalFactory(options);
adapter.database = db;
return adapter;
});
wrappedFactory.database = db;
return wrappedFactory;
};
export {
FileMakerAdapter,
parseWhere
};
//# sourceMappingURL=adapter.js.map