@proofkit/better-auth
Version:
FileMaker adapter for Better Auth
221 lines (220 loc) • 7.03 kB
JavaScript
import { isODataError, isFMODataError } from "@proofkit/fmodata";
import chalk from "chalk";
function normalizeBetterAuthFieldType(fieldType) {
if (typeof fieldType === "string") {
return fieldType;
}
if (Array.isArray(fieldType)) {
return fieldType.map(String).join("|");
}
return String(fieldType);
}
async function getMetadata(db) {
const metadata = await db.getMetadata({ format: "json" });
return metadata;
}
function mapFieldType(t) {
if (t.includes("boolean") || t.includes("number")) {
return "numeric";
}
if (t.includes("date")) {
return "timestamp";
}
return "string";
}
async function planMigration(db, betterAuthSchema) {
const metadata = await getMetadata(db);
const entitySetToType = {};
for (const [key, value] of Object.entries(metadata)) {
if (value.$Kind === "EntitySet" && value.$Type) {
const typeKey = value.$Type.split(".").pop();
entitySetToType[key] = typeKey || key;
}
}
const existingTables = Object.entries(entitySetToType).reduce(
(acc, [entitySetName, entityTypeKey]) => {
const entityType = metadata[entityTypeKey];
if (!entityType) {
return acc;
}
const fields = Object.entries(entityType).filter(
([_fieldKey, fieldValue]) => typeof fieldValue === "object" && fieldValue !== null && "$Type" in fieldValue
).map(([fieldKey, fieldValue]) => {
let type = "string";
if (fieldValue.$Type === "Edm.String") {
type = "string";
} else if (fieldValue.$Type === "Edm.DateTimeOffset") {
type = "timestamp";
} else if (fieldValue.$Type === "Edm.Decimal" || fieldValue.$Type === "Edm.Int32" || fieldValue.$Type === "Edm.Int64") {
type = "numeric";
}
return {
name: fieldKey,
type
};
});
acc[entitySetName] = fields;
return acc;
},
{}
);
const baTables = Object.entries(betterAuthSchema).sort((a, b) => (a[1].order ?? 0) - (b[1].order ?? 0)).map(([key, value]) => ({
...value,
modelName: key
}));
const migrationPlan = [];
for (const baTable of baTables) {
const fields = Object.entries(baTable.fields).map(([key, field]) => {
const t = normalizeBetterAuthFieldType(field.type);
const type = mapFieldType(t);
return {
name: field.fieldName ?? key,
type
};
});
const tableExists = baTable.modelName in existingTables;
if (tableExists) {
const existingFields = (existingTables[baTable.modelName] || []).map((f) => f.name);
const existingFieldMap = (existingTables[baTable.modelName] || []).reduce(
(acc, f) => {
acc[f.name] = f.type;
return acc;
},
{}
);
for (const field of fields) {
if (existingFields.includes(field.name) && existingFieldMap[field.name] !== field.type) {
console.warn(
`⚠️ WARNING: Field '${field.name}' in table '${baTable.modelName}' exists but has type '${existingFieldMap[field.name]}' (expected '${field.type}'). Change the field type in FileMaker to avoid potential errors.`
);
}
}
const fieldsToAdd = fields.filter((f) => !existingFields.includes(f.name));
if (fieldsToAdd.length > 0) {
migrationPlan.push({
tableName: baTable.modelName,
operation: "update",
fields: fieldsToAdd
});
}
} else {
migrationPlan.push({
tableName: baTable.modelName,
operation: "create",
fields: [
{
name: "id",
type: "string",
primary: true,
unique: true
},
...fields
]
});
}
}
return migrationPlan;
}
async function executeMigration(db, migrationPlan) {
for (const step of migrationPlan) {
const fmodataFields = step.fields.map((f) => ({
name: f.name,
type: f.type,
...f.primary ? { primary: true } : {},
...f.unique ? { unique: true } : {}
}));
if (step.operation === "create") {
console.log("Creating table:", step.tableName);
try {
await db.schema.createTable(step.tableName, fmodataFields);
} catch (error) {
throw migrationError("create", step.tableName, error);
}
} else if (step.operation === "update") {
console.log("Adding fields to table:", step.tableName);
try {
await db.schema.addFields(step.tableName, fmodataFields);
} catch (error) {
throw migrationError("update", step.tableName, error);
}
}
}
}
function formatError(error) {
if (isODataError(error)) {
const code = error.code ? ` (${error.code})` : "";
return `${error.message}${code}`;
}
if (isFMODataError(error)) {
return error.message;
}
if (error instanceof Error) {
return error.message;
}
return String(error);
}
function migrationError(operation, tableName, error) {
const action = operation === "create" ? "create table" : "update table";
const base = `Failed to ${action} "${tableName}"`;
if (isODataError(error) && error.code === "207") {
console.error(
chalk.red(`
${base}: Cannot modify schema.`),
chalk.yellow("\nThe account used does not have schema modification privileges."),
chalk.gray(
"\nUse --username and --password to provide Full Access credentials, or grant schema modification privileges to the current account."
)
);
} else {
console.error(chalk.red(`
${base}:`), formatError(error));
}
return new Error(`Migration failed: ${formatError(error)}`);
}
function prettyPrintMigrationPlan(migrationPlan, target) {
if (!migrationPlan.length) {
console.log("No changes to apply. Database is up to date.");
return;
}
console.log(chalk.bold.green("Migration plan:"));
if ((target == null ? void 0 : target.serverUrl) || (target == null ? void 0 : target.fileName)) {
const parts = [];
if (target.fileName) {
parts.push(chalk.cyan(target.fileName));
}
if (target.serverUrl) {
parts.push(chalk.gray(target.serverUrl));
}
console.log(` Target: ${parts.join(" @ ")}`);
}
for (const step of migrationPlan) {
const emoji = step.operation === "create" ? "✅" : "✏️";
console.log(
`
${emoji} ${step.operation === "create" ? chalk.bold.green("Create table") : chalk.bold.yellow("Update table")}: ${step.tableName}`
);
if (step.fields.length) {
for (const field of step.fields) {
let fieldDesc = ` - ${field.name} (${field.type}`;
if (field.primary) {
fieldDesc += ", primary";
}
if (field.unique) {
fieldDesc += ", unique";
}
fieldDesc += ")";
console.log(fieldDesc);
}
} else {
console.log(" (No fields to add)");
}
}
console.log("");
}
export {
executeMigration,
getMetadata,
planMigration,
prettyPrintMigrationPlan
};
//# sourceMappingURL=migrate.js.map