@proofkit/better-auth 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/migrate.ts CHANGED
@@ -2,10 +2,10 @@ import { type BetterAuthDbSchema } from "better-auth/db";
2
2
  import { type Metadata } from "fm-odata-client";
3
3
  import chalk from "chalk";
4
4
  import z from "zod/v4";
5
- import { createFmOdataFetch } from "./odata";
5
+ import { createRawFetch } from "./odata";
6
6
 
7
7
  export async function getMetadata(
8
- fetch: ReturnType<typeof createFmOdataFetch>,
8
+ fetch: ReturnType<typeof createRawFetch>["fetch"],
9
9
  databaseName: string,
10
10
  ) {
11
11
  console.log("getting metadata...");
@@ -21,11 +21,16 @@ export async function getMetadata(
21
21
  .catch(null),
22
22
  });
23
23
 
24
+ if (result.error) {
25
+ console.error("Failed to get metadata:", result.error);
26
+ return null;
27
+ }
28
+
24
29
  return (result.data?.[databaseName] ?? null) as Metadata | null;
25
30
  }
26
31
 
27
32
  export async function planMigration(
28
- fetch: ReturnType<typeof createFmOdataFetch>,
33
+ fetch: ReturnType<typeof createRawFetch>["fetch"],
29
34
  betterAuthSchema: BetterAuthDbSchema,
30
35
  databaseName: string,
31
36
  ): Promise<MigrationPlan> {
@@ -156,24 +161,41 @@ export async function planMigration(
156
161
  }
157
162
 
158
163
  export async function executeMigration(
159
- fetch: ReturnType<typeof createFmOdataFetch>,
164
+ fetch: ReturnType<typeof createRawFetch>["fetch"],
160
165
  migrationPlan: MigrationPlan,
161
166
  ) {
162
167
  for (const step of migrationPlan) {
163
168
  if (step.operation === "create") {
164
169
  console.log("Creating table:", step.tableName);
165
- await fetch("@post/FileMaker_Tables", {
170
+ const result = await fetch("/FileMaker_Tables", {
171
+ method: "POST",
166
172
  body: {
167
173
  tableName: step.tableName,
168
174
  fields: step.fields,
169
175
  },
170
176
  });
177
+
178
+ if (result.error) {
179
+ console.error(
180
+ `Failed to create table ${step.tableName}:`,
181
+ result.error,
182
+ );
183
+ throw new Error(`Migration failed: ${result.error}`);
184
+ }
171
185
  } else if (step.operation === "update") {
172
186
  console.log("Adding fields to table:", step.tableName);
173
- await fetch("@post/FileMaker_Tables/:tableName", {
174
- params: { tableName: step.tableName },
187
+ const result = await fetch(`/FileMaker_Tables/${step.tableName}`, {
188
+ method: "PATCH",
175
189
  body: { fields: step.fields },
176
190
  });
191
+
192
+ if (result.error) {
193
+ console.error(
194
+ `Failed to update table ${step.tableName}:`,
195
+ result.error,
196
+ );
197
+ throw new Error(`Migration failed: ${result.error}`);
198
+ }
177
199
  }
178
200
  }
179
201
  }
@@ -1,5 +1,3 @@
1
- import { createFetch, createSchema } from "@better-fetch/fetch";
2
- import { logger } from "@better-fetch/logger";
3
1
  import { logger as betterAuthLogger } from "better-auth";
4
2
  import { err, ok, Result } from "neverthrow";
5
3
  import { z } from "zod/v4";
@@ -20,83 +18,246 @@ export type FmOdataConfig = {
20
18
  logging?: true | "verbose" | "none";
21
19
  };
22
20
 
23
- const schema = createSchema({
24
- /**
25
- * Create a new table
26
- */
27
- "@post/FileMaker_Tables": {
28
- input: z.object({ tableName: z.string(), fields: z.array(z.any()) }),
29
- },
30
- /**
31
- * Add fields to a table
32
- */
33
- "@patch/FileMaker_Tables/:tableName": {
34
- params: z.object({ tableName: z.string() }),
35
- input: z.object({ fields: z.array(z.any()) }),
36
- },
37
- /**
38
- * Delete a table
39
- */
40
- "@delete/FileMaker_Tables/:tableName": {
41
- params: z.object({ tableName: z.string() }),
42
- },
43
- /**
44
- * Delete a field from a table
45
- */
46
- "@delete/FileMaker_Tables/:tableName/:fieldName": {
47
- params: z.object({ tableName: z.string(), fieldName: z.string() }),
48
- },
49
- });
50
-
51
- export function createFmOdataFetch(args: FmOdataConfig) {
21
+ export function validateUrl(input: string): Result<URL, unknown> {
22
+ try {
23
+ const url = new URL(input);
24
+ return ok(url);
25
+ } catch (error) {
26
+ return err(error);
27
+ }
28
+ }
29
+
30
+ export function createRawFetch(args: FmOdataConfig) {
52
31
  const result = validateUrl(args.serverUrl);
53
32
 
54
33
  if (result.isErr()) {
55
34
  throw new Error("Invalid server URL");
56
35
  }
36
+
57
37
  let baseURL = result.value.origin;
58
38
  if ("apiKey" in args.auth) {
59
39
  baseURL += `/otto`;
60
40
  }
61
41
  baseURL += `/fmi/odata/v4/${args.database}`;
62
42
 
63
- return createFetch({
64
- baseURL,
65
- auth:
66
- "apiKey" in args.auth
67
- ? { type: "Bearer", token: args.auth.apiKey }
68
- : {
69
- type: "Basic",
70
- username: args.auth.username,
71
- password: args.auth.password,
72
- },
73
- onError: (error) => {
74
- console.error("url", error.request.url.toString());
75
- console.log(error.error);
76
- console.log("error.request.body", JSON.stringify(error.request.body));
43
+ // Create authentication headers
44
+ const authHeaders: Record<string, string> = {};
45
+ if ("apiKey" in args.auth) {
46
+ authHeaders.Authorization = `Bearer ${args.auth.apiKey}`;
47
+ } else {
48
+ const credentials = btoa(`${args.auth.username}:${args.auth.password}`);
49
+ authHeaders.Authorization = `Basic ${credentials}`;
50
+ }
51
+
52
+ // Enhanced fetch function with body handling, validation, and structured responses
53
+ const wrappedFetch = async <TOutput = any>(
54
+ input: string | URL | Request,
55
+ options?: Omit<RequestInit, "body"> & {
56
+ body?: any; // Allow any type for body
57
+ output?: z.ZodSchema<TOutput>; // Optional schema for validation
77
58
  },
78
- schema,
79
- plugins: [
80
- logger({
81
- verbose: args.logging === "verbose",
82
- enabled: args.logging === "verbose" || !!args.logging,
83
- console: {
84
- fail: (...args) => betterAuthLogger.error("better-fetch", ...args),
85
- success: (...args) => betterAuthLogger.info("better-fetch", ...args),
86
- log: (...args) => betterAuthLogger.info("better-fetch", ...args),
87
- error: (...args) => betterAuthLogger.error("better-fetch", ...args),
88
- warn: (...args) => betterAuthLogger.warn("better-fetch", ...args),
89
- },
90
- }),
91
- ],
92
- });
93
- }
59
+ ): Promise<{ data?: TOutput; error?: string; response?: Response }> => {
60
+ try {
61
+ let url: string;
94
62
 
95
- export function validateUrl(input: string): Result<URL, unknown> {
96
- try {
97
- const url = new URL(input);
98
- return ok(url);
99
- } catch (error) {
100
- return err(error);
101
- }
63
+ // Handle different input types
64
+ if (typeof input === "string") {
65
+ // If it's already a full URL, use as-is, otherwise prepend baseURL
66
+ url = input.startsWith("http")
67
+ ? input
68
+ : `${baseURL}${input.startsWith("/") ? input : `/${input}`}`;
69
+ } else if (input instanceof URL) {
70
+ url = input.toString();
71
+ } else if (input instanceof Request) {
72
+ url = input.url;
73
+ } else {
74
+ url = String(input);
75
+ }
76
+
77
+ // Handle body serialization
78
+ let processedBody = options?.body;
79
+ if (
80
+ processedBody &&
81
+ typeof processedBody === "object" &&
82
+ !(processedBody instanceof FormData) &&
83
+ !(processedBody instanceof URLSearchParams) &&
84
+ !(processedBody instanceof ReadableStream)
85
+ ) {
86
+ processedBody = JSON.stringify(processedBody);
87
+ }
88
+
89
+ // Merge headers
90
+ const headers = {
91
+ "Content-Type": "application/json",
92
+ ...authHeaders,
93
+ ...(options?.headers || {}),
94
+ };
95
+
96
+ const requestInit: RequestInit = {
97
+ ...options,
98
+ headers,
99
+ body: processedBody,
100
+ };
101
+
102
+ // Optional logging
103
+ if (args.logging === "verbose" || args.logging === true) {
104
+ betterAuthLogger.info(
105
+ "raw-fetch",
106
+ `${requestInit.method || "GET"} ${url}`,
107
+ );
108
+ if (requestInit.body) {
109
+ betterAuthLogger.info("raw-fetch", "Request body:", requestInit.body);
110
+ }
111
+ }
112
+
113
+ const response = await fetch(url, requestInit);
114
+
115
+ // Optional logging for response details
116
+ if (args.logging === "verbose" || args.logging === true) {
117
+ betterAuthLogger.info(
118
+ "raw-fetch",
119
+ `Response status: ${response.status} ${response.statusText}`,
120
+ );
121
+ betterAuthLogger.info(
122
+ "raw-fetch",
123
+ `Response headers:`,
124
+ Object.fromEntries(response.headers.entries()),
125
+ );
126
+ }
127
+
128
+ // Check if response is ok
129
+ if (!response.ok) {
130
+ const errorText = await response.text().catch(() => "Unknown error");
131
+ if (args.logging === "verbose" || args.logging === true) {
132
+ betterAuthLogger.error(
133
+ "raw-fetch",
134
+ `HTTP Error ${response.status}: ${errorText}`,
135
+ );
136
+ }
137
+ return {
138
+ error: `HTTP ${response.status}: ${errorText}`,
139
+ response,
140
+ };
141
+ }
142
+
143
+ // Parse response based on content type
144
+ let responseData: any;
145
+ const contentType = response.headers.get("content-type");
146
+
147
+ if (args.logging === "verbose" || args.logging === true) {
148
+ betterAuthLogger.info(
149
+ "raw-fetch",
150
+ `Response content-type: ${contentType || "none"}`,
151
+ );
152
+ }
153
+
154
+ if (contentType?.includes("application/json")) {
155
+ try {
156
+ const responseText = await response.text();
157
+ if (args.logging === "verbose" || args.logging === true) {
158
+ betterAuthLogger.info(
159
+ "raw-fetch",
160
+ `Raw response text: "${responseText}"`,
161
+ );
162
+ betterAuthLogger.info(
163
+ "raw-fetch",
164
+ `Response text length: ${responseText.length}`,
165
+ );
166
+ }
167
+
168
+ // Handle empty responses
169
+ if (responseText.trim() === "") {
170
+ if (args.logging === "verbose" || args.logging === true) {
171
+ betterAuthLogger.info(
172
+ "raw-fetch",
173
+ "Empty JSON response, returning null",
174
+ );
175
+ }
176
+ responseData = null;
177
+ } else {
178
+ responseData = JSON.parse(responseText);
179
+ if (args.logging === "verbose" || args.logging === true) {
180
+ betterAuthLogger.info(
181
+ "raw-fetch",
182
+ "Successfully parsed JSON response",
183
+ );
184
+ }
185
+ }
186
+ } catch (parseError) {
187
+ if (args.logging === "verbose" || args.logging === true) {
188
+ betterAuthLogger.error(
189
+ "raw-fetch",
190
+ "JSON parse error:",
191
+ parseError,
192
+ );
193
+ }
194
+ return {
195
+ error: `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : "Unknown parse error"}`,
196
+ response,
197
+ };
198
+ }
199
+ } else if (contentType?.includes("text/")) {
200
+ // Handle text responses (text/plain, text/html, etc.)
201
+ responseData = await response.text();
202
+ if (args.logging === "verbose" || args.logging === true) {
203
+ betterAuthLogger.info(
204
+ "raw-fetch",
205
+ `Text response: "${responseData}"`,
206
+ );
207
+ }
208
+ } else {
209
+ // For other content types, try to get text but don't fail if it's binary
210
+ try {
211
+ responseData = await response.text();
212
+ if (args.logging === "verbose" || args.logging === true) {
213
+ betterAuthLogger.info(
214
+ "raw-fetch",
215
+ `Unknown content-type response as text: "${responseData}"`,
216
+ );
217
+ }
218
+ } catch {
219
+ // If text parsing fails (e.g., binary data), return null
220
+ responseData = null;
221
+ if (args.logging === "verbose" || args.logging === true) {
222
+ betterAuthLogger.info(
223
+ "raw-fetch",
224
+ "Could not parse response as text, returning null",
225
+ );
226
+ }
227
+ }
228
+ }
229
+
230
+ // Validate output if schema provided
231
+ if (options?.output) {
232
+ const validation = options.output.safeParse(responseData);
233
+ if (validation.success) {
234
+ return {
235
+ data: validation.data,
236
+ response,
237
+ };
238
+ } else {
239
+ return {
240
+ error: `Validation failed: ${validation.error.message}`,
241
+ response,
242
+ };
243
+ }
244
+ }
245
+
246
+ // Return unvalidated data
247
+ return {
248
+ data: responseData as TOutput,
249
+ response,
250
+ };
251
+ } catch (error) {
252
+ return {
253
+ error:
254
+ error instanceof Error ? error.message : "Unknown error occurred",
255
+ };
256
+ }
257
+ };
258
+
259
+ return {
260
+ baseURL,
261
+ fetch: wrappedFetch,
262
+ };
102
263
  }