UNPKG

@proofkit/better-auth

Version:

FileMaker adapter for Better Auth

221 lines (220 loc) 7.03 kB
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