@proofkit/better-auth
Version:
FileMaker adapter for Better Auth
201 lines (200 loc) • 6.64 kB
JavaScript
import chalk from "chalk";
import z from "zod/v4";
async function getMetadata(fetch, databaseName) {
var _a;
console.log("getting metadata...");
const result = await fetch("/$metadata", {
method: "GET",
headers: { accept: "application/json" },
output: z.looseObject({
$Version: z.string(),
"@ServerVersion": z.string()
}).or(z.null()).catch(null)
});
return ((_a = result.data) == null ? void 0 : _a[databaseName]) ?? null;
}
async function planMigration(fetch, betterAuthSchema, databaseName) {
const metadata = await getMetadata(fetch, databaseName);
let entitySetToType = {};
if (metadata) {
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 = metadata ? 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]) => ({
name: fieldKey,
type: fieldValue.$Type === "Edm.String" ? "varchar" : fieldValue.$Type === "Edm.DateTimeOffset" ? "timestamp" : fieldValue.$Type === "Edm.Decimal" || fieldValue.$Type === "Edm.Int32" || fieldValue.$Type === "Edm.Int64" ? "numeric" : "varchar"
}));
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,
keyName: key
}));
const migrationPlan = [];
for (const baTable of baTables) {
const fields = Object.entries(baTable.fields).map(
([key, field]) => ({
name: field.fieldName ?? key,
type: field.type === "boolean" || field.type.includes("number") ? "numeric" : field.type === "date" ? "timestamp" : "varchar"
})
);
const tableExists = Object.prototype.hasOwnProperty.call(
existingTables,
baTable.modelName
);
if (!tableExists) {
migrationPlan.push({
tableName: baTable.modelName,
operation: "create",
fields: [
{
name: "id",
type: "varchar",
primary: true,
unique: true
},
...fields
]
});
} else {
const existingFields = (existingTables[baTable.modelName] || []).map(
(f) => f.name
);
const existingFieldMap = (existingTables[baTable.modelName] || []).reduce(
(acc, f) => {
acc[f.name] = f.type;
return acc;
},
{}
);
fields.forEach((field) => {
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
});
}
}
}
return migrationPlan;
}
async function executeMigration(fetch, migrationPlan) {
for (const step of migrationPlan) {
if (step.operation === "create") {
console.log("Creating table:", step.tableName);
await fetch("@post/FileMaker_Tables", {
body: {
tableName: step.tableName,
fields: step.fields
}
});
} else if (step.operation === "update") {
console.log("Adding fields to table:", step.tableName);
await fetch("@post/FileMaker_Tables/:tableName", {
params: { tableName: step.tableName },
body: { fields: step.fields }
});
}
}
}
const genericFieldSchema = z.object({
name: z.string(),
nullable: z.boolean().optional(),
primary: z.boolean().optional(),
unique: z.boolean().optional(),
global: z.boolean().optional(),
repetitions: z.number().optional()
});
const stringFieldSchema = genericFieldSchema.extend({
type: z.literal("varchar"),
maxLength: z.number().optional(),
default: z.enum(["USER", "USERNAME", "CURRENT_USER"]).optional()
});
const numericFieldSchema = genericFieldSchema.extend({
type: z.literal("numeric")
});
const dateFieldSchema = genericFieldSchema.extend({
type: z.literal("date"),
default: z.enum(["CURRENT_DATE", "CURDATE"]).optional()
});
const timeFieldSchema = genericFieldSchema.extend({
type: z.literal("time"),
default: z.enum(["CURRENT_TIME", "CURTIME"]).optional()
});
const timestampFieldSchema = genericFieldSchema.extend({
type: z.literal("timestamp"),
default: z.enum(["CURRENT_TIMESTAMP", "CURTIMESTAMP"]).optional()
});
const containerFieldSchema = genericFieldSchema.extend({
type: z.literal("container"),
externalSecurePath: z.string().optional()
});
const fieldSchema = z.discriminatedUnion("type", [
stringFieldSchema,
numericFieldSchema,
dateFieldSchema,
timeFieldSchema,
timestampFieldSchema,
containerFieldSchema
]);
z.object({
tableName: z.string(),
operation: z.enum(["create", "update"]),
fields: z.array(fieldSchema)
}).array();
function prettyPrintMigrationPlan(migrationPlan) {
if (!migrationPlan.length) {
console.log("No changes to apply. Database is up to date.");
return;
}
console.log(chalk.bold.green("Migration plan:"));
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