diff --git a/.changeset/seven-moose-yell.md b/.changeset/seven-moose-yell.md new file mode 100644 index 00000000..26cfde5e --- /dev/null +++ b/.changeset/seven-moose-yell.md @@ -0,0 +1,5 @@ +--- +"@proofkit/better-auth": minor +--- + +Change underlying fetch implementation diff --git a/packages/better-auth/package.json b/packages/better-auth/package.json index b5da48a0..b288dc8f 100644 --- a/packages/better-auth/package.json +++ b/packages/better-auth/package.json @@ -46,8 +46,6 @@ "dependencies": { "@babel/preset-react": "^7.27.1", "@babel/preset-typescript": "^7.27.1", - "@better-fetch/fetch": "1.1.17", - "@better-fetch/logger": "^1.1.18", "@commander-js/extra-typings": "^14.0.0", "@tanstack/vite-config": "^0.2.0", "better-auth": "^1.2.10", @@ -57,6 +55,7 @@ "dotenv": "^16.5.0", "fs-extra": "^11.3.0", "neverthrow": "^8.2.0", + "odata-query": "^8.0.4", "prompts": "^2.4.2", "vite": "^6.3.4", "zod": "3.25.64" @@ -64,6 +63,7 @@ "devDependencies": { "@types/fs-extra": "^11.0.4", "@types/prompts": "^2.4.9", + "@vitest/ui": "^3.2.4", "fm-odata-client": "^3.0.1", "publint": "^0.3.12", "typescript": "^5.9.2", diff --git a/packages/better-auth/src/adapter.ts b/packages/better-auth/src/adapter.ts index ff106e7b..c4c91cb1 100644 --- a/packages/better-auth/src/adapter.ts +++ b/packages/better-auth/src/adapter.ts @@ -3,9 +3,10 @@ import { createAdapter, type AdapterDebugLogs, } from "better-auth/adapters"; -import { createFmOdataFetch, type FmOdataConfig } from "./odata"; +import { createRawFetch, type FmOdataConfig } from "./odata"; import { prettifyError, z } from "zod/v4"; import { logger } from "better-auth"; +import buildQuery from "odata-query"; const configSchema = z.object({ debugLogs: z.unknown().optional(), @@ -153,7 +154,7 @@ export const FileMakerAdapter = ( } const config = parsed.data; - const fetch = createFmOdataFetch({ + const { fetch, baseURL } = createRawFetch({ ...config.odata, logging: config.debugLogs ? "verbose" : "none", }); @@ -192,114 +193,177 @@ export const FileMakerAdapter = ( count: async ({ model, where }) => { const filter = parseWhere(where); logger.debug("$filter", filter); - const result = await fetch(`/${model}/$count`, { + + const query = buildQuery({ + filter: filter.length > 0 ? filter : undefined, + }); + + const result = await fetch(`/${model}/$count${query}`, { method: "GET", - query: { - $filter: filter, - }, output: z.object({ value: z.number() }), }); if (!result.data) { throw new Error("Failed to count records"); } - return result.data?.value ?? 0; + return (result.data?.value as any) ?? 0; }, findOne: async ({ model, where }) => { const filter = parseWhere(where); logger.debug("$filter", filter); - const result = await fetch(`/${model}`, { + + const query = buildQuery({ + top: 1, + filter: filter.length > 0 ? filter : undefined, + }); + + const result = await fetch(`/${model}${query}`, { method: "GET", - query: { - ...(filter.length > 0 ? { $filter: filter } : {}), - $top: 1, - }, output: z.object({ value: z.array(z.any()) }), }); if (result.error) { throw new Error("Failed to find record"); } - return result.data?.value?.[0] ?? null; + return (result.data?.value?.[0] as any) ?? null; }, findMany: async ({ model, where, limit, offset, sortBy }) => { const filter = parseWhere(where); - logger.debug("$filter", filter); + logger.debug("FIND MANY", { where, filter }); + + const query = buildQuery({ + top: limit, + skip: offset, + orderBy: sortBy + ? `${sortBy.field} ${sortBy.direction ?? "asc"}` + : undefined, + filter: filter.length > 0 ? filter : undefined, + }); + logger.debug("QUERY", query); - const rows = await fetch(`/${model}`, { + const result = await fetch(`/${model}${query}`, { method: "GET", - query: { - ...(filter.length > 0 ? { $filter: filter } : {}), - $top: limit, - $skip: offset, - ...(sortBy - ? { $orderby: `"${sortBy.field}" ${sortBy.direction ?? "asc"}` } - : {}), - }, output: z.object({ value: z.array(z.any()) }), }); - if (rows.error) { + logger.debug("RESULT", result); + + if (result.error) { throw new Error("Failed to find records"); } - return rows.data?.value ?? []; + + return (result.data?.value as any) ?? []; }, delete: async ({ model, where }) => { const filter = parseWhere(where); + console.log("DELETE", { model, where, filter }); logger.debug("$filter", filter); - console.log("delete", model, where, filter); - const result = await fetch(`/${model}`, { + + // Find a single id matching the filter + const query = buildQuery({ + top: 1, + select: [`"id"`], + filter: filter.length > 0 ? filter : undefined, + }); + + const toDelete = await fetch(`/${model}${query}`, { + method: "GET", + output: z.object({ value: z.array(z.object({ id: z.string() })) }), + }); + + const id = toDelete.data?.value?.[0]?.id; + if (!id) { + // Nothing to delete + return; + } + + const result = await fetch(`/${model}('${id}')`, { method: "DELETE", - query: { - ...(where.length > 0 ? { $filter: filter } : {}), - $top: 1, - }, }); if (result.error) { + console.log("DELETE ERROR", result.error); throw new Error("Failed to delete record"); } }, deleteMany: async ({ model, where }) => { const filter = parseWhere(where); - logger.debug( - where - .map((o) => `typeof ${o.value} is ${typeof o.value}`) - .join("\n"), - ); - logger.debug("$filter", filter); + console.log("DELETE MANY", { model, where, filter }); - const result = await fetch(`/${model}/$count`, { - method: "DELETE", - query: { - ...(where.length > 0 ? { $filter: filter } : {}), - }, - output: z.coerce.number(), + // Find all ids matching the filter + const query = buildQuery({ + select: [`"id"`], + filter: filter.length > 0 ? filter : undefined, }); - if (result.error) { - throw new Error("Failed to delete record"); + + const rows = await fetch(`/${model}${query}`, { + method: "GET", + output: z.object({ value: z.array(z.object({ id: z.string() })) }), + }); + + const ids = rows.data?.value?.map((r: any) => r.id) ?? []; + let deleted = 0; + for (const id of ids) { + const res = await fetch(`/${model}('${id}')`, { + method: "DELETE", + }); + if (!res.error) deleted++; } - return result.data ?? 0; + return deleted; }, update: async ({ model, where, update }) => { - const result = await fetch(`/${model}`, { + const filter = parseWhere(where); + logger.debug("UPDATE", { model, where, update }); + logger.debug("$filter", filter); + // Find one id to update + const query = buildQuery({ + select: [`"id"`], + filter: filter.length > 0 ? filter : undefined, + }); + + const existing = await fetch(`/${model}${query}`, { + method: "GET", + output: z.object({ value: z.array(z.object({ id: z.string() })) }), + }); + logger.debug("EXISTING", existing.data); + + const id = existing.data?.value?.[0]?.id; + if (!id) return null; + + const patchRes = await fetch(`/${model}('${id}')`, { method: "PATCH", - query: { - ...(where.length > 0 ? { $filter: parseWhere(where) } : {}), - $top: 1, - $select: [`"id"`], - }, body: update, - output: z.object({ value: z.array(z.any()) }), }); - return result.data?.value?.[0] ?? null; + logger.debug("PATCH RES", patchRes.data); + if (patchRes.error) return null; + + // Read back the updated record + const readBack = await fetch(`/${model}('${id}')`, { + method: "GET", + output: z.record(z.string(), z.unknown()), + }); + logger.debug("READ BACK", readBack.data); + return (readBack.data as any) ?? null; }, updateMany: async ({ model, where, update }) => { const filter = parseWhere(where); - const result = await fetch(`/${model}`, { - method: "PATCH", - query: { - ...(where.length > 0 ? { $filter: filter } : {}), - }, - body: update, + // Find all ids matching the filter + const query = buildQuery({ + select: [`"id"`], + filter: filter.length > 0 ? filter : undefined, }); - return result.data as any; + + const rows = await fetch(`/${model}${query}`, { + method: "GET", + output: z.object({ value: z.array(z.object({ id: z.string() })) }), + }); + + const ids = rows.data?.value?.map((r: any) => r.id) ?? []; + let updated = 0; + for (const id of ids) { + const res = await fetch(`/${model}('${id}')`, { + method: "PATCH", + body: update, + }); + if (!res.error) updated++; + } + return updated as any; }, }; }, diff --git a/packages/better-auth/src/cli/index.ts b/packages/better-auth/src/cli/index.ts index 94f17391..8bfad9b1 100644 --- a/packages/better-auth/src/cli/index.ts +++ b/packages/better-auth/src/cli/index.ts @@ -13,7 +13,7 @@ import { logger } from "better-auth"; import prompts from "prompts"; import chalk from "chalk"; import { AdapterOptions } from "../adapter"; -import { createFmOdataFetch } from "../odata"; +import { createRawFetch } from "../odata"; import "dotenv/config"; async function main() { @@ -64,7 +64,7 @@ async function main() { const betterAuthSchema = getAuthTables(config); const adapterConfig = (adapter.options as AdapterOptions).config; - const fetch = createFmOdataFetch({ + const { fetch } = createRawFetch({ ...adapterConfig.odata, auth: // If the username and password are provided in the CLI, use them to authenticate instead of what's in the config file. @@ -74,6 +74,7 @@ async function main() { password: options.password, } : adapterConfig.odata.auth, + logging: "verbose", // Enable logging for CLI operations }); const migrationPlan = await planMigration( diff --git a/packages/better-auth/src/migrate.ts b/packages/better-auth/src/migrate.ts index 82e2dfab..a273d92b 100644 --- a/packages/better-auth/src/migrate.ts +++ b/packages/better-auth/src/migrate.ts @@ -2,10 +2,10 @@ import { type BetterAuthDbSchema } from "better-auth/db"; import { type Metadata } from "fm-odata-client"; import chalk from "chalk"; import z from "zod/v4"; -import { createFmOdataFetch } from "./odata"; +import { createRawFetch } from "./odata"; export async function getMetadata( - fetch: ReturnType, + fetch: ReturnType["fetch"], databaseName: string, ) { console.log("getting metadata..."); @@ -21,11 +21,16 @@ export async function getMetadata( .catch(null), }); + if (result.error) { + console.error("Failed to get metadata:", result.error); + return null; + } + return (result.data?.[databaseName] ?? null) as Metadata | null; } export async function planMigration( - fetch: ReturnType, + fetch: ReturnType["fetch"], betterAuthSchema: BetterAuthDbSchema, databaseName: string, ): Promise { @@ -156,24 +161,41 @@ export async function planMigration( } export async function executeMigration( - fetch: ReturnType, + fetch: ReturnType["fetch"], migrationPlan: MigrationPlan, ) { for (const step of migrationPlan) { if (step.operation === "create") { console.log("Creating table:", step.tableName); - await fetch("@post/FileMaker_Tables", { + const result = await fetch("/FileMaker_Tables", { + method: "POST", body: { tableName: step.tableName, fields: step.fields, }, }); + + if (result.error) { + console.error( + `Failed to create table ${step.tableName}:`, + result.error, + ); + throw new Error(`Migration failed: ${result.error}`); + } } else if (step.operation === "update") { console.log("Adding fields to table:", step.tableName); - await fetch("@post/FileMaker_Tables/:tableName", { - params: { tableName: step.tableName }, + const result = await fetch(`/FileMaker_Tables/${step.tableName}`, { + method: "PATCH", body: { fields: step.fields }, }); + + if (result.error) { + console.error( + `Failed to update table ${step.tableName}:`, + result.error, + ); + throw new Error(`Migration failed: ${result.error}`); + } } } } diff --git a/packages/better-auth/src/odata/index.ts b/packages/better-auth/src/odata/index.ts index 257e8abb..19db591f 100644 --- a/packages/better-auth/src/odata/index.ts +++ b/packages/better-auth/src/odata/index.ts @@ -1,5 +1,3 @@ -import { createFetch, createSchema } from "@better-fetch/fetch"; -import { logger } from "@better-fetch/logger"; import { logger as betterAuthLogger } from "better-auth"; import { err, ok, Result } from "neverthrow"; import { z } from "zod/v4"; @@ -20,83 +18,246 @@ export type FmOdataConfig = { logging?: true | "verbose" | "none"; }; -const schema = createSchema({ - /** - * Create a new table - */ - "@post/FileMaker_Tables": { - input: z.object({ tableName: z.string(), fields: z.array(z.any()) }), - }, - /** - * Add fields to a table - */ - "@patch/FileMaker_Tables/:tableName": { - params: z.object({ tableName: z.string() }), - input: z.object({ fields: z.array(z.any()) }), - }, - /** - * Delete a table - */ - "@delete/FileMaker_Tables/:tableName": { - params: z.object({ tableName: z.string() }), - }, - /** - * Delete a field from a table - */ - "@delete/FileMaker_Tables/:tableName/:fieldName": { - params: z.object({ tableName: z.string(), fieldName: z.string() }), - }, -}); - -export function createFmOdataFetch(args: FmOdataConfig) { +export function validateUrl(input: string): Result { + try { + const url = new URL(input); + return ok(url); + } catch (error) { + return err(error); + } +} + +export function createRawFetch(args: FmOdataConfig) { const result = validateUrl(args.serverUrl); if (result.isErr()) { throw new Error("Invalid server URL"); } + let baseURL = result.value.origin; if ("apiKey" in args.auth) { baseURL += `/otto`; } baseURL += `/fmi/odata/v4/${args.database}`; - return createFetch({ - baseURL, - auth: - "apiKey" in args.auth - ? { type: "Bearer", token: args.auth.apiKey } - : { - type: "Basic", - username: args.auth.username, - password: args.auth.password, - }, - onError: (error) => { - console.error("url", error.request.url.toString()); - console.log(error.error); - console.log("error.request.body", JSON.stringify(error.request.body)); + // Create authentication headers + const authHeaders: Record = {}; + if ("apiKey" in args.auth) { + authHeaders.Authorization = `Bearer ${args.auth.apiKey}`; + } else { + const credentials = btoa(`${args.auth.username}:${args.auth.password}`); + authHeaders.Authorization = `Basic ${credentials}`; + } + + // Enhanced fetch function with body handling, validation, and structured responses + const wrappedFetch = async ( + input: string | URL | Request, + options?: Omit & { + body?: any; // Allow any type for body + output?: z.ZodSchema; // Optional schema for validation }, - schema, - plugins: [ - logger({ - verbose: args.logging === "verbose", - enabled: args.logging === "verbose" || !!args.logging, - console: { - fail: (...args) => betterAuthLogger.error("better-fetch", ...args), - success: (...args) => betterAuthLogger.info("better-fetch", ...args), - log: (...args) => betterAuthLogger.info("better-fetch", ...args), - error: (...args) => betterAuthLogger.error("better-fetch", ...args), - warn: (...args) => betterAuthLogger.warn("better-fetch", ...args), - }, - }), - ], - }); -} + ): Promise<{ data?: TOutput; error?: string; response?: Response }> => { + try { + let url: string; -export function validateUrl(input: string): Result { - try { - const url = new URL(input); - return ok(url); - } catch (error) { - return err(error); - } + // Handle different input types + if (typeof input === "string") { + // If it's already a full URL, use as-is, otherwise prepend baseURL + url = input.startsWith("http") + ? input + : `${baseURL}${input.startsWith("/") ? input : `/${input}`}`; + } else if (input instanceof URL) { + url = input.toString(); + } else if (input instanceof Request) { + url = input.url; + } else { + url = String(input); + } + + // Handle body serialization + let processedBody = options?.body; + if ( + processedBody && + typeof processedBody === "object" && + !(processedBody instanceof FormData) && + !(processedBody instanceof URLSearchParams) && + !(processedBody instanceof ReadableStream) + ) { + processedBody = JSON.stringify(processedBody); + } + + // Merge headers + const headers = { + "Content-Type": "application/json", + ...authHeaders, + ...(options?.headers || {}), + }; + + const requestInit: RequestInit = { + ...options, + headers, + body: processedBody, + }; + + // Optional logging + if (args.logging === "verbose" || args.logging === true) { + betterAuthLogger.info( + "raw-fetch", + `${requestInit.method || "GET"} ${url}`, + ); + if (requestInit.body) { + betterAuthLogger.info("raw-fetch", "Request body:", requestInit.body); + } + } + + const response = await fetch(url, requestInit); + + // Optional logging for response details + if (args.logging === "verbose" || args.logging === true) { + betterAuthLogger.info( + "raw-fetch", + `Response status: ${response.status} ${response.statusText}`, + ); + betterAuthLogger.info( + "raw-fetch", + `Response headers:`, + Object.fromEntries(response.headers.entries()), + ); + } + + // Check if response is ok + if (!response.ok) { + const errorText = await response.text().catch(() => "Unknown error"); + if (args.logging === "verbose" || args.logging === true) { + betterAuthLogger.error( + "raw-fetch", + `HTTP Error ${response.status}: ${errorText}`, + ); + } + return { + error: `HTTP ${response.status}: ${errorText}`, + response, + }; + } + + // Parse response based on content type + let responseData: any; + const contentType = response.headers.get("content-type"); + + if (args.logging === "verbose" || args.logging === true) { + betterAuthLogger.info( + "raw-fetch", + `Response content-type: ${contentType || "none"}`, + ); + } + + if (contentType?.includes("application/json")) { + try { + const responseText = await response.text(); + if (args.logging === "verbose" || args.logging === true) { + betterAuthLogger.info( + "raw-fetch", + `Raw response text: "${responseText}"`, + ); + betterAuthLogger.info( + "raw-fetch", + `Response text length: ${responseText.length}`, + ); + } + + // Handle empty responses + if (responseText.trim() === "") { + if (args.logging === "verbose" || args.logging === true) { + betterAuthLogger.info( + "raw-fetch", + "Empty JSON response, returning null", + ); + } + responseData = null; + } else { + responseData = JSON.parse(responseText); + if (args.logging === "verbose" || args.logging === true) { + betterAuthLogger.info( + "raw-fetch", + "Successfully parsed JSON response", + ); + } + } + } catch (parseError) { + if (args.logging === "verbose" || args.logging === true) { + betterAuthLogger.error( + "raw-fetch", + "JSON parse error:", + parseError, + ); + } + return { + error: `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : "Unknown parse error"}`, + response, + }; + } + } else if (contentType?.includes("text/")) { + // Handle text responses (text/plain, text/html, etc.) + responseData = await response.text(); + if (args.logging === "verbose" || args.logging === true) { + betterAuthLogger.info( + "raw-fetch", + `Text response: "${responseData}"`, + ); + } + } else { + // For other content types, try to get text but don't fail if it's binary + try { + responseData = await response.text(); + if (args.logging === "verbose" || args.logging === true) { + betterAuthLogger.info( + "raw-fetch", + `Unknown content-type response as text: "${responseData}"`, + ); + } + } catch { + // If text parsing fails (e.g., binary data), return null + responseData = null; + if (args.logging === "verbose" || args.logging === true) { + betterAuthLogger.info( + "raw-fetch", + "Could not parse response as text, returning null", + ); + } + } + } + + // Validate output if schema provided + if (options?.output) { + const validation = options.output.safeParse(responseData); + if (validation.success) { + return { + data: validation.data, + response, + }; + } else { + return { + error: `Validation failed: ${validation.error.message}`, + response, + }; + } + } + + // Return unvalidated data + return { + data: responseData as TOutput, + response, + }; + } catch (error) { + return { + error: + error instanceof Error ? error.message : "Unknown error occurred", + }; + } + }; + + return { + baseURL, + fetch: wrappedFetch, + }; } diff --git a/packages/better-auth/tests/adapter.test.ts b/packages/better-auth/tests/adapter.test.ts index b31543dd..1acf1ce8 100644 --- a/packages/better-auth/tests/adapter.test.ts +++ b/packages/better-auth/tests/adapter.test.ts @@ -1,7 +1,8 @@ import { describe, beforeAll, it, expect } from "vitest"; import { runAdapterTest } from "better-auth/adapters/test"; import { FileMakerAdapter } from "../src"; -import { createFmOdataFetch } from "../src/odata"; +import { createRawFetch } from "../src/odata"; +import { z } from "zod/v4"; if (!process.env.FM_SERVER) { throw new Error("FM_SERVER is not set"); @@ -16,25 +17,42 @@ if (!process.env.FM_PASSWORD) { throw new Error("FM_PASSWORD is not set"); } -const fetch = createFmOdataFetch({ +const { fetch } = createRawFetch({ serverUrl: process.env.FM_SERVER, auth: { username: process.env.FM_USERNAME, password: process.env.FM_PASSWORD, }, database: process.env.FM_DATABASE, + logging: "verbose", // Enable verbose logging to see the response details }); describe("My Adapter Tests", async () => { beforeAll(async () => { // reset the database for (const table of ["user", "session", "account", "verification"]) { - await fetch(`/${table}`, { - method: "DELETE", - query: { - $filter: `"id" ne '0'`, - }, + const result = await fetch(`/${table}`, { + output: z.object({ value: z.array(z.any()) }), }); + + if (result.error) { + console.log("Error fetching records:", result.error); + continue; + } + + const records = result.data?.value || []; + for (const record of records) { + const deleteResult = await fetch(`/${table}('${record.id}')`, { + method: "DELETE", + }); + + if (deleteResult.error) { + console.log( + `Error deleting record ${record.id}:`, + deleteResult.error, + ); + } + } } }); @@ -57,42 +75,71 @@ describe("My Adapter Tests", async () => { return adapter(betterAuthOptions); }, }); + + it("should sort descending", async () => { + const result = await adapter({}).findMany({ + model: "verification", + where: [ + { + field: "identifier", + operator: "eq", + value: "zyzaUHEsETWiuORCCdyguVVlVPcnduXk", + }, + ], + limit: 1, + sortBy: { direction: "desc", field: "createdAt" }, + }); + + console.log(result); + + // expect(result.data).toHaveLength(1); + }); }); it("should properly filter by dates", async () => { - // delete all users - await fetch(`/user`, { + // delete all users - using buildQuery to construct the filter properly + const deleteAllResult = await fetch(`/user?$filter="id" ne '0'`, { method: "DELETE", - query: { - $filter: `"id" ne '0'`, - }, }); + if (deleteAllResult.error) { + console.log("Error deleting all users:", deleteAllResult.error); + } + // create user const date = new Date("2025-01-10").toISOString(); - await fetch(`/user`, { + const createResult = await fetch(`/user`, { method: "POST", body: { id: "filter-test", createdAt: date, }, - throw: true, + output: z.object({ id: z.string() }), }); - const result = await fetch(`/user`, { + if (createResult.error) { + throw new Error(`Failed to create user: ${createResult.error}`); + } + + const result = await fetch(`/user?$filter=createdAt ge 2025-01-05`, { method: "GET", - query: { - $filter: `createdAt ge 2025-01-05`, - }, + output: z.object({ value: z.array(z.any()) }), }); console.log(result); + if (result.error) { + throw new Error(`Failed to fetch users: ${result.error}`); + } + expect(result.data?.value).toHaveLength(1); // delete record - await fetch(`/user('filter-test')`, { + const deleteResult = await fetch(`/user('filter-test')`, { method: "DELETE", - throw: true, }); + + if (deleteResult.error) { + console.log("Error deleting test record:", deleteResult.error); + } }); diff --git a/packages/better-auth/tests/migrate.test.ts b/packages/better-auth/tests/migrate.test.ts index f1ba6e51..60ce5291 100644 --- a/packages/better-auth/tests/migrate.test.ts +++ b/packages/better-auth/tests/migrate.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { createFmOdataFetch } from "../src/odata"; +import { createRawFetch } from "../src/odata"; import { getMetadata } from "../src/migrate"; if (!process.env.FM_SERVER) { @@ -12,12 +12,13 @@ if (!process.env.OTTO_API_KEY) { throw new Error("OTTO_API_KEY is not set"); } -const fetch = createFmOdataFetch({ +const { fetch } = createRawFetch({ serverUrl: process.env.FM_SERVER, auth: { apiKey: process.env.OTTO_API_KEY, }, database: process.env.FM_DATABASE, + logging: "verbose", }); describe("migrate", () => { @@ -29,11 +30,16 @@ describe("migrate", () => { it("can create/update/delete a table", async () => { const tableName = "test_table"; - await fetch("@delete/FileMaker_Tables/:tableName", { - params: { tableName }, + + // Delete table if it exists (cleanup) + const deleteResult = await fetch(`/FileMaker_Tables/${tableName}`, { + method: "DELETE", }); + // Don't throw on delete errors as table might not exist - await fetch("@post/FileMaker_Tables", { + // Create table + const createResult = await fetch("/FileMaker_Tables", { + method: "POST", body: { tableName, fields: [ @@ -44,12 +50,15 @@ describe("migrate", () => { }, ], }, - throw: true, }); - await fetch("@patch/FileMaker_Tables/:tableName", { - params: { tableName }, + if (createResult.error) { + throw new Error(`Failed to create table: ${createResult.error}`); + } + // Add field to table + const updateResult = await fetch(`/FileMaker_Tables/${tableName}`, { + method: "PATCH", body: { fields: [ { @@ -58,17 +67,31 @@ describe("migrate", () => { }, ], }, - throw: true, }); - await fetch("@delete/FileMaker_Tables/:tableName/:fieldName", { - params: { tableName, fieldName: "Phone" }, - throw: true, - }); + if (updateResult.error) { + throw new Error(`Failed to update table: ${updateResult.error}`); + } + + // Delete field from table + const deleteFieldResult = await fetch( + `/FileMaker_Tables/${tableName}/Phone`, + { + method: "DELETE", + }, + ); + + if (deleteFieldResult.error) { + throw new Error(`Failed to delete field: ${deleteFieldResult.error}`); + } - await fetch("@delete/FileMaker_Tables/:tableName", { - params: { tableName }, - throw: true, + // Delete table + const deleteTableResult = await fetch(`/FileMaker_Tables/${tableName}`, { + method: "DELETE", }); + + if (deleteTableResult.error) { + throw new Error(`Failed to delete table: ${deleteTableResult.error}`); + } }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b8a899c..bd87f32a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,7 +31,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) apps/demo: dependencies: @@ -107,7 +107,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) apps/docs: dependencies: @@ -225,7 +225,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@1.21.7)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@1.21.7)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) packages/better-auth: dependencies: @@ -235,12 +235,6 @@ importers: '@babel/preset-typescript': specifier: ^7.27.1 version: 7.27.1(@babel/core@7.27.7) - '@better-fetch/fetch': - specifier: 1.1.17 - version: 1.1.17 - '@better-fetch/logger': - specifier: ^1.1.18 - version: 1.1.18 '@commander-js/extra-typings': specifier: ^14.0.0 version: 14.0.0(commander@14.0.0) @@ -268,6 +262,9 @@ importers: neverthrow: specifier: ^8.2.0 version: 8.2.0 + odata-query: + specifier: ^8.0.4 + version: 8.0.4 prompts: specifier: ^2.4.2 version: 2.4.2 @@ -284,6 +281,9 @@ importers: '@types/prompts': specifier: ^2.4.9 version: 2.4.9 + '@vitest/ui': + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4) fm-odata-client: specifier: ^3.0.1 version: 3.0.1 @@ -295,7 +295,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) packages/cli: dependencies: @@ -434,7 +434,7 @@ importers: version: 7.7.0 '@vitest/coverage-v8': specifier: ^1.4.0 - version: 1.6.1(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@1.21.7)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0)) + version: 1.6.1(vitest@3.2.4) drizzle-kit: specifier: ^0.21.4 version: 0.21.4 @@ -482,7 +482,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@1.21.7)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@1.21.7)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) zod: specifier: 3.25.64 version: 3.25.64 @@ -561,7 +561,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) packages/fmodata: {} @@ -632,7 +632,7 @@ importers: version: 5.9.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) packages/webviewer: dependencies: @@ -925,9 +925,6 @@ packages: '@better-fetch/fetch@1.1.18': resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} - '@better-fetch/logger@1.1.18': - resolution: {integrity: sha512-jiBJefQhU1tCzG5HQ+Qez0/Kn5qKO4t5e1cocdNBPRXrzKHPqwW8TbD9p4u0iKYVG3knT5GR8R8DdesVJdiLSQ==} - '@braidai/lang@1.1.1': resolution: {integrity: sha512-5uM+no3i3DafVgkoW7ayPhEGHNNBZCSj5TrGDQt0ayEKQda5f3lAXlmQg0MR5E0gKgmTzUUEtSWHsEC3h9jUcg==} @@ -2436,6 +2433,9 @@ packages: resolution: {integrity: sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA==} engines: {node: '>=16'} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@prisma/adapter-planetscale@5.22.0': resolution: {integrity: sha512-4fffELMJCAsvLaO4E4YKw6SsX8z3524f0th8dgagr4/p4PQwOJa8wQUktC3DXZdUGG0jyQUZF9ZYPM5e18UB+A==} peerDependencies: @@ -3694,6 +3694,11 @@ packages: '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/ui@3.2.4': + resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} + peerDependencies: + vitest: 3.2.4 + '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} @@ -6389,6 +6394,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -6590,6 +6599,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + odata-query@8.0.4: + resolution: {integrity: sha512-v66MVxAZxlmOlFVaC9gvcDX5OcHO6yqc08AXhNhQ9LMbSzJKJ88uY1a7uDmLw2u4oMPGOMjnb8jdimA4kOD4Rw==} + ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -7371,6 +7383,10 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -7696,6 +7712,10 @@ packages: resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} engines: {node: '>=14.16'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -8644,10 +8664,6 @@ snapshots: '@better-fetch/fetch@1.1.18': {} - '@better-fetch/logger@1.1.18': - dependencies: - consola: 3.4.2 - '@braidai/lang@1.1.1': {} '@bundled-es-modules/cookie@2.0.1': @@ -9895,6 +9911,8 @@ snapshots: '@planetscale/database@1.19.0': {} + '@polka/url@1.0.0-next.29': {} + '@prisma/adapter-planetscale@5.22.0(@planetscale/database@1.19.0)': dependencies: '@planetscale/database': 1.19.0 @@ -11142,7 +11160,7 @@ snapshots: dependencies: crypto-js: 4.2.0 - '@vitest/coverage-v8@1.6.1(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@1.21.7)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0))': + '@vitest/coverage-v8@1.6.1(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -11157,7 +11175,7 @@ snapshots: std-env: 3.9.0 strip-literal: 2.1.1 test-exclude: 6.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@1.21.7)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@1.21.7)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -11207,6 +11225,17 @@ snapshots: dependencies: tinyspy: 4.0.3 + '@vitest/ui@3.2.4(vitest@3.2.4)': + dependencies: + '@vitest/utils': 3.2.4 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.1 + tinyglobby: 0.2.14 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0) + '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -14536,6 +14565,8 @@ snapshots: mri@1.2.0: {} + mrmime@2.0.1: {} + ms@2.1.3: {} msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2): @@ -14757,6 +14788,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + odata-query@8.0.4: + dependencies: + tslib: 2.8.1 + ohash@2.0.11: {} oidc-token-hash@5.1.0: {} @@ -15704,6 +15739,12 @@ snapshots: is-arrayish: 0.3.2 optional: true + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + sisteransi@1.0.5: {} skin-tone@2.0.0: @@ -16046,6 +16087,8 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + totalist@3.0.1: {} + tough-cookie@4.1.4: dependencies: psl: 1.15.0 @@ -16475,7 +16518,7 @@ snapshots: tsx: 4.20.3 yaml: 2.8.0 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@1.21.7)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@1.21.7)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -16503,6 +16546,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.17.1 + '@vitest/ui': 3.2.4(vitest@3.2.4) happy-dom: 15.11.7 transitivePeerDependencies: - jiti @@ -16518,7 +16562,7 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.17.1)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@2.4.2)(lightningcss@1.30.1)(msw@2.10.2(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -16546,6 +16590,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.17.1 + '@vitest/ui': 3.2.4(vitest@3.2.4) happy-dom: 15.11.7 transitivePeerDependencies: - jiti