@proofkit/better-auth 0.1.0 → 0.2.0-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.
@@ -1,11 +1,11 @@
1
1
  import { createAdapter } from "better-auth/adapters";
2
- import { FmOdata } from "./odata/index.js";
2
+ import { createFmOdataFetch } from "./odata/index.js";
3
3
  import { z, prettifyError } from "zod/v4";
4
4
  const configSchema = z.object({
5
5
  debugLogs: z.unknown().optional(),
6
6
  usePlural: z.boolean().optional(),
7
7
  odata: z.object({
8
- hostname: z.string(),
8
+ serverUrl: z.string(),
9
9
  auth: z.object({ username: z.string(), password: z.string() }),
10
10
  database: z.string().endsWith(".fmp12")
11
11
  })
@@ -14,7 +14,7 @@ const defaultConfig = {
14
14
  debugLogs: false,
15
15
  usePlural: false,
16
16
  odata: {
17
- hostname: "",
17
+ serverUrl: "",
18
18
  auth: { username: "", password: "" },
19
19
  database: ""
20
20
  }
@@ -32,7 +32,7 @@ function parseWhere(where) {
32
32
  if (typeof value === "boolean") return value ? "true" : "false";
33
33
  if (value instanceof Date) return `'${value.toISOString()}'`;
34
34
  if (Array.isArray(value)) return `(${value.map(formatValue).join(",")})`;
35
- return value.toString();
35
+ return (value == null ? void 0 : value.toString()) ?? "";
36
36
  }
37
37
  const opMap = {
38
38
  eq: "eq",
@@ -88,8 +88,10 @@ const FileMakerAdapter = (_config = defaultConfig) => {
88
88
  throw new Error(`Invalid configuration: ${prettifyError(parsed.error)}`);
89
89
  }
90
90
  const config = parsed.data;
91
- const odata = config.odata instanceof FmOdata ? config.odata : new FmOdata(config.odata);
92
- const db = odata.database;
91
+ const fetch = createFmOdataFetch({
92
+ ...config.odata
93
+ // logging: config.debugLogs ? true : "none",
94
+ });
93
95
  return createAdapter({
94
96
  config: {
95
97
  adapterId: "filemaker",
@@ -111,61 +113,115 @@ const FileMakerAdapter = (_config = defaultConfig) => {
111
113
  return {
112
114
  options: { config },
113
115
  create: async ({ data, model, select }) => {
114
- const row = await db.table(model).create(data);
115
- return row;
116
+ const result = await fetch(`/${model}`, {
117
+ method: "POST",
118
+ body: data,
119
+ output: z.looseObject({ id: z.string() })
120
+ });
121
+ if (result.error) {
122
+ throw new Error("Failed to create record");
123
+ }
124
+ return result.data;
116
125
  },
117
126
  count: async ({ model, where }) => {
118
- const count = await db.table(model).count(parseWhere(where));
119
- return count;
127
+ var _a;
128
+ const result = await fetch(`/${model}/$count`, {
129
+ method: "GET",
130
+ query: {
131
+ $filter: parseWhere(where)
132
+ },
133
+ output: z.object({ value: z.number() })
134
+ });
135
+ if (!result.data) {
136
+ throw new Error("Failed to count records");
137
+ }
138
+ return ((_a = result.data) == null ? void 0 : _a.value) ?? 0;
120
139
  },
121
140
  findOne: async ({ model, where }) => {
122
- const row = await db.table(model).query({
123
- filter: parseWhere(where),
124
- top: 1
141
+ var _a, _b;
142
+ const result = await fetch(`/${model}`, {
143
+ method: "GET",
144
+ query: {
145
+ ...where.length > 0 ? { $filter: parseWhere(where) } : {},
146
+ $top: 1
147
+ },
148
+ output: z.object({ value: z.array(z.any()) })
125
149
  });
126
- return row[0] ?? null;
150
+ if (result.error) {
151
+ throw new Error("Failed to find record");
152
+ }
153
+ return ((_b = (_a = result.data) == null ? void 0 : _a.value) == null ? void 0 : _b[0]) ?? null;
127
154
  },
128
155
  findMany: async ({ model, where, limit, offset, sortBy }) => {
156
+ var _a;
129
157
  const filter = parseWhere(where);
130
- const rows = await db.table(model).query({
131
- filter,
132
- top: limit,
133
- skip: offset,
134
- orderBy: sortBy
158
+ const rows = await fetch(`/${model}`, {
159
+ method: "GET",
160
+ query: {
161
+ ...filter.length > 0 ? { $filter: filter } : {},
162
+ $top: limit,
163
+ $skip: offset,
164
+ ...sortBy ? { $orderby: `"${sortBy.field}" ${sortBy.direction ?? "asc"}` } : {}
165
+ },
166
+ output: z.object({ value: z.array(z.any()) })
135
167
  });
136
- return rows.map((row) => row);
168
+ if (rows.error) {
169
+ throw new Error("Failed to find records");
170
+ }
171
+ return ((_a = rows.data) == null ? void 0 : _a.value) ?? [];
137
172
  },
138
173
  delete: async ({ model, where }) => {
139
- const rows = await db.table(model).query({
140
- filter: parseWhere(where),
141
- top: 1,
142
- select: [`"id"`]
174
+ const result = await fetch(`/${model}`, {
175
+ method: "DELETE",
176
+ query: {
177
+ ...where.length > 0 ? { $filter: parseWhere(where) } : {},
178
+ $top: 1,
179
+ $select: [`"id"`]
180
+ }
143
181
  });
144
- const row = rows[0];
145
- if (!row) return;
146
- await db.table(model).delete(row.id);
182
+ if (result.error) {
183
+ throw new Error("Failed to delete record");
184
+ }
147
185
  },
148
186
  deleteMany: async ({ model, where }) => {
149
- const filter = parseWhere(where);
150
- const count = await db.table(model).count(filter);
151
- await db.table(model).deleteMany(filter);
152
- return count;
187
+ const result = await fetch(`/${model}/$count`, {
188
+ method: "DELETE",
189
+ query: {
190
+ ...where.length > 0 ? { $filter: parseWhere(where) } : {},
191
+ $top: 1,
192
+ $select: [`"id"`]
193
+ },
194
+ output: z.coerce.number()
195
+ });
196
+ if (result.error) {
197
+ throw new Error("Failed to delete record");
198
+ }
199
+ return result.data ?? 0;
153
200
  },
154
201
  update: async ({ model, where, update }) => {
155
- const rows = await db.table(model).query({
156
- filter: parseWhere(where),
157
- top: 1,
158
- select: [`"id"`]
202
+ var _a, _b;
203
+ const result = await fetch(`/${model}`, {
204
+ method: "PATCH",
205
+ query: {
206
+ ...where.length > 0 ? { $filter: parseWhere(where) } : {},
207
+ $top: 1,
208
+ $select: [`"id"`]
209
+ },
210
+ body: update,
211
+ output: z.object({ value: z.array(z.any()) })
159
212
  });
160
- const row = rows[0];
161
- if (!row) return null;
162
- const result = await db.table(model).update(row["id"], update);
163
- return result;
213
+ return ((_b = (_a = result.data) == null ? void 0 : _a.value) == null ? void 0 : _b[0]) ?? null;
164
214
  },
165
215
  updateMany: async ({ model, where, update }) => {
166
216
  const filter = parseWhere(where);
167
- const rows = await db.table(model).updateMany(filter, update);
168
- return rows.length;
217
+ const result = await fetch(`/${model}`, {
218
+ method: "PATCH",
219
+ query: {
220
+ ...where.length > 0 ? { $filter: filter } : {}
221
+ },
222
+ body: update
223
+ });
224
+ return result.data;
169
225
  }
170
226
  };
171
227
  }
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.js","sources":["../../src/adapter.ts"],"sourcesContent":["import {\n CleanedWhere,\n createAdapter,\n type AdapterDebugLogs,\n} from \"better-auth/adapters\";\nimport { FmOdata, type FmOdataConfig } from \"./odata\";\nimport { prettifyError, z } from \"zod/v4\";\n\ninterface FileMakerAdapterConfig {\n /**\n * Helps you debug issues with the adapter.\n */\n debugLogs?: AdapterDebugLogs;\n /**\n * If the table names in the schema are plural.\n */\n usePlural?: boolean;\n\n /**\n * Connection details for the FileMaker server.\n */\n odata: FmOdataConfig;\n}\n\nexport type AdapterOptions = {\n config: FileMakerAdapterConfig;\n};\n\nconst configSchema = z.object({\n debugLogs: z.unknown().optional(),\n usePlural: z.boolean().optional(),\n odata: z.object({\n hostname: z.string(),\n auth: z.object({ username: z.string(), password: z.string() }),\n database: z.string().endsWith(\".fmp12\"),\n }),\n});\n\nconst defaultConfig: Required<FileMakerAdapterConfig> = {\n debugLogs: false,\n usePlural: false,\n odata: {\n hostname: \"\",\n auth: { username: \"\", password: \"\" },\n database: \"\",\n },\n};\n\n/**\n * Parse the where clause to an OData filter string.\n * @param where - The where clause to parse.\n * @returns The OData filter string.\n * @internal\n */\nexport function parseWhere(where?: CleanedWhere[]): string {\n if (!where || where.length === 0) return \"\";\n\n // Helper to quote field names with special chars or if field is 'id'\n function quoteField(field: string, value?: any) {\n // Never quote for null or date values (per test expectations)\n if (value === null || value instanceof Date) return field;\n // Always quote if field is 'id' or has space or underscore\n if (field === \"id\" || /[\\s_]/.test(field)) return `\"${field}\"`;\n return field;\n }\n\n // Helper to format values for OData\n function formatValue(value: any): string {\n if (value === null) return \"null\";\n if (typeof value === \"string\") return `'${value.replace(/'/g, \"''\")}'`;\n if (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n if (value instanceof Date) return `'${value.toISOString()}'`;\n if (Array.isArray(value)) return `(${value.map(formatValue).join(\",\")})`;\n return value.toString();\n }\n\n // Map our operators to OData\n const opMap: Record<string, string> = {\n eq: \"eq\",\n ne: \"ne\",\n lt: \"lt\",\n lte: \"le\",\n gt: \"gt\",\n gte: \"ge\",\n };\n\n // Build each clause\n const clauses: string[] = [];\n for (let i = 0; i < where.length; i++) {\n const cond = where[i];\n if (!cond) continue;\n const field = quoteField(cond.field, cond.value);\n let clause = \"\";\n switch (cond.operator) {\n case \"eq\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"gt\":\n case \"gte\":\n clause = `${field} ${opMap[cond.operator!]} ${formatValue(cond.value)}`;\n break;\n case \"in\":\n if (Array.isArray(cond.value)) {\n clause = cond.value\n .map((v) => `${field} eq ${formatValue(v)}`)\n .join(\" or \");\n clause = `(${clause})`;\n }\n break;\n case \"contains\":\n clause = `contains(${field}, ${formatValue(cond.value)})`;\n break;\n case \"starts_with\":\n clause = `startswith(${field}, ${formatValue(cond.value)})`;\n break;\n case \"ends_with\":\n clause = `endswith(${field}, ${formatValue(cond.value)})`;\n break;\n default:\n clause = `${field} eq ${formatValue(cond.value)}`;\n }\n clauses.push(clause);\n // Add connector if not last\n if (i < where.length - 1) {\n clauses.push((cond.connector || \"and\").toLowerCase());\n }\n }\n return clauses.join(\" \");\n}\n\nexport const FileMakerAdapter = (\n _config: FileMakerAdapterConfig = defaultConfig,\n) => {\n const parsed = configSchema.loose().safeParse(_config);\n\n if (!parsed.success) {\n throw new Error(`Invalid configuration: ${prettifyError(parsed.error)}`);\n }\n const config = parsed.data;\n\n const odata =\n config.odata instanceof FmOdata ? config.odata : new FmOdata(config.odata);\n const db = odata.database;\n\n return createAdapter({\n config: {\n adapterId: \"filemaker\",\n adapterName: \"FileMaker\",\n usePlural: config.usePlural ?? false, // Whether the table names in the schema are plural.\n debugLogs: config.debugLogs ?? false, // Whether to enable debug logs.\n supportsJSON: false, // Whether the database supports JSON. (Default: false)\n supportsDates: false, // Whether the database supports dates. (Default: true)\n supportsBooleans: false, // Whether the database supports booleans. (Default: true)\n supportsNumericIds: false, // Whether the database supports auto-incrementing numeric IDs. (Default: true)\n },\n adapter: ({ options }) => {\n return {\n options: { config },\n create: async ({ data, model, select }) => {\n const row = await db.table(model).create(data);\n return row as unknown as typeof data;\n },\n count: async ({ model, where }) => {\n const count = await db.table(model).count(parseWhere(where));\n return count;\n },\n findOne: async ({ model, where }) => {\n const row = await db.table(model).query({\n filter: parseWhere(where),\n top: 1,\n });\n return (row[0] as any) ?? null;\n },\n findMany: async ({ model, where, limit, offset, sortBy }) => {\n const filter = parseWhere(where);\n\n const rows = await db.table(model).query({\n filter,\n top: limit,\n skip: offset,\n orderBy: sortBy,\n });\n return rows.map((row) => row as any);\n },\n delete: async ({ model, where }) => {\n const rows = await db.table(model).query({\n filter: parseWhere(where),\n top: 1,\n select: [`\"id\"`],\n });\n const row = rows[0] as { id: string } | undefined;\n if (!row) return;\n await db.table(model).delete(row.id);\n },\n deleteMany: async ({ model, where }) => {\n const filter = parseWhere(where);\n const count = await db.table(model).count(filter);\n await db.table(model).deleteMany(filter);\n return count;\n },\n update: async ({ model, where, update }) => {\n const rows = await db.table(model).query({\n filter: parseWhere(where),\n top: 1,\n select: [`\"id\"`],\n });\n const row = rows[0] as { id: string } | undefined;\n if (!row) return null;\n const result = await db.table(model).update(row[\"id\"], update as any);\n return result as any;\n },\n updateMany: async ({ model, where, update }) => {\n const filter = parseWhere(where);\n const rows = await db.table(model).updateMany(filter, update as any);\n return rows.length;\n },\n };\n },\n });\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.js","sources":["../../src/adapter.ts"],"sourcesContent":["import {\n CleanedWhere,\n createAdapter,\n type AdapterDebugLogs,\n} from \"better-auth/adapters\";\nimport { createFmOdataFetch, type FmOdataConfig } from \"./odata\";\nimport { prettifyError, z } from \"zod/v4\";\n\ninterface FileMakerAdapterConfig {\n /**\n * Helps you debug issues with the adapter.\n */\n debugLogs?: AdapterDebugLogs;\n /**\n * If the table names in the schema are plural.\n */\n usePlural?: boolean;\n\n /**\n * Connection details for the FileMaker server.\n */\n odata: FmOdataConfig;\n}\n\nexport type AdapterOptions = {\n config: FileMakerAdapterConfig;\n};\n\nconst configSchema = z.object({\n debugLogs: z.unknown().optional(),\n usePlural: z.boolean().optional(),\n odata: z.object({\n serverUrl: z.string(),\n auth: z.object({ username: z.string(), password: z.string() }),\n database: z.string().endsWith(\".fmp12\"),\n }),\n});\n\nconst defaultConfig: Required<FileMakerAdapterConfig> = {\n debugLogs: false,\n usePlural: false,\n odata: {\n serverUrl: \"\",\n auth: { username: \"\", password: \"\" },\n database: \"\",\n },\n};\n\n/**\n * Parse the where clause to an OData filter string.\n * @param where - The where clause to parse.\n * @returns The OData filter string.\n * @internal\n */\nexport function parseWhere(where?: CleanedWhere[]): string {\n if (!where || where.length === 0) return \"\";\n\n // Helper to quote field names with special chars or if field is 'id'\n function quoteField(field: string, value?: any) {\n // Never quote for null or date values (per test expectations)\n if (value === null || value instanceof Date) return field;\n // Always quote if field is 'id' or has space or underscore\n if (field === \"id\" || /[\\s_]/.test(field)) return `\"${field}\"`;\n return field;\n }\n\n // Helper to format values for OData\n function formatValue(value: any): string {\n if (value === null) return \"null\";\n if (typeof value === \"string\") return `'${value.replace(/'/g, \"''\")}'`;\n if (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n if (value instanceof Date) return `'${value.toISOString()}'`;\n if (Array.isArray(value)) return `(${value.map(formatValue).join(\",\")})`;\n return value?.toString() ?? \"\";\n }\n\n // Map our operators to OData\n const opMap: Record<string, string> = {\n eq: \"eq\",\n ne: \"ne\",\n lt: \"lt\",\n lte: \"le\",\n gt: \"gt\",\n gte: \"ge\",\n };\n\n // Build each clause\n const clauses: string[] = [];\n for (let i = 0; i < where.length; i++) {\n const cond = where[i];\n if (!cond) continue;\n const field = quoteField(cond.field, cond.value);\n let clause = \"\";\n switch (cond.operator) {\n case \"eq\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"gt\":\n case \"gte\":\n clause = `${field} ${opMap[cond.operator!]} ${formatValue(cond.value)}`;\n break;\n case \"in\":\n if (Array.isArray(cond.value)) {\n clause = cond.value\n .map((v) => `${field} eq ${formatValue(v)}`)\n .join(\" or \");\n clause = `(${clause})`;\n }\n break;\n case \"contains\":\n clause = `contains(${field}, ${formatValue(cond.value)})`;\n break;\n case \"starts_with\":\n clause = `startswith(${field}, ${formatValue(cond.value)})`;\n break;\n case \"ends_with\":\n clause = `endswith(${field}, ${formatValue(cond.value)})`;\n break;\n default:\n clause = `${field} eq ${formatValue(cond.value)}`;\n }\n clauses.push(clause);\n // Add connector if not last\n if (i < where.length - 1) {\n clauses.push((cond.connector || \"and\").toLowerCase());\n }\n }\n return clauses.join(\" \");\n}\n\nexport const FileMakerAdapter = (\n _config: FileMakerAdapterConfig = defaultConfig,\n) => {\n const parsed = configSchema.loose().safeParse(_config);\n\n if (!parsed.success) {\n throw new Error(`Invalid configuration: ${prettifyError(parsed.error)}`);\n }\n const config = parsed.data;\n\n const fetch = createFmOdataFetch({\n ...config.odata,\n // logging: config.debugLogs ? true : \"none\",\n });\n\n return createAdapter({\n config: {\n adapterId: \"filemaker\",\n adapterName: \"FileMaker\",\n usePlural: config.usePlural ?? false, // Whether the table names in the schema are plural.\n debugLogs: config.debugLogs ?? false, // Whether to enable debug logs.\n supportsJSON: false, // Whether the database supports JSON. (Default: false)\n supportsDates: false, // Whether the database supports dates. (Default: true)\n supportsBooleans: false, // Whether the database supports booleans. (Default: true)\n supportsNumericIds: false, // Whether the database supports auto-incrementing numeric IDs. (Default: true)\n },\n adapter: ({ options }) => {\n return {\n options: { config },\n create: async ({ data, model, select }) => {\n const result = await fetch(`/${model}`, {\n method: \"POST\",\n body: data,\n output: z.looseObject({ id: z.string() }),\n });\n\n if (result.error) {\n throw new Error(\"Failed to create record\");\n }\n\n return result.data as any;\n },\n count: async ({ model, where }) => {\n const result = await fetch(`/${model}/$count`, {\n method: \"GET\",\n query: {\n $filter: parseWhere(where),\n },\n output: z.object({ value: z.number() }),\n });\n if (!result.data) {\n throw new Error(\"Failed to count records\");\n }\n return result.data?.value ?? 0;\n },\n findOne: async ({ model, where }) => {\n const result = await fetch(`/${model}`, {\n method: \"GET\",\n query: {\n ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),\n $top: 1,\n },\n output: z.object({ value: z.array(z.any()) }),\n });\n if (result.error) {\n throw new Error(\"Failed to find record\");\n }\n return result.data?.value?.[0] ?? null;\n },\n findMany: async ({ model, where, limit, offset, sortBy }) => {\n const filter = parseWhere(where);\n\n const rows = await fetch(`/${model}`, {\n method: \"GET\",\n query: {\n ...(filter.length > 0 ? { $filter: filter } : {}),\n $top: limit,\n $skip: offset,\n ...(sortBy\n ? { $orderby: `\"${sortBy.field}\" ${sortBy.direction ?? \"asc\"}` }\n : {}),\n },\n output: z.object({ value: z.array(z.any()) }),\n });\n if (rows.error) {\n throw new Error(\"Failed to find records\");\n }\n return rows.data?.value ?? [];\n },\n delete: async ({ model, where }) => {\n const result = await fetch(`/${model}`, {\n method: \"DELETE\",\n query: {\n ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),\n $top: 1,\n $select: [`\"id\"`],\n },\n });\n if (result.error) {\n throw new Error(\"Failed to delete record\");\n }\n },\n deleteMany: async ({ model, where }) => {\n const result = await fetch(`/${model}/$count`, {\n method: \"DELETE\",\n query: {\n ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),\n $top: 1,\n $select: [`\"id\"`],\n },\n output: z.coerce.number(),\n });\n if (result.error) {\n throw new Error(\"Failed to delete record\");\n }\n return result.data ?? 0;\n },\n update: async ({ model, where, update }) => {\n const result = await fetch(`/${model}`, {\n method: \"PATCH\",\n query: {\n ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),\n $top: 1,\n $select: [`\"id\"`],\n },\n body: update,\n output: z.object({ value: z.array(z.any()) }),\n });\n return result.data?.value?.[0] ?? null;\n },\n updateMany: async ({ model, where, update }) => {\n const filter = parseWhere(where);\n const result = await fetch(`/${model}`, {\n method: \"PATCH\",\n query: {\n ...(where.length > 0 ? { $filter: filter } : {}),\n },\n body: update,\n });\n return result.data as any;\n },\n };\n },\n });\n};\n"],"names":[],"mappings":"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"}
@@ -7,7 +7,7 @@ import { getConfig } from "../better-auth-cli/utils/get-config.js";
7
7
  import { logger } from "better-auth";
8
8
  import prompts from "prompts";
9
9
  import chalk from "chalk";
10
- import { FmOdata } from "../odata/index.js";
10
+ import { createFmOdataFetch } from "../odata/index.js";
11
11
  async function main() {
12
12
  const program = new Command();
13
13
  program.command("migrate", { isDefault: true }).option(
@@ -42,7 +42,7 @@ async function main() {
42
42
  }
43
43
  const betterAuthSchema = getAuthTables(config);
44
44
  const adapterConfig = adapter.options.config;
45
- const db = new FmOdata({
45
+ const fetch = createFmOdataFetch({
46
46
  ...adapterConfig.odata,
47
47
  auth: (
48
48
  // If the username and password are provided in the CLI, use them to authenticate instead of what's in the config file.
@@ -51,8 +51,12 @@ async function main() {
51
51
  password: options.password
52
52
  } : adapterConfig.odata.auth
53
53
  )
54
- }).database;
55
- const migrationPlan = await planMigration(db, betterAuthSchema);
54
+ });
55
+ const migrationPlan = await planMigration(
56
+ fetch,
57
+ betterAuthSchema,
58
+ adapterConfig.odata.database
59
+ );
56
60
  if (migrationPlan.length === 0) {
57
61
  logger.info("No changes to apply. Database is up to date.");
58
62
  return;
@@ -76,7 +80,7 @@ async function main() {
76
80
  return;
77
81
  }
78
82
  }
79
- await executeMigration(db, migrationPlan);
83
+ await executeMigration(fetch, migrationPlan);
80
84
  logger.info("Migration applied successfully.");
81
85
  });
82
86
  await program.parseAsync(process.argv);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node --no-warnings\nimport { Command } from \"@commander-js/extra-typings\";\nimport fs from \"fs-extra\";\n\nimport {\n executeMigration,\n planMigration,\n prettyPrintMigrationPlan,\n} from \"../migrate\";\nimport { getAdapter, getAuthTables } from \"better-auth/db\";\nimport { getConfig } from \"../better-auth-cli/utils/get-config\";\nimport { logger } from \"better-auth\";\nimport { BasicAuth, Connection, Database } from \"fm-odata-client\";\nimport prompts from \"prompts\";\nimport chalk from \"chalk\";\nimport { AdapterOptions } from \"../adapter\";\nimport { FmOdata } from \"../odata\";\n\nasync function main() {\n const program = new Command();\n\n program\n .command(\"migrate\", { isDefault: true })\n .option(\n \"--cwd <path>\",\n \"Path to the current working directory\",\n process.cwd(),\n )\n .option(\"--config <path>\", \"Path to the config file\")\n .option(\"-u, --username <username>\", \"Full Access Username\")\n .option(\"-p, --password <password>\", \"Full Access Password\")\n .option(\"-y, --yes\", \"Skip confirmation\", false)\n\n .action(async (options) => {\n const cwd = options.cwd;\n if (!fs.existsSync(cwd)) {\n logger.error(`The directory \"${cwd}\" does not exist.`);\n process.exit(1);\n }\n\n const config = await getConfig({\n cwd,\n configPath: options.config,\n });\n if (!config) {\n logger.error(\n \"No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag.\",\n );\n return;\n }\n\n const adapter = await getAdapter(config).catch((e) => {\n logger.error(e.message);\n process.exit(1);\n });\n\n if (adapter.id !== \"filemaker\") {\n logger.error(\n \"This generator is only compatible with the FileMaker adapter.\",\n );\n return;\n }\n\n const betterAuthSchema = getAuthTables(config);\n\n const adapterConfig = (adapter.options as AdapterOptions).config;\n const db = new FmOdata({\n ...adapterConfig.odata,\n auth:\n // If the username and password are provided in the CLI, use them to authenticate instead of what's in the config file.\n options.username && options.password\n ? {\n username: options.username,\n password: options.password,\n }\n : adapterConfig.odata.auth,\n }).database;\n\n const migrationPlan = await planMigration(db, betterAuthSchema);\n\n if (migrationPlan.length === 0) {\n logger.info(\"No changes to apply. Database is up to date.\");\n return;\n }\n\n if (!options.yes) {\n prettyPrintMigrationPlan(migrationPlan);\n\n if (migrationPlan.length > 0) {\n console.log(\n chalk.gray(\n \"💡 Tip: You can use the --yes flag to skip this confirmation.\",\n ),\n );\n }\n\n const { confirm } = await prompts({\n type: \"confirm\",\n name: \"confirm\",\n message: \"Apply the above changes to your database?\",\n });\n if (!confirm) {\n logger.error(\"Schema changes not applied.\");\n return;\n }\n }\n\n await executeMigration(db, migrationPlan);\n\n logger.info(\"Migration applied successfully.\");\n });\n await program.parseAsync(process.argv);\n process.exit(0);\n}\n\nmain().catch(console.error);\n"],"names":[],"mappings":";;;;;;;;;;AAkBA,eAAe,OAAO;AACd,QAAA,UAAU,IAAI,QAAQ;AAE5B,UACG,QAAQ,WAAW,EAAE,WAAW,KAAM,CAAA,EACtC;AAAA,IACC;AAAA,IACA;AAAA,IACA,QAAQ,IAAI;AAAA,EAAA,EAEb,OAAO,mBAAmB,yBAAyB,EACnD,OAAO,6BAA6B,sBAAsB,EAC1D,OAAO,6BAA6B,sBAAsB,EAC1D,OAAO,aAAa,qBAAqB,KAAK,EAE9C,OAAO,OAAO,YAAY;AACzB,UAAM,MAAM,QAAQ;AACpB,QAAI,CAAC,GAAG,WAAW,GAAG,GAAG;AAChB,aAAA,MAAM,kBAAkB,GAAG,mBAAmB;AACrD,cAAQ,KAAK,CAAC;AAAA,IAAA;AAGV,UAAA,SAAS,MAAM,UAAU;AAAA,MAC7B;AAAA,MACA,YAAY,QAAQ;AAAA,IAAA,CACrB;AACD,QAAI,CAAC,QAAQ;AACJ,aAAA;AAAA,QACL;AAAA,MACF;AACA;AAAA,IAAA;AAGF,UAAM,UAAU,MAAM,WAAW,MAAM,EAAE,MAAM,CAAC,MAAM;AAC7C,aAAA,MAAM,EAAE,OAAO;AACtB,cAAQ,KAAK,CAAC;AAAA,IAAA,CACf;AAEG,QAAA,QAAQ,OAAO,aAAa;AACvB,aAAA;AAAA,QACL;AAAA,MACF;AACA;AAAA,IAAA;AAGI,UAAA,mBAAmB,cAAc,MAAM;AAEvC,UAAA,gBAAiB,QAAQ,QAA2B;AACpD,UAAA,KAAK,IAAI,QAAQ;AAAA,MACrB,GAAG,cAAc;AAAA,MACjB;AAAA;AAAA,QAEE,QAAQ,YAAY,QAAQ,WACxB;AAAA,UACE,UAAU,QAAQ;AAAA,UAClB,UAAU,QAAQ;AAAA,QAAA,IAEpB,cAAc,MAAM;AAAA;AAAA,IAC3B,CAAA,EAAE;AAEH,UAAM,gBAAgB,MAAM,cAAc,IAAI,gBAAgB;AAE1D,QAAA,cAAc,WAAW,GAAG;AAC9B,aAAO,KAAK,8CAA8C;AAC1D;AAAA,IAAA;AAGE,QAAA,CAAC,QAAQ,KAAK;AAChB,+BAAyB,aAAa;AAElC,UAAA,cAAc,SAAS,GAAG;AACpB,gBAAA;AAAA,UACN,MAAM;AAAA,YACJ;AAAA,UAAA;AAAA,QAEJ;AAAA,MAAA;AAGF,YAAM,EAAE,YAAY,MAAM,QAAQ;AAAA,QAChC,MAAM;AAAA,QACN,MAAM;AAAA,QACN,SAAS;AAAA,MAAA,CACV;AACD,UAAI,CAAC,SAAS;AACZ,eAAO,MAAM,6BAA6B;AAC1C;AAAA,MAAA;AAAA,IACF;AAGI,UAAA,iBAAiB,IAAI,aAAa;AAExC,WAAO,KAAK,iCAAiC;AAAA,EAAA,CAC9C;AACG,QAAA,QAAQ,WAAW,QAAQ,IAAI;AACrC,UAAQ,KAAK,CAAC;AAChB;AAEA,OAAO,MAAM,QAAQ,KAAK;"}
1
+ {"version":3,"file":"index.js","sources":["../../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node --no-warnings\nimport { Command } from \"@commander-js/extra-typings\";\nimport fs from \"fs-extra\";\n\nimport {\n executeMigration,\n planMigration,\n prettyPrintMigrationPlan,\n} from \"../migrate\";\nimport { getAdapter, getAuthTables } from \"better-auth/db\";\nimport { getConfig } from \"../better-auth-cli/utils/get-config\";\nimport { logger } from \"better-auth\";\nimport { BasicAuth, Connection, Database } from \"fm-odata-client\";\nimport prompts from \"prompts\";\nimport chalk from \"chalk\";\nimport { AdapterOptions } from \"../adapter\";\nimport { createFmOdataFetch } from \"../odata\";\n\nasync function main() {\n const program = new Command();\n\n program\n .command(\"migrate\", { isDefault: true })\n .option(\n \"--cwd <path>\",\n \"Path to the current working directory\",\n process.cwd(),\n )\n .option(\"--config <path>\", \"Path to the config file\")\n .option(\"-u, --username <username>\", \"Full Access Username\")\n .option(\"-p, --password <password>\", \"Full Access Password\")\n .option(\"-y, --yes\", \"Skip confirmation\", false)\n\n .action(async (options) => {\n const cwd = options.cwd;\n if (!fs.existsSync(cwd)) {\n logger.error(`The directory \"${cwd}\" does not exist.`);\n process.exit(1);\n }\n\n const config = await getConfig({\n cwd,\n configPath: options.config,\n });\n if (!config) {\n logger.error(\n \"No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag.\",\n );\n return;\n }\n\n const adapter = await getAdapter(config).catch((e) => {\n logger.error(e.message);\n process.exit(1);\n });\n\n if (adapter.id !== \"filemaker\") {\n logger.error(\n \"This generator is only compatible with the FileMaker adapter.\",\n );\n return;\n }\n\n const betterAuthSchema = getAuthTables(config);\n\n const adapterConfig = (adapter.options as AdapterOptions).config;\n const fetch = createFmOdataFetch({\n ...adapterConfig.odata,\n auth:\n // If the username and password are provided in the CLI, use them to authenticate instead of what's in the config file.\n options.username && options.password\n ? {\n username: options.username,\n password: options.password,\n }\n : adapterConfig.odata.auth,\n });\n\n const migrationPlan = await planMigration(\n fetch,\n betterAuthSchema,\n adapterConfig.odata.database,\n );\n\n if (migrationPlan.length === 0) {\n logger.info(\"No changes to apply. Database is up to date.\");\n return;\n }\n\n if (!options.yes) {\n prettyPrintMigrationPlan(migrationPlan);\n\n if (migrationPlan.length > 0) {\n console.log(\n chalk.gray(\n \"💡 Tip: You can use the --yes flag to skip this confirmation.\",\n ),\n );\n }\n\n const { confirm } = await prompts({\n type: \"confirm\",\n name: \"confirm\",\n message: \"Apply the above changes to your database?\",\n });\n if (!confirm) {\n logger.error(\"Schema changes not applied.\");\n return;\n }\n }\n\n await executeMigration(fetch, migrationPlan);\n\n logger.info(\"Migration applied successfully.\");\n });\n await program.parseAsync(process.argv);\n process.exit(0);\n}\n\nmain().catch(console.error);\n"],"names":[],"mappings":";;;;;;;;;;AAkBA,eAAe,OAAO;AACd,QAAA,UAAU,IAAI,QAAQ;AAE5B,UACG,QAAQ,WAAW,EAAE,WAAW,KAAM,CAAA,EACtC;AAAA,IACC;AAAA,IACA;AAAA,IACA,QAAQ,IAAI;AAAA,EAAA,EAEb,OAAO,mBAAmB,yBAAyB,EACnD,OAAO,6BAA6B,sBAAsB,EAC1D,OAAO,6BAA6B,sBAAsB,EAC1D,OAAO,aAAa,qBAAqB,KAAK,EAE9C,OAAO,OAAO,YAAY;AACzB,UAAM,MAAM,QAAQ;AACpB,QAAI,CAAC,GAAG,WAAW,GAAG,GAAG;AAChB,aAAA,MAAM,kBAAkB,GAAG,mBAAmB;AACrD,cAAQ,KAAK,CAAC;AAAA,IAAA;AAGV,UAAA,SAAS,MAAM,UAAU;AAAA,MAC7B;AAAA,MACA,YAAY,QAAQ;AAAA,IAAA,CACrB;AACD,QAAI,CAAC,QAAQ;AACJ,aAAA;AAAA,QACL;AAAA,MACF;AACA;AAAA,IAAA;AAGF,UAAM,UAAU,MAAM,WAAW,MAAM,EAAE,MAAM,CAAC,MAAM;AAC7C,aAAA,MAAM,EAAE,OAAO;AACtB,cAAQ,KAAK,CAAC;AAAA,IAAA,CACf;AAEG,QAAA,QAAQ,OAAO,aAAa;AACvB,aAAA;AAAA,QACL;AAAA,MACF;AACA;AAAA,IAAA;AAGI,UAAA,mBAAmB,cAAc,MAAM;AAEvC,UAAA,gBAAiB,QAAQ,QAA2B;AAC1D,UAAM,QAAQ,mBAAmB;AAAA,MAC/B,GAAG,cAAc;AAAA,MACjB;AAAA;AAAA,QAEE,QAAQ,YAAY,QAAQ,WACxB;AAAA,UACE,UAAU,QAAQ;AAAA,UAClB,UAAU,QAAQ;AAAA,QAAA,IAEpB,cAAc,MAAM;AAAA;AAAA,IAAA,CAC3B;AAED,UAAM,gBAAgB,MAAM;AAAA,MAC1B;AAAA,MACA;AAAA,MACA,cAAc,MAAM;AAAA,IACtB;AAEI,QAAA,cAAc,WAAW,GAAG;AAC9B,aAAO,KAAK,8CAA8C;AAC1D;AAAA,IAAA;AAGE,QAAA,CAAC,QAAQ,KAAK;AAChB,+BAAyB,aAAa;AAElC,UAAA,cAAc,SAAS,GAAG;AACpB,gBAAA;AAAA,UACN,MAAM;AAAA,YACJ;AAAA,UAAA;AAAA,QAEJ;AAAA,MAAA;AAGF,YAAM,EAAE,YAAY,MAAM,QAAQ;AAAA,QAChC,MAAM;AAAA,QACN,MAAM;AAAA,QACN,SAAS;AAAA,MAAA,CACV;AACD,UAAI,CAAC,SAAS;AACZ,eAAO,MAAM,6BAA6B;AAC1C;AAAA,MAAA;AAAA,IACF;AAGI,UAAA,iBAAiB,OAAO,aAAa;AAE3C,WAAO,KAAK,iCAAiC;AAAA,EAAA,CAC9C;AACG,QAAA,QAAQ,WAAW,QAAQ,IAAI;AACrC,UAAQ,KAAK,CAAC;AAChB;AAEA,OAAO,MAAM,QAAQ,KAAK;"}
@@ -1,8 +1,10 @@
1
1
  import { BetterAuthDbSchema } from 'better-auth/db';
2
- import { Database } from 'fm-odata-client';
2
+ import { Metadata } from 'fm-odata-client';
3
3
  import { default as z } from 'zod/v4';
4
- export declare function planMigration(db: Database, betterAuthSchema: BetterAuthDbSchema): Promise<MigrationPlan>;
5
- export declare function executeMigration(db: Database, migrationPlan: MigrationPlan): Promise<void>;
4
+ import { createFmOdataFetch } from './odata.js';
5
+ export declare function getMetadata(fetch: ReturnType<typeof createFmOdataFetch>, databaseName: string): Promise<Metadata>;
6
+ export declare function planMigration(fetch: ReturnType<typeof createFmOdataFetch>, betterAuthSchema: BetterAuthDbSchema, databaseName: string): Promise<MigrationPlan>;
7
+ export declare function executeMigration(fetch: ReturnType<typeof createFmOdataFetch>, migrationPlan: MigrationPlan): Promise<void>;
6
8
  export declare const migrationPlanSchema: z.ZodArray<z.ZodObject<{
7
9
  tableName: z.ZodString;
8
10
  operation: z.ZodEnum<{
@@ -1,10 +1,19 @@
1
1
  import chalk from "chalk";
2
2
  import z from "zod/v4";
3
- async function planMigration(db, betterAuthSchema) {
4
- const metadata = await db.getMetadata().catch((error) => {
5
- console.error("Failed to get metadata from database", error);
6
- return null;
3
+ async function getMetadata(fetch, databaseName) {
4
+ var _a;
5
+ const result = await fetch("/$metadata", {
6
+ method: "GET",
7
+ headers: { accept: "application/json" },
8
+ output: z.looseObject({
9
+ $Version: z.string(),
10
+ "@ServerVersion": z.string()
11
+ })
7
12
  });
13
+ return (_a = result.data) == null ? void 0 : _a[databaseName];
14
+ }
15
+ async function planMigration(fetch, betterAuthSchema, databaseName) {
16
+ const metadata = await getMetadata(fetch, databaseName);
8
17
  let entitySetToType = {};
9
18
  if (metadata) {
10
19
  for (const [key, value] of Object.entries(metadata)) {
@@ -91,14 +100,22 @@ async function planMigration(db, betterAuthSchema) {
91
100
  }
92
101
  return migrationPlan;
93
102
  }
94
- async function executeMigration(db, migrationPlan) {
103
+ async function executeMigration(fetch, migrationPlan) {
95
104
  for (const step of migrationPlan) {
96
105
  if (step.operation === "create") {
97
106
  console.log("Creating table:", step.tableName);
98
- await db.schemaManager().createTable(step.tableName, step.fields);
107
+ await fetch("@post/FileMaker_Tables", {
108
+ body: {
109
+ tableName: step.tableName,
110
+ fields: step.fields
111
+ }
112
+ });
99
113
  } else if (step.operation === "update") {
100
114
  console.log("Adding fields to table:", step.tableName);
101
- await db.schemaManager().addFields(step.tableName, step.fields);
115
+ await fetch("@post/FileMaker_Tables/:tableName", {
116
+ params: { tableName: step.tableName },
117
+ body: { fields: step.fields }
118
+ });
102
119
  }
103
120
  }
104
121
  }
@@ -175,6 +192,7 @@ ${emoji} ${step.operation === "create" ? chalk.bold.green("Create table") : chal
175
192
  }
176
193
  export {
177
194
  executeMigration,
195
+ getMetadata,
178
196
  planMigration,
179
197
  prettyPrintMigrationPlan
180
198
  };
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.js","sources":["../../src/migrate.ts"],"sourcesContent":["import { type BetterAuthDbSchema } from \"better-auth/db\";\nimport { Database, type Field as FmField } from \"fm-odata-client\";\nimport chalk from \"chalk\";\nimport z from \"zod/v4\";\n\nexport async function planMigration(\n db: Database,\n betterAuthSchema: BetterAuthDbSchema,\n): Promise<MigrationPlan> {\n const metadata = await db.getMetadata().catch((error) => {\n console.error(\"Failed to get metadata from database\", error);\n return null;\n });\n\n // Build a map from entity set name to entity type key\n let entitySetToType: Record<string, string> = {};\n if (metadata) {\n for (const [key, value] of Object.entries(metadata)) {\n if (value.$Kind === \"EntitySet\" && value.$Type) {\n // $Type is like 'betterauth_test.fmp12.proofkit_user_'\n const typeKey = value.$Type.split(\".\").pop(); // e.g., 'proofkit_user_'\n entitySetToType[key] = typeKey || key;\n }\n }\n }\n\n const existingTables = metadata\n ? Object.entries(entitySetToType).reduce(\n (acc, [entitySetName, entityTypeKey]) => {\n const entityType = metadata[entityTypeKey];\n if (!entityType) return acc;\n const fields = Object.entries(entityType)\n .filter(\n ([fieldKey, fieldValue]) =>\n typeof fieldValue === \"object\" &&\n fieldValue !== null &&\n \"$Type\" in fieldValue,\n )\n .map(([fieldKey, fieldValue]) => ({\n name: fieldKey,\n type:\n fieldValue.$Type === \"Edm.String\"\n ? \"string\"\n : fieldValue.$Type === \"Edm.DateTimeOffset\"\n ? \"timestamp\"\n : fieldValue.$Type === \"Edm.Decimal\" ||\n fieldValue.$Type === \"Edm.Int32\" ||\n fieldValue.$Type === \"Edm.Int64\"\n ? \"numeric\"\n : \"string\",\n }));\n acc[entitySetName] = fields;\n return acc;\n },\n {} as Record<string, { name: string; type: string }[]>,\n )\n : {};\n\n const baTables = Object.entries(betterAuthSchema)\n .sort((a, b) => (a[1].order ?? 0) - (b[1].order ?? 0))\n .map(([key, value]) => ({\n ...value,\n keyName: key,\n }));\n\n const migrationPlan: MigrationPlan = [];\n\n for (const baTable of baTables) {\n const fields: FmField[] = Object.entries(baTable.fields).map(\n ([key, field]) => ({\n name: field.fieldName ?? key,\n type:\n field.type === \"boolean\" || field.type.includes(\"number\")\n ? \"numeric\"\n : field.type === \"date\"\n ? \"timestamp\"\n : \"string\",\n }),\n );\n\n // get existing table or create it\n const tableExists = Object.prototype.hasOwnProperty.call(\n existingTables,\n baTable.modelName,\n );\n\n if (!tableExists) {\n migrationPlan.push({\n tableName: baTable.modelName,\n operation: \"create\",\n fields: [\n {\n name: \"id\",\n type: \"string\",\n primary: true,\n unique: true,\n },\n ...fields,\n ],\n });\n } else {\n const existingFields = (existingTables[baTable.modelName] || []).map(\n (f) => f.name,\n );\n const existingFieldMap = (existingTables[baTable.modelName] || []).reduce(\n (acc, f) => {\n acc[f.name] = f.type;\n return acc;\n },\n {} as Record<string, string>,\n );\n // Warn about type mismatches (optional, not in plan)\n fields.forEach((field) => {\n if (\n existingFields.includes(field.name) &&\n existingFieldMap[field.name] !== field.type\n ) {\n console.warn(\n `⚠️ 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.`,\n );\n }\n });\n const fieldsToAdd = fields.filter(\n (f) => !existingFields.includes(f.name),\n );\n if (fieldsToAdd.length > 0) {\n migrationPlan.push({\n tableName: baTable.modelName,\n operation: \"update\",\n fields: fieldsToAdd,\n });\n }\n }\n }\n\n return migrationPlan;\n}\n\nexport async function executeMigration(\n db: Database,\n migrationPlan: MigrationPlan,\n) {\n for (const step of migrationPlan) {\n if (step.operation === \"create\") {\n console.log(\"Creating table:\", step.tableName);\n await db.schemaManager().createTable(step.tableName, step.fields);\n } else if (step.operation === \"update\") {\n console.log(\"Adding fields to table:\", step.tableName);\n await db.schemaManager().addFields(step.tableName, step.fields);\n }\n }\n}\n\nconst genericFieldSchema = z.object({\n name: z.string(),\n nullable: z.boolean().optional(),\n primary: z.boolean().optional(),\n unique: z.boolean().optional(),\n global: z.boolean().optional(),\n repetitions: z.number().optional(),\n});\n\nconst stringFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"string\"),\n maxLength: z.number().optional(),\n default: z.enum([\"USER\", \"USERNAME\", \"CURRENT_USER\"]).optional(),\n});\n\nconst numericFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"numeric\"),\n});\n\nconst dateFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"date\"),\n default: z.enum([\"CURRENT_DATE\", \"CURDATE\"]).optional(),\n});\n\nconst timeFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"time\"),\n default: z.enum([\"CURRENT_TIME\", \"CURTIME\"]).optional(),\n});\n\nconst timestampFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"timestamp\"),\n default: z.enum([\"CURRENT_TIMESTAMP\", \"CURTIMESTAMP\"]).optional(),\n});\n\nconst containerFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"container\"),\n externalSecurePath: z.string().optional(),\n});\n\nconst fieldSchema = z.discriminatedUnion(\"type\", [\n stringFieldSchema,\n numericFieldSchema,\n dateFieldSchema,\n timeFieldSchema,\n timestampFieldSchema,\n containerFieldSchema,\n]);\n\nexport const migrationPlanSchema = z\n .object({\n tableName: z.string(),\n operation: z.enum([\"create\", \"update\"]),\n fields: z.array(fieldSchema),\n })\n .array();\n\nexport type MigrationPlan = z.infer<typeof migrationPlanSchema>;\n\nexport function prettyPrintMigrationPlan(migrationPlan: MigrationPlan) {\n if (!migrationPlan.length) {\n console.log(\"No changes to apply. Database is up to date.\");\n return;\n }\n console.log(chalk.bold.green(\"Migration plan:\"));\n for (const step of migrationPlan) {\n const emoji = step.operation === \"create\" ? \"✅\" : \"✏️\";\n console.log(\n `\\n${emoji} ${step.operation === \"create\" ? chalk.bold.green(\"Create table\") : chalk.bold.yellow(\"Update table\")}: ${step.tableName}`,\n );\n if (step.fields.length) {\n for (const field of step.fields) {\n let fieldDesc = ` - ${field.name} (${field.type}`;\n if (field.primary) fieldDesc += \", primary\";\n if (field.unique) fieldDesc += \", unique\";\n fieldDesc += \")\";\n console.log(fieldDesc);\n }\n } else {\n console.log(\" (No fields to add)\");\n }\n }\n console.log(\"\");\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"migrate.js","sources":["../../src/migrate.ts"],"sourcesContent":["import { type BetterAuthDbSchema } from \"better-auth/db\";\nimport { type Metadata, type Field as FmField } from \"fm-odata-client\";\nimport chalk from \"chalk\";\nimport z from \"zod/v4\";\nimport { createFmOdataFetch } from \"./odata\";\n\nexport async function getMetadata(\n fetch: ReturnType<typeof createFmOdataFetch>,\n databaseName: string,\n) {\n const result = await fetch(\"/$metadata\", {\n method: \"GET\",\n headers: { accept: \"application/json\" },\n output: z.looseObject({\n $Version: z.string(),\n \"@ServerVersion\": z.string(),\n }),\n });\n return result.data?.[databaseName] as Metadata;\n}\n\nexport async function planMigration(\n fetch: ReturnType<typeof createFmOdataFetch>,\n betterAuthSchema: BetterAuthDbSchema,\n databaseName: string,\n): Promise<MigrationPlan> {\n const metadata = await getMetadata(fetch, databaseName);\n\n // Build a map from entity set name to entity type key\n let entitySetToType: Record<string, string> = {};\n if (metadata) {\n for (const [key, value] of Object.entries(metadata)) {\n if (value.$Kind === \"EntitySet\" && value.$Type) {\n // $Type is like 'betterauth_test.fmp12.proofkit_user_'\n const typeKey = value.$Type.split(\".\").pop(); // e.g., 'proofkit_user_'\n entitySetToType[key] = typeKey || key;\n }\n }\n }\n\n const existingTables = metadata\n ? Object.entries(entitySetToType).reduce(\n (acc, [entitySetName, entityTypeKey]) => {\n const entityType = metadata[entityTypeKey];\n if (!entityType) return acc;\n const fields = Object.entries(entityType)\n .filter(\n ([fieldKey, fieldValue]) =>\n typeof fieldValue === \"object\" &&\n fieldValue !== null &&\n \"$Type\" in fieldValue,\n )\n .map(([fieldKey, fieldValue]) => ({\n name: fieldKey,\n type:\n fieldValue.$Type === \"Edm.String\"\n ? \"string\"\n : fieldValue.$Type === \"Edm.DateTimeOffset\"\n ? \"timestamp\"\n : fieldValue.$Type === \"Edm.Decimal\" ||\n fieldValue.$Type === \"Edm.Int32\" ||\n fieldValue.$Type === \"Edm.Int64\"\n ? \"numeric\"\n : \"string\",\n }));\n acc[entitySetName] = fields;\n return acc;\n },\n {} as Record<string, { name: string; type: string }[]>,\n )\n : {};\n\n const baTables = Object.entries(betterAuthSchema)\n .sort((a, b) => (a[1].order ?? 0) - (b[1].order ?? 0))\n .map(([key, value]) => ({\n ...value,\n keyName: key,\n }));\n\n const migrationPlan: MigrationPlan = [];\n\n for (const baTable of baTables) {\n const fields: FmField[] = Object.entries(baTable.fields).map(\n ([key, field]) => ({\n name: field.fieldName ?? key,\n type:\n field.type === \"boolean\" || field.type.includes(\"number\")\n ? \"numeric\"\n : field.type === \"date\"\n ? \"timestamp\"\n : \"string\",\n }),\n );\n\n // get existing table or create it\n const tableExists = Object.prototype.hasOwnProperty.call(\n existingTables,\n baTable.modelName,\n );\n\n if (!tableExists) {\n migrationPlan.push({\n tableName: baTable.modelName,\n operation: \"create\",\n fields: [\n {\n name: \"id\",\n type: \"string\",\n primary: true,\n unique: true,\n },\n ...fields,\n ],\n });\n } else {\n const existingFields = (existingTables[baTable.modelName] || []).map(\n (f) => f.name,\n );\n const existingFieldMap = (existingTables[baTable.modelName] || []).reduce(\n (acc, f) => {\n acc[f.name] = f.type;\n return acc;\n },\n {} as Record<string, string>,\n );\n // Warn about type mismatches (optional, not in plan)\n fields.forEach((field) => {\n if (\n existingFields.includes(field.name) &&\n existingFieldMap[field.name] !== field.type\n ) {\n console.warn(\n `⚠️ 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.`,\n );\n }\n });\n const fieldsToAdd = fields.filter(\n (f) => !existingFields.includes(f.name),\n );\n if (fieldsToAdd.length > 0) {\n migrationPlan.push({\n tableName: baTable.modelName,\n operation: \"update\",\n fields: fieldsToAdd,\n });\n }\n }\n }\n\n return migrationPlan;\n}\n\nexport async function executeMigration(\n fetch: ReturnType<typeof createFmOdataFetch>,\n migrationPlan: MigrationPlan,\n) {\n for (const step of migrationPlan) {\n if (step.operation === \"create\") {\n console.log(\"Creating table:\", step.tableName);\n await fetch(\"@post/FileMaker_Tables\", {\n body: {\n tableName: step.tableName,\n fields: step.fields,\n },\n });\n } else if (step.operation === \"update\") {\n console.log(\"Adding fields to table:\", step.tableName);\n await fetch(\"@post/FileMaker_Tables/:tableName\", {\n params: { tableName: step.tableName },\n body: { fields: step.fields },\n });\n }\n }\n}\n\nconst genericFieldSchema = z.object({\n name: z.string(),\n nullable: z.boolean().optional(),\n primary: z.boolean().optional(),\n unique: z.boolean().optional(),\n global: z.boolean().optional(),\n repetitions: z.number().optional(),\n});\n\nconst stringFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"string\"),\n maxLength: z.number().optional(),\n default: z.enum([\"USER\", \"USERNAME\", \"CURRENT_USER\"]).optional(),\n});\n\nconst numericFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"numeric\"),\n});\n\nconst dateFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"date\"),\n default: z.enum([\"CURRENT_DATE\", \"CURDATE\"]).optional(),\n});\n\nconst timeFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"time\"),\n default: z.enum([\"CURRENT_TIME\", \"CURTIME\"]).optional(),\n});\n\nconst timestampFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"timestamp\"),\n default: z.enum([\"CURRENT_TIMESTAMP\", \"CURTIMESTAMP\"]).optional(),\n});\n\nconst containerFieldSchema = genericFieldSchema.extend({\n type: z.literal(\"container\"),\n externalSecurePath: z.string().optional(),\n});\n\nconst fieldSchema = z.discriminatedUnion(\"type\", [\n stringFieldSchema,\n numericFieldSchema,\n dateFieldSchema,\n timeFieldSchema,\n timestampFieldSchema,\n containerFieldSchema,\n]);\n\nexport const migrationPlanSchema = z\n .object({\n tableName: z.string(),\n operation: z.enum([\"create\", \"update\"]),\n fields: z.array(fieldSchema),\n })\n .array();\n\nexport type MigrationPlan = z.infer<typeof migrationPlanSchema>;\n\nexport function prettyPrintMigrationPlan(migrationPlan: MigrationPlan) {\n if (!migrationPlan.length) {\n console.log(\"No changes to apply. Database is up to date.\");\n return;\n }\n console.log(chalk.bold.green(\"Migration plan:\"));\n for (const step of migrationPlan) {\n const emoji = step.operation === \"create\" ? \"✅\" : \"✏️\";\n console.log(\n `\\n${emoji} ${step.operation === \"create\" ? chalk.bold.green(\"Create table\") : chalk.bold.yellow(\"Update table\")}: ${step.tableName}`,\n );\n if (step.fields.length) {\n for (const field of step.fields) {\n let fieldDesc = ` - ${field.name} (${field.type}`;\n if (field.primary) fieldDesc += \", primary\";\n if (field.unique) fieldDesc += \", unique\";\n fieldDesc += \")\";\n console.log(fieldDesc);\n }\n } else {\n console.log(\" (No fields to add)\");\n }\n }\n console.log(\"\");\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import { Connection, Database } from 'fm-odata-client';
1
+ import { Result } from 'neverthrow';
2
+ import { z } from 'zod/v4';
2
3
  export type BasicAuthCredentials = {
3
4
  username: string;
4
5
  password: string;
@@ -10,12 +11,94 @@ export type ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;
10
11
  export declare function isBasicAuth(auth: ODataAuth): auth is BasicAuthCredentials;
11
12
  export declare function isOttoAPIKeyAuth(auth: ODataAuth): auth is OttoAPIKeyAuth;
12
13
  export type FmOdataConfig = {
13
- hostname: string;
14
+ serverUrl: string;
14
15
  auth: ODataAuth;
15
16
  database: string;
17
+ logging?: true | "verbose" | "none";
16
18
  };
17
- export declare class FmOdata {
18
- connection: Connection;
19
- database: Database;
20
- constructor(args: FmOdataConfig);
21
- }
19
+ export declare function createFmOdataFetch(args: FmOdataConfig): import('@better-fetch/fetch').BetterFetch<{
20
+ baseURL: string;
21
+ auth: {
22
+ type: "Bearer";
23
+ token: string;
24
+ username?: undefined;
25
+ password?: undefined;
26
+ } | {
27
+ type: "Basic";
28
+ username: string;
29
+ password: string;
30
+ token?: undefined;
31
+ };
32
+ onError: (error: import('@better-fetch/fetch').ErrorContext) => void;
33
+ schema: {
34
+ schema: {
35
+ "@post/FileMaker_Tables": {
36
+ input: z.ZodObject<{
37
+ tableName: z.ZodString;
38
+ fields: z.ZodArray<z.ZodAny>;
39
+ }, z.core.$strip>;
40
+ };
41
+ "@patch/FileMaker_Tables/:tableName": {
42
+ params: z.ZodObject<{
43
+ tableName: z.ZodString;
44
+ }, z.core.$strip>;
45
+ input: z.ZodObject<{
46
+ fields: z.ZodArray<z.ZodAny>;
47
+ }, z.core.$strip>;
48
+ };
49
+ "@delete/FileMaker_Tables/:tableName": {
50
+ params: z.ZodObject<{
51
+ tableName: z.ZodString;
52
+ }, z.core.$strip>;
53
+ };
54
+ "@delete/FileMaker_Tables/:tableName/:fieldName": {
55
+ params: z.ZodObject<{
56
+ tableName: z.ZodString;
57
+ fieldName: z.ZodString;
58
+ }, z.core.$strip>;
59
+ };
60
+ };
61
+ config: import('@better-fetch/fetch').SchemaConfig;
62
+ };
63
+ plugins: {
64
+ id: string;
65
+ name: string;
66
+ version: string;
67
+ hooks: {
68
+ onRequest<T extends Record<string, any>>(context: import('@better-fetch/fetch').RequestContext<T>): void;
69
+ onSuccess(context: import('@better-fetch/fetch').SuccessContext<any>): Promise<void>;
70
+ onRetry(response: import('@better-fetch/fetch').ResponseContext): void;
71
+ onError(context: import('@better-fetch/fetch').ErrorContext): Promise<void>;
72
+ };
73
+ }[];
74
+ }, unknown, unknown, {
75
+ schema: {
76
+ "@post/FileMaker_Tables": {
77
+ input: z.ZodObject<{
78
+ tableName: z.ZodString;
79
+ fields: z.ZodArray<z.ZodAny>;
80
+ }, z.core.$strip>;
81
+ };
82
+ "@patch/FileMaker_Tables/:tableName": {
83
+ params: z.ZodObject<{
84
+ tableName: z.ZodString;
85
+ }, z.core.$strip>;
86
+ input: z.ZodObject<{
87
+ fields: z.ZodArray<z.ZodAny>;
88
+ }, z.core.$strip>;
89
+ };
90
+ "@delete/FileMaker_Tables/:tableName": {
91
+ params: z.ZodObject<{
92
+ tableName: z.ZodString;
93
+ }, z.core.$strip>;
94
+ };
95
+ "@delete/FileMaker_Tables/:tableName/:fieldName": {
96
+ params: z.ZodObject<{
97
+ tableName: z.ZodString;
98
+ fieldName: z.ZodString;
99
+ }, z.core.$strip>;
100
+ };
101
+ };
102
+ config: import('@better-fetch/fetch').SchemaConfig;
103
+ }>;
104
+ export declare function validateUrl(input: string): Result<URL, unknown>;
@@ -1,27 +1,83 @@
1
- var __defProp = Object.defineProperty;
2
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { Connection, BasicAuth, Database } from "fm-odata-client";
5
- function isOttoAPIKeyAuth(auth) {
6
- return typeof auth.apiKey === "string";
1
+ import { createSchema, createFetch } from "@better-fetch/fetch";
2
+ import { logger } from "@better-fetch/logger";
3
+ import { ok, err } from "neverthrow";
4
+ import { z } from "zod/v4";
5
+ const schema = createSchema({
6
+ /**
7
+ * Create a new table
8
+ */
9
+ "@post/FileMaker_Tables": {
10
+ input: z.object({ tableName: z.string(), fields: z.array(z.any()) })
11
+ },
12
+ /**
13
+ * Add fields to a table
14
+ */
15
+ "@patch/FileMaker_Tables/:tableName": {
16
+ params: z.object({ tableName: z.string() }),
17
+ input: z.object({ fields: z.array(z.any()) })
18
+ },
19
+ /**
20
+ * Delete a table
21
+ */
22
+ "@delete/FileMaker_Tables/:tableName": {
23
+ params: z.object({ tableName: z.string() })
24
+ },
25
+ /**
26
+ * Delete a field from a table
27
+ */
28
+ "@delete/FileMaker_Tables/:tableName/:fieldName": {
29
+ params: z.object({ tableName: z.string(), fieldName: z.string() })
30
+ }
31
+ });
32
+ function createFmOdataFetch(args) {
33
+ const result = validateUrl(args.serverUrl);
34
+ if (result.isErr()) {
35
+ throw new Error("Invalid server URL");
36
+ }
37
+ let baseURL = result.value.origin;
38
+ if ("apiKey" in args.auth) {
39
+ baseURL += `/otto`;
40
+ }
41
+ baseURL += `/fmi/odata/v4/${args.database}`;
42
+ return createFetch({
43
+ baseURL,
44
+ auth: "apiKey" in args.auth ? { type: "Bearer", token: args.auth.apiKey } : {
45
+ type: "Basic",
46
+ username: args.auth.username,
47
+ password: args.auth.password
48
+ },
49
+ onError: (error) => {
50
+ console.error("url", error.request.url.toString());
51
+ console.log(error.error);
52
+ console.log("error.request.body", JSON.stringify(error.request.body));
53
+ },
54
+ schema,
55
+ plugins: [
56
+ logger({
57
+ verbose: args.logging === "verbose",
58
+ enabled: args.logging === "verbose" || !!args.logging,
59
+ console: {
60
+ fail: (...args2) => console.error(...args2),
61
+ success: (...args2) => console.log(...args2),
62
+ log: (...args2) => console.log(...args2),
63
+ error: (...args2) => console.error(...args2),
64
+ warn: (...args2) => console.warn(...args2)
65
+ }
66
+ })
67
+ ]
68
+ });
7
69
  }
8
- class FmOdata {
9
- constructor(args) {
10
- __publicField(this, "connection");
11
- __publicField(this, "database");
12
- if (isOttoAPIKeyAuth(args.auth)) {
13
- throw new Error("Otto API key auth is yet not supported");
14
- } else {
15
- this.connection = new Connection(
16
- args.hostname.replace(/^https?:\/\//, "").replace(/\/$/, ""),
17
- new BasicAuth(args.auth.username, args.auth.password)
18
- );
19
- }
20
- this.database = new Database(this.connection, args.database);
70
+ function validateUrl(input) {
71
+ try {
72
+ const url = new URL(input);
73
+ return ok(url);
74
+ } catch (error) {
75
+ console.error(error);
76
+ return err(error);
21
77
  }
22
78
  }
23
79
  export {
24
- FmOdata,
25
- isOttoAPIKeyAuth
80
+ createFmOdataFetch,
81
+ validateUrl
26
82
  };
27
83
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../src/odata/index.ts"],"sourcesContent":["import { BasicAuth, Connection, Database } from \"fm-odata-client\";\n\nexport type BasicAuthCredentials = {\n username: string;\n password: string;\n};\nexport type OttoAPIKeyAuth = {\n apiKey: string;\n};\nexport type ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;\n\nexport function isBasicAuth(auth: ODataAuth): auth is BasicAuthCredentials {\n return (\n typeof (auth as BasicAuthCredentials).username === \"string\" &&\n typeof (auth as BasicAuthCredentials).password === \"string\"\n );\n}\n\nexport function isOttoAPIKeyAuth(auth: ODataAuth): auth is OttoAPIKeyAuth {\n return typeof (auth as OttoAPIKeyAuth).apiKey === \"string\";\n}\n\nexport type FmOdataConfig = {\n hostname: string;\n auth: ODataAuth;\n database: string;\n};\n\nexport class FmOdata {\n public connection: Connection;\n public database: Database;\n\n constructor(args: FmOdataConfig) {\n if (isOttoAPIKeyAuth(args.auth)) {\n throw new Error(\"Otto API key auth is yet not supported\");\n } else {\n this.connection = new Connection(\n args.hostname.replace(/^https?:\\/\\//, \"\").replace(/\\/$/, \"\"),\n new BasicAuth(args.auth.username, args.auth.password),\n );\n }\n\n this.database = new Database(this.connection, args.database);\n }\n}\n"],"names":[],"mappings":";;;;AAkBO,SAAS,iBAAiB,MAAyC;AACjE,SAAA,OAAQ,KAAwB,WAAW;AACpD;AAQO,MAAM,QAAQ;AAAA,EAInB,YAAY,MAAqB;AAH1B;AACA;AAGD,QAAA,iBAAiB,KAAK,IAAI,GAAG;AACzB,YAAA,IAAI,MAAM,wCAAwC;AAAA,IAAA,OACnD;AACL,WAAK,aAAa,IAAI;AAAA,QACpB,KAAK,SAAS,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,OAAO,EAAE;AAAA,QAC3D,IAAI,UAAU,KAAK,KAAK,UAAU,KAAK,KAAK,QAAQ;AAAA,MACtD;AAAA,IAAA;AAGF,SAAK,WAAW,IAAI,SAAS,KAAK,YAAY,KAAK,QAAQ;AAAA,EAAA;AAE/D;"}
1
+ {"version":3,"file":"index.js","sources":["../../../src/odata/index.ts"],"sourcesContent":["import { createFetch, createSchema } from \"@better-fetch/fetch\";\nimport { logger } from \"@better-fetch/logger\";\nimport { err, ok, Result } from \"neverthrow\";\nimport { z } from \"zod/v4\";\n\nexport type BasicAuthCredentials = {\n username: string;\n password: string;\n};\nexport type OttoAPIKeyAuth = {\n apiKey: string;\n};\nexport type ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;\n\nexport function isBasicAuth(auth: ODataAuth): auth is BasicAuthCredentials {\n return (\n typeof (auth as BasicAuthCredentials).username === \"string\" &&\n typeof (auth as BasicAuthCredentials).password === \"string\"\n );\n}\n\nexport function isOttoAPIKeyAuth(auth: ODataAuth): auth is OttoAPIKeyAuth {\n return typeof (auth as OttoAPIKeyAuth).apiKey === \"string\";\n}\n\nexport type FmOdataConfig = {\n serverUrl: string;\n auth: ODataAuth;\n database: string;\n logging?: true | \"verbose\" | \"none\";\n};\n\nconst schema = createSchema({\n /**\n * Create a new table\n */\n \"@post/FileMaker_Tables\": {\n input: z.object({ tableName: z.string(), fields: z.array(z.any()) }),\n },\n /**\n * Add fields to a table\n */\n \"@patch/FileMaker_Tables/:tableName\": {\n params: z.object({ tableName: z.string() }),\n input: z.object({ fields: z.array(z.any()) }),\n },\n /**\n * Delete a table\n */\n \"@delete/FileMaker_Tables/:tableName\": {\n params: z.object({ tableName: z.string() }),\n },\n /**\n * Delete a field from a table\n */\n \"@delete/FileMaker_Tables/:tableName/:fieldName\": {\n params: z.object({ tableName: z.string(), fieldName: z.string() }),\n },\n});\n\nexport function createFmOdataFetch(args: FmOdataConfig) {\n const result = validateUrl(args.serverUrl);\n\n if (result.isErr()) {\n throw new Error(\"Invalid server URL\");\n }\n let baseURL = result.value.origin;\n if (\"apiKey\" in args.auth) {\n baseURL += `/otto`;\n }\n baseURL += `/fmi/odata/v4/${args.database}`;\n\n return createFetch({\n baseURL,\n auth:\n \"apiKey\" in args.auth\n ? { type: \"Bearer\", token: args.auth.apiKey }\n : {\n type: \"Basic\",\n username: args.auth.username,\n password: args.auth.password,\n },\n onError: (error) => {\n console.error(\"url\", error.request.url.toString());\n console.log(error.error);\n console.log(\"error.request.body\", JSON.stringify(error.request.body));\n },\n schema,\n plugins: [\n logger({\n verbose: args.logging === \"verbose\",\n enabled: args.logging === \"verbose\" || !!args.logging,\n console: {\n fail: (...args) => console.error(...args),\n success: (...args) => console.log(...args),\n log: (...args) => console.log(...args),\n error: (...args) => console.error(...args),\n warn: (...args) => console.warn(...args),\n },\n }),\n ],\n });\n}\n\nexport function validateUrl(input: string): Result<URL, unknown> {\n try {\n const url = new URL(input);\n return ok(url);\n } catch (error) {\n console.error(error);\n return err(error);\n }\n}\n"],"names":["args"],"mappings":";;;;AAgCA,MAAM,SAAS,aAAa;AAAA;AAAA;AAAA;AAAA,EAI1B,0BAA0B;AAAA,IACxB,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,QAAQ,EAAE,MAAM,EAAE,IAAK,CAAA,EAAG,CAAA;AAAA,EACrE;AAAA;AAAA;AAAA;AAAA,EAIA,sCAAsC;AAAA,IACpC,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,OAAA,GAAU;AAAA,IAC1C,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAA,CAAK,EAAG,CAAA;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA,EAIA,uCAAuC;AAAA,IACrC,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,SAAU,CAAA;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA,EAIA,kDAAkD;AAAA,IAChD,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,GAAG,WAAW,EAAE,SAAU,CAAA;AAAA,EAAA;AAErE,CAAC;AAEM,SAAS,mBAAmB,MAAqB;AAChD,QAAA,SAAS,YAAY,KAAK,SAAS;AAErC,MAAA,OAAO,SAAS;AACZ,UAAA,IAAI,MAAM,oBAAoB;AAAA,EAAA;AAElC,MAAA,UAAU,OAAO,MAAM;AACvB,MAAA,YAAY,KAAK,MAAM;AACd,eAAA;AAAA,EAAA;AAEF,aAAA,iBAAiB,KAAK,QAAQ;AAEzC,SAAO,YAAY;AAAA,IACjB;AAAA,IACA,MACE,YAAY,KAAK,OACb,EAAE,MAAM,UAAU,OAAO,KAAK,KAAK,OAAA,IACnC;AAAA,MACE,MAAM;AAAA,MACN,UAAU,KAAK,KAAK;AAAA,MACpB,UAAU,KAAK,KAAK;AAAA,IACtB;AAAA,IACN,SAAS,CAAC,UAAU;AAClB,cAAQ,MAAM,OAAO,MAAM,QAAQ,IAAI,UAAU;AACzC,cAAA,IAAI,MAAM,KAAK;AACvB,cAAQ,IAAI,sBAAsB,KAAK,UAAU,MAAM,QAAQ,IAAI,CAAC;AAAA,IACtE;AAAA,IACA;AAAA,IACA,SAAS;AAAA,MACP,OAAO;AAAA,QACL,SAAS,KAAK,YAAY;AAAA,QAC1B,SAAS,KAAK,YAAY,aAAa,CAAC,CAAC,KAAK;AAAA,QAC9C,SAAS;AAAA,UACP,MAAM,IAAIA,UAAS,QAAQ,MAAM,GAAGA,KAAI;AAAA,UACxC,SAAS,IAAIA,UAAS,QAAQ,IAAI,GAAGA,KAAI;AAAA,UACzC,KAAK,IAAIA,UAAS,QAAQ,IAAI,GAAGA,KAAI;AAAA,UACrC,OAAO,IAAIA,UAAS,QAAQ,MAAM,GAAGA,KAAI;AAAA,UACzC,MAAM,IAAIA,UAAS,QAAQ,KAAK,GAAGA,KAAI;AAAA,QAAA;AAAA,MAE1C,CAAA;AAAA,IAAA;AAAA,EACH,CACD;AACH;AAEO,SAAS,YAAY,OAAqC;AAC3D,MAAA;AACI,UAAA,MAAM,IAAI,IAAI,KAAK;AACzB,WAAO,GAAG,GAAG;AAAA,WACN,OAAO;AACd,YAAQ,MAAM,KAAK;AACnB,WAAO,IAAI,KAAK;AAAA,EAAA;AAEpB;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proofkit/better-auth",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-beta.0",
4
4
  "description": "FileMaker adapter for Better Auth",
5
5
  "type": "module",
6
6
  "main": "dist/esm/index.js",
@@ -38,6 +38,8 @@
38
38
  "dependencies": {
39
39
  "@babel/preset-react": "^7.27.1",
40
40
  "@babel/preset-typescript": "^7.27.1",
41
+ "@better-fetch/fetch": "1.1.17",
42
+ "@better-fetch/logger": "^1.1.18",
41
43
  "@commander-js/extra-typings": "^14.0.0",
42
44
  "@tanstack/vite-config": "^0.2.0",
43
45
  "better-auth": "^1.2.10",
@@ -45,17 +47,16 @@
45
47
  "chalk": "5.4.1",
46
48
  "commander": "^14.0.0",
47
49
  "dotenv": "^16.5.0",
48
- "execa": "^9.5.3",
49
- "fm-odata-client": "^3.0.1",
50
50
  "fs-extra": "^11.3.0",
51
+ "neverthrow": "^8.2.0",
51
52
  "prompts": "^2.4.2",
52
53
  "vite": "^6.3.4",
53
- "zod": "3.25.64",
54
- "@proofkit/fmdapi": "5.0.0"
54
+ "zod": "3.25.64"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/fs-extra": "^11.0.4",
58
58
  "@types/prompts": "^2.4.9",
59
+ "fm-odata-client": "^3.0.1",
59
60
  "publint": "^0.3.12",
60
61
  "typescript": "^5.8.3",
61
62
  "vitest": "^3.2.3"
package/src/adapter.ts CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createAdapter,
4
4
  type AdapterDebugLogs,
5
5
  } from "better-auth/adapters";
6
- import { FmOdata, type FmOdataConfig } from "./odata";
6
+ import { createFmOdataFetch, type FmOdataConfig } from "./odata";
7
7
  import { prettifyError, z } from "zod/v4";
8
8
 
9
9
  interface FileMakerAdapterConfig {
@@ -30,7 +30,7 @@ const configSchema = z.object({
30
30
  debugLogs: z.unknown().optional(),
31
31
  usePlural: z.boolean().optional(),
32
32
  odata: z.object({
33
- hostname: z.string(),
33
+ serverUrl: z.string(),
34
34
  auth: z.object({ username: z.string(), password: z.string() }),
35
35
  database: z.string().endsWith(".fmp12"),
36
36
  }),
@@ -40,7 +40,7 @@ const defaultConfig: Required<FileMakerAdapterConfig> = {
40
40
  debugLogs: false,
41
41
  usePlural: false,
42
42
  odata: {
43
- hostname: "",
43
+ serverUrl: "",
44
44
  auth: { username: "", password: "" },
45
45
  database: "",
46
46
  },
@@ -71,7 +71,7 @@ export function parseWhere(where?: CleanedWhere[]): string {
71
71
  if (typeof value === "boolean") return value ? "true" : "false";
72
72
  if (value instanceof Date) return `'${value.toISOString()}'`;
73
73
  if (Array.isArray(value)) return `(${value.map(formatValue).join(",")})`;
74
- return value.toString();
74
+ return value?.toString() ?? "";
75
75
  }
76
76
 
77
77
  // Map our operators to OData
@@ -139,9 +139,10 @@ export const FileMakerAdapter = (
139
139
  }
140
140
  const config = parsed.data;
141
141
 
142
- const odata =
143
- config.odata instanceof FmOdata ? config.odata : new FmOdata(config.odata);
144
- const db = odata.database;
142
+ const fetch = createFmOdataFetch({
143
+ ...config.odata,
144
+ // logging: config.debugLogs ? true : "none",
145
+ });
145
146
 
146
147
  return createAdapter({
147
148
  config: {
@@ -158,62 +159,116 @@ export const FileMakerAdapter = (
158
159
  return {
159
160
  options: { config },
160
161
  create: async ({ data, model, select }) => {
161
- const row = await db.table(model).create(data);
162
- return row as unknown as typeof data;
162
+ const result = await fetch(`/${model}`, {
163
+ method: "POST",
164
+ body: data,
165
+ output: z.looseObject({ id: z.string() }),
166
+ });
167
+
168
+ if (result.error) {
169
+ throw new Error("Failed to create record");
170
+ }
171
+
172
+ return result.data as any;
163
173
  },
164
174
  count: async ({ model, where }) => {
165
- const count = await db.table(model).count(parseWhere(where));
166
- return count;
175
+ const result = await fetch(`/${model}/$count`, {
176
+ method: "GET",
177
+ query: {
178
+ $filter: parseWhere(where),
179
+ },
180
+ output: z.object({ value: z.number() }),
181
+ });
182
+ if (!result.data) {
183
+ throw new Error("Failed to count records");
184
+ }
185
+ return result.data?.value ?? 0;
167
186
  },
168
187
  findOne: async ({ model, where }) => {
169
- const row = await db.table(model).query({
170
- filter: parseWhere(where),
171
- top: 1,
188
+ const result = await fetch(`/${model}`, {
189
+ method: "GET",
190
+ query: {
191
+ ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
192
+ $top: 1,
193
+ },
194
+ output: z.object({ value: z.array(z.any()) }),
172
195
  });
173
- return (row[0] as any) ?? null;
196
+ if (result.error) {
197
+ throw new Error("Failed to find record");
198
+ }
199
+ return result.data?.value?.[0] ?? null;
174
200
  },
175
201
  findMany: async ({ model, where, limit, offset, sortBy }) => {
176
202
  const filter = parseWhere(where);
177
203
 
178
- const rows = await db.table(model).query({
179
- filter,
180
- top: limit,
181
- skip: offset,
182
- orderBy: sortBy,
204
+ const rows = await fetch(`/${model}`, {
205
+ method: "GET",
206
+ query: {
207
+ ...(filter.length > 0 ? { $filter: filter } : {}),
208
+ $top: limit,
209
+ $skip: offset,
210
+ ...(sortBy
211
+ ? { $orderby: `"${sortBy.field}" ${sortBy.direction ?? "asc"}` }
212
+ : {}),
213
+ },
214
+ output: z.object({ value: z.array(z.any()) }),
183
215
  });
184
- return rows.map((row) => row as any);
216
+ if (rows.error) {
217
+ throw new Error("Failed to find records");
218
+ }
219
+ return rows.data?.value ?? [];
185
220
  },
186
221
  delete: async ({ model, where }) => {
187
- const rows = await db.table(model).query({
188
- filter: parseWhere(where),
189
- top: 1,
190
- select: [`"id"`],
222
+ const result = await fetch(`/${model}`, {
223
+ method: "DELETE",
224
+ query: {
225
+ ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
226
+ $top: 1,
227
+ $select: [`"id"`],
228
+ },
191
229
  });
192
- const row = rows[0] as { id: string } | undefined;
193
- if (!row) return;
194
- await db.table(model).delete(row.id);
230
+ if (result.error) {
231
+ throw new Error("Failed to delete record");
232
+ }
195
233
  },
196
234
  deleteMany: async ({ model, where }) => {
197
- const filter = parseWhere(where);
198
- const count = await db.table(model).count(filter);
199
- await db.table(model).deleteMany(filter);
200
- return count;
235
+ const result = await fetch(`/${model}/$count`, {
236
+ method: "DELETE",
237
+ query: {
238
+ ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
239
+ $top: 1,
240
+ $select: [`"id"`],
241
+ },
242
+ output: z.coerce.number(),
243
+ });
244
+ if (result.error) {
245
+ throw new Error("Failed to delete record");
246
+ }
247
+ return result.data ?? 0;
201
248
  },
202
249
  update: async ({ model, where, update }) => {
203
- const rows = await db.table(model).query({
204
- filter: parseWhere(where),
205
- top: 1,
206
- select: [`"id"`],
250
+ const result = await fetch(`/${model}`, {
251
+ method: "PATCH",
252
+ query: {
253
+ ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
254
+ $top: 1,
255
+ $select: [`"id"`],
256
+ },
257
+ body: update,
258
+ output: z.object({ value: z.array(z.any()) }),
207
259
  });
208
- const row = rows[0] as { id: string } | undefined;
209
- if (!row) return null;
210
- const result = await db.table(model).update(row["id"], update as any);
211
- return result as any;
260
+ return result.data?.value?.[0] ?? null;
212
261
  },
213
262
  updateMany: async ({ model, where, update }) => {
214
263
  const filter = parseWhere(where);
215
- const rows = await db.table(model).updateMany(filter, update as any);
216
- return rows.length;
264
+ const result = await fetch(`/${model}`, {
265
+ method: "PATCH",
266
+ query: {
267
+ ...(where.length > 0 ? { $filter: filter } : {}),
268
+ },
269
+ body: update,
270
+ });
271
+ return result.data as any;
217
272
  },
218
273
  };
219
274
  },
package/src/cli/index.ts CHANGED
@@ -14,7 +14,7 @@ import { BasicAuth, Connection, Database } from "fm-odata-client";
14
14
  import prompts from "prompts";
15
15
  import chalk from "chalk";
16
16
  import { AdapterOptions } from "../adapter";
17
- import { FmOdata } from "../odata";
17
+ import { createFmOdataFetch } from "../odata";
18
18
 
19
19
  async function main() {
20
20
  const program = new Command();
@@ -64,7 +64,7 @@ async function main() {
64
64
  const betterAuthSchema = getAuthTables(config);
65
65
 
66
66
  const adapterConfig = (adapter.options as AdapterOptions).config;
67
- const db = new FmOdata({
67
+ const fetch = createFmOdataFetch({
68
68
  ...adapterConfig.odata,
69
69
  auth:
70
70
  // If the username and password are provided in the CLI, use them to authenticate instead of what's in the config file.
@@ -74,9 +74,13 @@ async function main() {
74
74
  password: options.password,
75
75
  }
76
76
  : adapterConfig.odata.auth,
77
- }).database;
77
+ });
78
78
 
79
- const migrationPlan = await planMigration(db, betterAuthSchema);
79
+ const migrationPlan = await planMigration(
80
+ fetch,
81
+ betterAuthSchema,
82
+ adapterConfig.odata.database,
83
+ );
80
84
 
81
85
  if (migrationPlan.length === 0) {
82
86
  logger.info("No changes to apply. Database is up to date.");
@@ -105,7 +109,7 @@ async function main() {
105
109
  }
106
110
  }
107
111
 
108
- await executeMigration(db, migrationPlan);
112
+ await executeMigration(fetch, migrationPlan);
109
113
 
110
114
  logger.info("Migration applied successfully.");
111
115
  });
package/src/migrate.ts CHANGED
@@ -1,16 +1,30 @@
1
1
  import { type BetterAuthDbSchema } from "better-auth/db";
2
- import { Database, type Field as FmField } from "fm-odata-client";
2
+ import { type Metadata, type Field as FmField } from "fm-odata-client";
3
3
  import chalk from "chalk";
4
4
  import z from "zod/v4";
5
+ import { createFmOdataFetch } from "./odata";
6
+
7
+ export async function getMetadata(
8
+ fetch: ReturnType<typeof createFmOdataFetch>,
9
+ databaseName: string,
10
+ ) {
11
+ const result = await fetch("/$metadata", {
12
+ method: "GET",
13
+ headers: { accept: "application/json" },
14
+ output: z.looseObject({
15
+ $Version: z.string(),
16
+ "@ServerVersion": z.string(),
17
+ }),
18
+ });
19
+ return result.data?.[databaseName] as Metadata;
20
+ }
5
21
 
6
22
  export async function planMigration(
7
- db: Database,
23
+ fetch: ReturnType<typeof createFmOdataFetch>,
8
24
  betterAuthSchema: BetterAuthDbSchema,
25
+ databaseName: string,
9
26
  ): Promise<MigrationPlan> {
10
- const metadata = await db.getMetadata().catch((error) => {
11
- console.error("Failed to get metadata from database", error);
12
- return null;
13
- });
27
+ const metadata = await getMetadata(fetch, databaseName);
14
28
 
15
29
  // Build a map from entity set name to entity type key
16
30
  let entitySetToType: Record<string, string> = {};
@@ -137,16 +151,24 @@ export async function planMigration(
137
151
  }
138
152
 
139
153
  export async function executeMigration(
140
- db: Database,
154
+ fetch: ReturnType<typeof createFmOdataFetch>,
141
155
  migrationPlan: MigrationPlan,
142
156
  ) {
143
157
  for (const step of migrationPlan) {
144
158
  if (step.operation === "create") {
145
159
  console.log("Creating table:", step.tableName);
146
- await db.schemaManager().createTable(step.tableName, step.fields);
160
+ await fetch("@post/FileMaker_Tables", {
161
+ body: {
162
+ tableName: step.tableName,
163
+ fields: step.fields,
164
+ },
165
+ });
147
166
  } else if (step.operation === "update") {
148
167
  console.log("Adding fields to table:", step.tableName);
149
- await db.schemaManager().addFields(step.tableName, step.fields);
168
+ await fetch("@post/FileMaker_Tables/:tableName", {
169
+ params: { tableName: step.tableName },
170
+ body: { fields: step.fields },
171
+ });
150
172
  }
151
173
  }
152
174
  }
@@ -1,4 +1,7 @@
1
- import { BasicAuth, Connection, Database } from "fm-odata-client";
1
+ import { createFetch, createSchema } from "@better-fetch/fetch";
2
+ import { logger } from "@better-fetch/logger";
3
+ import { err, ok, Result } from "neverthrow";
4
+ import { z } from "zod/v4";
2
5
 
3
6
  export type BasicAuthCredentials = {
4
7
  username: string;
@@ -21,25 +24,90 @@ export function isOttoAPIKeyAuth(auth: ODataAuth): auth is OttoAPIKeyAuth {
21
24
  }
22
25
 
23
26
  export type FmOdataConfig = {
24
- hostname: string;
27
+ serverUrl: string;
25
28
  auth: ODataAuth;
26
29
  database: string;
30
+ logging?: true | "verbose" | "none";
27
31
  };
28
32
 
29
- export class FmOdata {
30
- public connection: Connection;
31
- public database: Database;
32
-
33
- constructor(args: FmOdataConfig) {
34
- if (isOttoAPIKeyAuth(args.auth)) {
35
- throw new Error("Otto API key auth is yet not supported");
36
- } else {
37
- this.connection = new Connection(
38
- args.hostname.replace(/^https?:\/\//, "").replace(/\/$/, ""),
39
- new BasicAuth(args.auth.username, args.auth.password),
40
- );
41
- }
42
-
43
- this.database = new Database(this.connection, args.database);
33
+ const schema = createSchema({
34
+ /**
35
+ * Create a new table
36
+ */
37
+ "@post/FileMaker_Tables": {
38
+ input: z.object({ tableName: z.string(), fields: z.array(z.any()) }),
39
+ },
40
+ /**
41
+ * Add fields to a table
42
+ */
43
+ "@patch/FileMaker_Tables/:tableName": {
44
+ params: z.object({ tableName: z.string() }),
45
+ input: z.object({ fields: z.array(z.any()) }),
46
+ },
47
+ /**
48
+ * Delete a table
49
+ */
50
+ "@delete/FileMaker_Tables/:tableName": {
51
+ params: z.object({ tableName: z.string() }),
52
+ },
53
+ /**
54
+ * Delete a field from a table
55
+ */
56
+ "@delete/FileMaker_Tables/:tableName/:fieldName": {
57
+ params: z.object({ tableName: z.string(), fieldName: z.string() }),
58
+ },
59
+ });
60
+
61
+ export function createFmOdataFetch(args: FmOdataConfig) {
62
+ const result = validateUrl(args.serverUrl);
63
+
64
+ if (result.isErr()) {
65
+ throw new Error("Invalid server URL");
66
+ }
67
+ let baseURL = result.value.origin;
68
+ if ("apiKey" in args.auth) {
69
+ baseURL += `/otto`;
70
+ }
71
+ baseURL += `/fmi/odata/v4/${args.database}`;
72
+
73
+ return createFetch({
74
+ baseURL,
75
+ auth:
76
+ "apiKey" in args.auth
77
+ ? { type: "Bearer", token: args.auth.apiKey }
78
+ : {
79
+ type: "Basic",
80
+ username: args.auth.username,
81
+ password: args.auth.password,
82
+ },
83
+ onError: (error) => {
84
+ console.error("url", error.request.url.toString());
85
+ console.log(error.error);
86
+ console.log("error.request.body", JSON.stringify(error.request.body));
87
+ },
88
+ schema,
89
+ plugins: [
90
+ logger({
91
+ verbose: args.logging === "verbose",
92
+ enabled: args.logging === "verbose" || !!args.logging,
93
+ console: {
94
+ fail: (...args) => console.error(...args),
95
+ success: (...args) => console.log(...args),
96
+ log: (...args) => console.log(...args),
97
+ error: (...args) => console.error(...args),
98
+ warn: (...args) => console.warn(...args),
99
+ },
100
+ }),
101
+ ],
102
+ });
103
+ }
104
+
105
+ export function validateUrl(input: string): Result<URL, unknown> {
106
+ try {
107
+ const url = new URL(input);
108
+ return ok(url);
109
+ } catch (error) {
110
+ console.error(error);
111
+ return err(error);
44
112
  }
45
113
  }