Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix-metadata-key-lookup-better-auth-1.5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@proofkit/fmodata": patch
"@proofkit/better-auth": minor
---

Fix `getMetadata()` key lookup when FileMaker Server returns the database name without `.fmp12` extension. Upgrade better-auth to 1.5.x (`createAdapter` → `createAdapterFactory`, removed `getAdapter`).
2 changes: 1 addition & 1 deletion packages/better-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@commander-js/extra-typings": "^14.0.0",
"@proofkit/fmodata": "workspace:*",
"@tanstack/vite-config": "^0.2.1",
"better-auth": "^1.4.11",
"better-auth": "^1.5.4",
"c12": "^3.3.3",
"chalk": "5.4.1",
"commander": "^14.0.2",
Expand Down
4 changes: 2 additions & 2 deletions packages/better-auth/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: library code */
import type { Database } from "@proofkit/fmodata";
import { logger } from "better-auth";
import { type CleanedWhere, createAdapter, type DBAdapterDebugLogOption } from "better-auth/adapters";
import { type CleanedWhere, createAdapterFactory, type DBAdapterDebugLogOption } from "better-auth/adapters";

export interface FileMakerAdapterConfig {
/**
Expand Down Expand Up @@ -164,7 +164,7 @@ export const FileMakerAdapter = (config: FileMakerAdapterConfig) => {

const db = config.database;

const adapterFactory = createAdapter({
const adapterFactory = createAdapterFactory({
config: {
adapterId: "filemaker",
adapterName: "FileMaker",
Expand Down
19 changes: 14 additions & 5 deletions packages/better-auth/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Command } from "@commander-js/extra-typings";
import type { Database, FFetchOptions } from "@proofkit/fmodata";
import { FMServerConnection } from "@proofkit/fmodata";
import { logger } from "better-auth";
import { getAdapter, getSchema } from "better-auth/db";
import { getSchema } from "better-auth/db";
import chalk from "chalk";
import fs from "fs-extra";
import prompts from "prompts";
Expand Down Expand Up @@ -40,12 +40,21 @@ async function main() {
return;
}

const adapter = await getAdapter(config).catch((e) => {
logger.error(e.message);
// Resolve adapter directly (getAdapter removed in Better Auth 1.5)
const databaseFactory = config.database;
if (!databaseFactory || typeof databaseFactory !== "function") {
logger.error("No database adapter found in auth config.");
process.exit(1);
});
}
let adapter: { id?: string; database?: unknown };
try {
adapter = (databaseFactory as (opts: unknown) => { id?: string; database?: unknown })(config);
} catch (e) {
logger.error(e instanceof Error ? e.message : String(e));
process.exit(1);
}

if (adapter.id !== "filemaker") {
if (adapter?.id !== "filemaker") {
logger.error("This generator is only compatible with the FileMaker adapter.");
return;
}
Expand Down
14 changes: 8 additions & 6 deletions packages/better-auth/tests/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ describe("FileMakerAdapter", () => {
model: "user",
where: [{ field: "email", operator: "eq", value: "test@example.com", connector: "AND" }],
});
const user = result as { id: string; email: string } | null;

expect(result).toBeDefined();
expect(result?.id).toBe("user-123");
expect(result?.email).toBe("test@example.com");
expect(user).toBeDefined();
expect(user?.id).toBe("user-123");
expect(user?.email).toBe("test@example.com");
});

it("should return null when no record found", async () => {
Expand Down Expand Up @@ -163,10 +164,11 @@ describe("FileMakerAdapter", () => {
where: [{ field: "id", operator: "eq", value: "user-123", connector: "AND" }],
update: { email: "updated@example.com", name: "Updated User" },
});
const user = result as { email: string; name: string } | null;

expect(result).toBeDefined();
expect(result?.email).toBe("updated@example.com");
expect(result?.name).toBe("Updated User");
expect(user).toBeDefined();
expect(user?.email).toBe("updated@example.com");
expect(user?.name).toBe("Updated User");
});

it("should return null when record to update not found", async () => {
Expand Down
13 changes: 4 additions & 9 deletions packages/better-auth/tests/e2e/adapter.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { FMServerConnection } from "@proofkit/fmodata";
import { runAdapterTest } from "better-auth/adapters/test";
import { beforeAll, describe, expect, it } from "vitest";
import { FileMakerAdapter } from "../../src";

// Note: runAdapterTest was removed in Better Auth 1.5. Adapter behavior is covered by
// unit tests (adapter.test.ts) and the custom e2e tests below.

if (!process.env.FM_SERVER) {
throw new Error("FM_SERVER is not set");
}
Expand All @@ -25,7 +27,7 @@ const connection = new FMServerConnection({
});
const db = connection.database(process.env.FM_DATABASE);

describe("My Adapter Tests", async () => {
describe("My Adapter Tests", () => {
beforeAll(async () => {
// reset the database
for (const table of ["user", "session", "account", "verification"]) {
Expand Down Expand Up @@ -69,13 +71,6 @@ describe("My Adapter Tests", async () => {
database: db,
});

await runAdapterTest({
// biome-ignore lint/suspicious/useAwait: must be an async function
getAdapter: async (betterAuthOptions = {}) => {
return adapter(betterAuthOptions);
},
});

it("should sort descending", async () => {
const result = await adapter({}).findMany({
model: "verification",
Expand Down
19 changes: 11 additions & 8 deletions packages/fmodata/src/client/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { EntitySet } from "./entity-set";
import { SchemaManager } from "./schema-manager";
import { WebhookManager } from "./webhook-builder";

const FMP12_EXT_REGEX = /\.fmp12$/i;

interface MetadataArgs {
format?: "xml" | "json";
/**
Expand Down Expand Up @@ -137,15 +139,16 @@ export class Database<IncludeSpecialColumns extends boolean = false> {
throw result.error;
}

if (args?.format === "json") {
const data = result.data as Record<string, Metadata>;
const metadata = data[this.databaseName];
if (!metadata) {
throw new Error(`Metadata for database "${this.databaseName}" not found in response`);
}
return metadata;
if (args?.format === "xml") {
return result.data as string;
}

const data = result.data as Record<string, Metadata>;
const metadata = data[this.databaseName] ?? data[this.databaseName.replace(FMP12_EXT_REGEX, "")];
if (!metadata) {
throw new Error(`Metadata for database "${this.databaseName}" not found in response`);
}
return result.data as string;
return metadata;
}

/**
Expand Down
122 changes: 122 additions & 0 deletions packages/fmodata/tests/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Metadata Key Lookup Tests
*
* Covers the behavior of Database.getMetadata() when the OData server
* returns metadata keys without the .fmp12 extension.
*
* FileMaker Server returns the database name as the key in the metadata
* response WITHOUT the .fmp12 extension (e.g. "GMT_Web" not "GMT_Web.fmp12"),
* but the Database instance is constructed with the full filename including
* the extension.
*/

import { FMServerConnection } from "@proofkit/fmodata";
import { describe, expect, it } from "vitest";

function makeMetadataFetch(responseBody: unknown, status = 200): typeof fetch {
return (_input: RequestInfo | URL, _init?: RequestInit): Promise<Response> =>
Promise.resolve(
new Response(JSON.stringify(responseBody), {
status,
headers: { "content-type": "application/json" },
}),
);
}

const SAMPLE_METADATA = {
"@SchemaVersion": "1.0",
someTable: { $Kind: "EntityType" },
};

describe("Database.getMetadata() key lookup", () => {
it("resolves metadata when server returns key without .fmp12 extension", async () => {
const responseBody = {
$Version: "4.01",
GMT_Web: SAMPLE_METADATA,
};

const client = new FMServerConnection({
serverUrl: "https://api.example.com",
auth: { apiKey: "test" },
fetchClientOptions: { fetchHandler: makeMetadataFetch(responseBody) },
});

const db = client.database("GMT_Web.fmp12");
const metadata = await db.getMetadata();

expect(metadata).toEqual(SAMPLE_METADATA);
});

it("resolves metadata when server returns key with .fmp12 extension (legacy/future servers)", async () => {
const responseBody = {
$Version: "4.01",
"GMT_Web.fmp12": SAMPLE_METADATA,
};

const client = new FMServerConnection({
serverUrl: "https://api.example.com",
auth: { apiKey: "test" },
fetchClientOptions: { fetchHandler: makeMetadataFetch(responseBody) },
});

const db = client.database("GMT_Web.fmp12");
const metadata = await db.getMetadata();

expect(metadata).toEqual(SAMPLE_METADATA);
});

it("prefers the full name (with .fmp12) over the stripped name when both are present", async () => {
const metadataWithExt = { note: "full name match" };
const metadataWithoutExt = { note: "stripped name match" };

const responseBody = {
"GMT_Web.fmp12": metadataWithExt,
GMT_Web: metadataWithoutExt,
};

const client = new FMServerConnection({
serverUrl: "https://api.example.com",
auth: { apiKey: "test" },
fetchClientOptions: { fetchHandler: makeMetadataFetch(responseBody) },
});

const db = client.database("GMT_Web.fmp12");
const metadata = await db.getMetadata();

expect(metadata).toEqual(metadataWithExt);
});

it("throws when neither the full name nor the stripped name is present", async () => {
const responseBody = {
$Version: "4.01",
some_other_db: SAMPLE_METADATA,
};

const client = new FMServerConnection({
serverUrl: "https://api.example.com",
auth: { apiKey: "test" },
fetchClientOptions: { fetchHandler: makeMetadataFetch(responseBody) },
});

const db = client.database("GMT_Web.fmp12");
await expect(db.getMetadata()).rejects.toThrow('Metadata for database "GMT_Web.fmp12" not found in response');
});

it("works with { format: 'json' } explicit argument as well", async () => {
const responseBody = {
$Version: "4.01",
GMT_Web: SAMPLE_METADATA,
};

const client = new FMServerConnection({
serverUrl: "https://api.example.com",
auth: { apiKey: "test" },
fetchClientOptions: { fetchHandler: makeMetadataFetch(responseBody) },
});

const db = client.database("GMT_Web.fmp12");
const metadata = await db.getMetadata({ format: "json" });

expect(metadata).toEqual(SAMPLE_METADATA);
});
});
Loading
Loading