@proofkit/better-auth 0.3.0 → 0.3.1-beta.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
@@ -1,13 +1,10 @@
1
- import { type BetterAuthDbSchema } from "better-auth/db";
2
- import { type Metadata } from "fm-odata-client";
1
+ import type { BetterAuthDbSchema } from "better-auth/db";
3
2
  import chalk from "chalk";
3
+ import type { Metadata } from "fm-odata-client";
4
4
  import z from "zod/v4";
5
- import { createRawFetch } from "./odata";
5
+ import type { createRawFetch } from "./odata";
6
6
 
7
- export async function getMetadata(
8
- fetch: ReturnType<typeof createRawFetch>["fetch"],
9
- databaseName: string,
10
- ) {
7
+ export async function getMetadata(fetch: ReturnType<typeof createRawFetch>["fetch"], databaseName: string) {
11
8
  console.log("getting metadata...");
12
9
  const result = await fetch("/$metadata", {
13
10
  method: "GET",
@@ -37,7 +34,7 @@ export async function planMigration(
37
34
  const metadata = await getMetadata(fetch, databaseName);
38
35
 
39
36
  // Build a map from entity set name to entity type key
40
- let entitySetToType: Record<string, string> = {};
37
+ const entitySetToType: Record<string, string> = {};
41
38
  if (metadata) {
42
39
  for (const [key, value] of Object.entries(metadata)) {
43
40
  if (value.$Kind === "EntitySet" && value.$Type) {
@@ -52,27 +49,32 @@ export async function planMigration(
52
49
  ? Object.entries(entitySetToType).reduce(
53
50
  (acc, [entitySetName, entityTypeKey]) => {
54
51
  const entityType = metadata[entityTypeKey];
55
- if (!entityType) return acc;
52
+ if (!entityType) {
53
+ return acc;
54
+ }
56
55
  const fields = Object.entries(entityType)
57
56
  .filter(
58
- ([fieldKey, fieldValue]) =>
59
- typeof fieldValue === "object" &&
60
- fieldValue !== null &&
61
- "$Type" in fieldValue,
57
+ ([_fieldKey, fieldValue]) =>
58
+ typeof fieldValue === "object" && fieldValue !== null && "$Type" in fieldValue,
62
59
  )
63
- .map(([fieldKey, fieldValue]) => ({
64
- name: fieldKey,
65
- type:
66
- fieldValue.$Type === "Edm.String"
67
- ? "varchar"
68
- : fieldValue.$Type === "Edm.DateTimeOffset"
69
- ? "timestamp"
70
- : fieldValue.$Type === "Edm.Decimal" ||
71
- fieldValue.$Type === "Edm.Int32" ||
72
- fieldValue.$Type === "Edm.Int64"
73
- ? "numeric"
74
- : "varchar",
75
- }));
60
+ .map(([fieldKey, fieldValue]) => {
61
+ let type = "varchar";
62
+ if (fieldValue.$Type === "Edm.String") {
63
+ type = "varchar";
64
+ } else if (fieldValue.$Type === "Edm.DateTimeOffset") {
65
+ type = "timestamp";
66
+ } else if (
67
+ fieldValue.$Type === "Edm.Decimal" ||
68
+ fieldValue.$Type === "Edm.Int32" ||
69
+ fieldValue.$Type === "Edm.Int64"
70
+ ) {
71
+ type = "numeric";
72
+ }
73
+ return {
74
+ name: fieldKey,
75
+ type,
76
+ };
77
+ });
76
78
  acc[entitySetName] = fields;
77
79
  return acc;
78
80
  },
@@ -90,42 +92,24 @@ export async function planMigration(
90
92
  const migrationPlan: MigrationPlan = [];
91
93
 
92
94
  for (const baTable of baTables) {
93
- const fields: FmField[] = Object.entries(baTable.fields).map(
94
- ([key, field]) => ({
95
+ const fields: FmField[] = Object.entries(baTable.fields).map(([key, field]) => {
96
+ let type: "varchar" | "numeric" | "timestamp" = "varchar";
97
+ if (field.type === "boolean" || field.type.includes("number")) {
98
+ type = "numeric";
99
+ } else if (field.type === "date") {
100
+ type = "timestamp";
101
+ }
102
+ return {
95
103
  name: field.fieldName ?? key,
96
- type:
97
- field.type === "boolean" || field.type.includes("number")
98
- ? "numeric"
99
- : field.type === "date"
100
- ? "timestamp"
101
- : "varchar",
102
- }),
103
- );
104
+ type,
105
+ };
106
+ });
104
107
 
105
108
  // get existing table or create it
106
- const tableExists = Object.prototype.hasOwnProperty.call(
107
- existingTables,
108
- baTable.modelName,
109
- );
109
+ const tableExists = baTable.modelName in existingTables;
110
110
 
111
- if (!tableExists) {
112
- migrationPlan.push({
113
- tableName: baTable.modelName,
114
- operation: "create",
115
- fields: [
116
- {
117
- name: "id",
118
- type: "varchar",
119
- primary: true,
120
- unique: true,
121
- },
122
- ...fields,
123
- ],
124
- });
125
- } else {
126
- const existingFields = (existingTables[baTable.modelName] || []).map(
127
- (f) => f.name,
128
- );
111
+ if (tableExists) {
112
+ const existingFields = (existingTables[baTable.modelName] || []).map((f) => f.name);
129
113
  const existingFieldMap = (existingTables[baTable.modelName] || []).reduce(
130
114
  (acc, f) => {
131
115
  acc[f.name] = f.type;
@@ -134,19 +118,14 @@ export async function planMigration(
134
118
  {} as Record<string, string>,
135
119
  );
136
120
  // Warn about type mismatches (optional, not in plan)
137
- fields.forEach((field) => {
138
- if (
139
- existingFields.includes(field.name) &&
140
- existingFieldMap[field.name] !== field.type
141
- ) {
121
+ for (const field of fields) {
122
+ if (existingFields.includes(field.name) && existingFieldMap[field.name] !== field.type) {
142
123
  console.warn(
143
124
  `⚠️ 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.`,
144
125
  );
145
126
  }
146
- });
147
- const fieldsToAdd = fields.filter(
148
- (f) => !existingFields.includes(f.name),
149
- );
127
+ }
128
+ const fieldsToAdd = fields.filter((f) => !existingFields.includes(f.name));
150
129
  if (fieldsToAdd.length > 0) {
151
130
  migrationPlan.push({
152
131
  tableName: baTable.modelName,
@@ -154,6 +133,20 @@ export async function planMigration(
154
133
  fields: fieldsToAdd,
155
134
  });
156
135
  }
136
+ } else {
137
+ migrationPlan.push({
138
+ tableName: baTable.modelName,
139
+ operation: "create",
140
+ fields: [
141
+ {
142
+ name: "id",
143
+ type: "varchar",
144
+ primary: true,
145
+ unique: true,
146
+ },
147
+ ...fields,
148
+ ],
149
+ });
157
150
  }
158
151
  }
159
152
 
@@ -176,10 +169,7 @@ export async function executeMigration(
176
169
  });
177
170
 
178
171
  if (result.error) {
179
- console.error(
180
- `Failed to create table ${step.tableName}:`,
181
- result.error,
182
- );
172
+ console.error(`Failed to create table ${step.tableName}:`, result.error);
183
173
  throw new Error(`Migration failed: ${result.error}`);
184
174
  }
185
175
  } else if (step.operation === "update") {
@@ -190,10 +180,7 @@ export async function executeMigration(
190
180
  });
191
181
 
192
182
  if (result.error) {
193
- console.error(
194
- `Failed to update table ${step.tableName}:`,
195
- result.error,
196
- );
183
+ console.error(`Failed to update table ${step.tableName}:`, result.error);
197
184
  throw new Error(`Migration failed: ${result.error}`);
198
185
  }
199
186
  }
@@ -274,8 +261,12 @@ export function prettyPrintMigrationPlan(migrationPlan: MigrationPlan) {
274
261
  if (step.fields.length) {
275
262
  for (const field of step.fields) {
276
263
  let fieldDesc = ` - ${field.name} (${field.type}`;
277
- if (field.primary) fieldDesc += ", primary";
278
- if (field.unique) fieldDesc += ", unique";
264
+ if (field.primary) {
265
+ fieldDesc += ", primary";
266
+ }
267
+ if (field.unique) {
268
+ fieldDesc += ", unique";
269
+ }
279
270
  fieldDesc += ")";
280
271
  console.log(fieldDesc);
281
272
  }
@@ -1,22 +1,23 @@
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: library code */
1
2
  import { logger as betterAuthLogger } from "better-auth";
2
- import { err, ok, Result } from "neverthrow";
3
- import { z } from "zod/v4";
3
+ import { err, ok, type Result } from "neverthrow";
4
+ import type { z } from "zod/v4";
4
5
 
5
- type BasicAuthCredentials = {
6
+ interface BasicAuthCredentials {
6
7
  username: string;
7
8
  password: string;
8
- };
9
- type OttoAPIKeyAuth = {
9
+ }
10
+ interface OttoAPIKeyAuth {
10
11
  apiKey: string;
11
- };
12
+ }
12
13
  type ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;
13
14
 
14
- export type FmOdataConfig = {
15
+ export interface FmOdataConfig {
15
16
  serverUrl: string;
16
17
  auth: ODataAuth;
17
18
  database: string;
18
19
  logging?: true | "verbose" | "none";
19
- };
20
+ }
20
21
 
21
22
  export function validateUrl(input: string): Result<URL, unknown> {
22
23
  try {
@@ -36,7 +37,7 @@ export function createRawFetch(args: FmOdataConfig) {
36
37
 
37
38
  let baseURL = result.value.origin;
38
39
  if ("apiKey" in args.auth) {
39
- baseURL += `/otto`;
40
+ baseURL += "/otto";
40
41
  }
41
42
  baseURL += `/fmi/odata/v4/${args.database}`;
42
43
 
@@ -63,9 +64,7 @@ export function createRawFetch(args: FmOdataConfig) {
63
64
  // Handle different input types
64
65
  if (typeof input === "string") {
65
66
  // 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}`}`;
67
+ url = input.startsWith("http") ? input : `${baseURL}${input.startsWith("/") ? input : `/${input}`}`;
69
68
  } else if (input instanceof URL) {
70
69
  url = input.toString();
71
70
  } else if (input instanceof Request) {
@@ -101,10 +100,7 @@ export function createRawFetch(args: FmOdataConfig) {
101
100
 
102
101
  // Optional logging
103
102
  if (args.logging === "verbose" || args.logging === true) {
104
- betterAuthLogger.info(
105
- "raw-fetch",
106
- `${requestInit.method || "GET"} ${url}`,
107
- );
103
+ betterAuthLogger.info("raw-fetch", `${requestInit.method || "GET"} ${url}`);
108
104
  if (requestInit.body) {
109
105
  betterAuthLogger.info("raw-fetch", "Request body:", requestInit.body);
110
106
  }
@@ -114,25 +110,15 @@ export function createRawFetch(args: FmOdataConfig) {
114
110
 
115
111
  // Optional logging for response details
116
112
  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
- );
113
+ betterAuthLogger.info("raw-fetch", `Response status: ${response.status} ${response.statusText}`);
114
+ betterAuthLogger.info("raw-fetch", "Response headers:", Object.fromEntries(response.headers.entries()));
126
115
  }
127
116
 
128
117
  // Check if response is ok
129
118
  if (!response.ok) {
130
119
  const errorText = await response.text().catch(() => "Unknown error");
131
120
  if (args.logging === "verbose" || args.logging === true) {
132
- betterAuthLogger.error(
133
- "raw-fetch",
134
- `HTTP Error ${response.status}: ${errorText}`,
135
- );
121
+ betterAuthLogger.error("raw-fetch", `HTTP Error ${response.status}: ${errorText}`);
136
122
  }
137
123
  return {
138
124
  error: `HTTP ${response.status}: ${errorText}`,
@@ -145,51 +131,32 @@ export function createRawFetch(args: FmOdataConfig) {
145
131
  const contentType = response.headers.get("content-type");
146
132
 
147
133
  if (args.logging === "verbose" || args.logging === true) {
148
- betterAuthLogger.info(
149
- "raw-fetch",
150
- `Response content-type: ${contentType || "none"}`,
151
- );
134
+ betterAuthLogger.info("raw-fetch", `Response content-type: ${contentType || "none"}`);
152
135
  }
153
136
 
154
137
  if (contentType?.includes("application/json")) {
155
138
  try {
156
139
  const responseText = await response.text();
157
140
  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
- );
141
+ betterAuthLogger.info("raw-fetch", `Raw response text: "${responseText}"`);
142
+ betterAuthLogger.info("raw-fetch", `Response text length: ${responseText.length}`);
166
143
  }
167
144
 
168
145
  // Handle empty responses
169
146
  if (responseText.trim() === "") {
170
147
  if (args.logging === "verbose" || args.logging === true) {
171
- betterAuthLogger.info(
172
- "raw-fetch",
173
- "Empty JSON response, returning null",
174
- );
148
+ betterAuthLogger.info("raw-fetch", "Empty JSON response, returning null");
175
149
  }
176
150
  responseData = null;
177
151
  } else {
178
152
  responseData = JSON.parse(responseText);
179
153
  if (args.logging === "verbose" || args.logging === true) {
180
- betterAuthLogger.info(
181
- "raw-fetch",
182
- "Successfully parsed JSON response",
183
- );
154
+ betterAuthLogger.info("raw-fetch", "Successfully parsed JSON response");
184
155
  }
185
156
  }
186
157
  } catch (parseError) {
187
158
  if (args.logging === "verbose" || args.logging === true) {
188
- betterAuthLogger.error(
189
- "raw-fetch",
190
- "JSON parse error:",
191
- parseError,
192
- );
159
+ betterAuthLogger.error("raw-fetch", "JSON parse error:", parseError);
193
160
  }
194
161
  return {
195
162
  error: `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : "Unknown parse error"}`,
@@ -200,29 +167,20 @@ export function createRawFetch(args: FmOdataConfig) {
200
167
  // Handle text responses (text/plain, text/html, etc.)
201
168
  responseData = await response.text();
202
169
  if (args.logging === "verbose" || args.logging === true) {
203
- betterAuthLogger.info(
204
- "raw-fetch",
205
- `Text response: "${responseData}"`,
206
- );
170
+ betterAuthLogger.info("raw-fetch", `Text response: "${responseData}"`);
207
171
  }
208
172
  } else {
209
173
  // For other content types, try to get text but don't fail if it's binary
210
174
  try {
211
175
  responseData = await response.text();
212
176
  if (args.logging === "verbose" || args.logging === true) {
213
- betterAuthLogger.info(
214
- "raw-fetch",
215
- `Unknown content-type response as text: "${responseData}"`,
216
- );
177
+ betterAuthLogger.info("raw-fetch", `Unknown content-type response as text: "${responseData}"`);
217
178
  }
218
179
  } catch {
219
180
  // If text parsing fails (e.g., binary data), return null
220
181
  responseData = null;
221
182
  if (args.logging === "verbose" || args.logging === true) {
222
- betterAuthLogger.info(
223
- "raw-fetch",
224
- "Could not parse response as text, returning null",
225
- );
183
+ betterAuthLogger.info("raw-fetch", "Could not parse response as text, returning null");
226
184
  }
227
185
  }
228
186
  }
@@ -235,12 +193,11 @@ export function createRawFetch(args: FmOdataConfig) {
235
193
  data: validation.data,
236
194
  response,
237
195
  };
238
- } else {
239
- return {
240
- error: `Validation failed: ${validation.error.message}`,
241
- response,
242
- };
243
196
  }
197
+ return {
198
+ error: `Validation failed: ${validation.error.message}`,
199
+ response,
200
+ };
244
201
  }
245
202
 
246
203
  // Return unvalidated data
@@ -250,8 +207,7 @@ export function createRawFetch(args: FmOdataConfig) {
250
207
  };
251
208
  } catch (error) {
252
209
  return {
253
- error:
254
- error instanceof Error ? error.message : "Unknown error occurred",
210
+ error: error instanceof Error ? error.message : "Unknown error occurred",
255
211
  };
256
212
  }
257
213
  };