@proofkit/better-auth 0.1.0 → 0.2.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,9 +1,23 @@
1
1
  import { CleanedWhere, AdapterDebugLogs } from 'better-auth/adapters';
2
- import { FmOdataConfig } from './odata.js';
2
+ import { z } from 'zod/v4';
3
+ declare const configSchema: z.ZodObject<{
4
+ debugLogs: z.ZodOptional<z.ZodUnknown>;
5
+ usePlural: z.ZodOptional<z.ZodBoolean>;
6
+ odata: z.ZodObject<{
7
+ serverUrl: z.ZodURL;
8
+ auth: z.ZodUnion<readonly [z.ZodObject<{
9
+ username: z.ZodString;
10
+ password: z.ZodString;
11
+ }, z.core.$strip>, z.ZodObject<{
12
+ apiKey: z.ZodString;
13
+ }, z.core.$strip>]>;
14
+ database: z.ZodString;
15
+ }, z.core.$strip>;
16
+ }, z.core.$strip>;
3
17
  interface FileMakerAdapterConfig {
4
18
  debugLogs?: AdapterDebugLogs;
5
19
  usePlural?: boolean;
6
- odata: FmOdataConfig;
20
+ odata: z.infer<typeof configSchema>["odata"];
7
21
  }
8
22
  export type AdapterOptions = {
9
23
  config: FileMakerAdapterConfig;
@@ -1,12 +1,15 @@
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(),
9
- auth: z.object({ username: z.string(), password: z.string() }),
8
+ serverUrl: z.url(),
9
+ auth: z.union([
10
+ z.object({ username: z.string(), password: z.string() }),
11
+ z.object({ apiKey: z.string() })
12
+ ]),
10
13
  database: z.string().endsWith(".fmp12")
11
14
  })
12
15
  });
@@ -14,7 +17,7 @@ const defaultConfig = {
14
17
  debugLogs: false,
15
18
  usePlural: false,
16
19
  odata: {
17
- hostname: "",
20
+ serverUrl: "",
18
21
  auth: { username: "", password: "" },
19
22
  database: ""
20
23
  }
@@ -32,7 +35,7 @@ function parseWhere(where) {
32
35
  if (typeof value === "boolean") return value ? "true" : "false";
33
36
  if (value instanceof Date) return `'${value.toISOString()}'`;
34
37
  if (Array.isArray(value)) return `(${value.map(formatValue).join(",")})`;
35
- return value.toString();
38
+ return (value == null ? void 0 : value.toString()) ?? "";
36
39
  }
37
40
  const opMap = {
38
41
  eq: "eq",
@@ -88,8 +91,10 @@ const FileMakerAdapter = (_config = defaultConfig) => {
88
91
  throw new Error(`Invalid configuration: ${prettifyError(parsed.error)}`);
89
92
  }
90
93
  const config = parsed.data;
91
- const odata = config.odata instanceof FmOdata ? config.odata : new FmOdata(config.odata);
92
- const db = odata.database;
94
+ const fetch = createFmOdataFetch({
95
+ ...config.odata
96
+ // logging: config.debugLogs ? true : "none",
97
+ });
93
98
  return createAdapter({
94
99
  config: {
95
100
  adapterId: "filemaker",
@@ -111,61 +116,115 @@ const FileMakerAdapter = (_config = defaultConfig) => {
111
116
  return {
112
117
  options: { config },
113
118
  create: async ({ data, model, select }) => {
114
- const row = await db.table(model).create(data);
115
- return row;
119
+ const result = await fetch(`/${model}`, {
120
+ method: "POST",
121
+ body: data,
122
+ output: z.looseObject({ id: z.string() })
123
+ });
124
+ if (result.error) {
125
+ throw new Error("Failed to create record");
126
+ }
127
+ return result.data;
116
128
  },
117
129
  count: async ({ model, where }) => {
118
- const count = await db.table(model).count(parseWhere(where));
119
- return count;
130
+ var _a;
131
+ const result = await fetch(`/${model}/$count`, {
132
+ method: "GET",
133
+ query: {
134
+ $filter: parseWhere(where)
135
+ },
136
+ output: z.object({ value: z.number() })
137
+ });
138
+ if (!result.data) {
139
+ throw new Error("Failed to count records");
140
+ }
141
+ return ((_a = result.data) == null ? void 0 : _a.value) ?? 0;
120
142
  },
121
143
  findOne: async ({ model, where }) => {
122
- const row = await db.table(model).query({
123
- filter: parseWhere(where),
124
- top: 1
144
+ var _a, _b;
145
+ const result = await fetch(`/${model}`, {
146
+ method: "GET",
147
+ query: {
148
+ ...where.length > 0 ? { $filter: parseWhere(where) } : {},
149
+ $top: 1
150
+ },
151
+ output: z.object({ value: z.array(z.any()) })
125
152
  });
126
- return row[0] ?? null;
153
+ if (result.error) {
154
+ throw new Error("Failed to find record");
155
+ }
156
+ return ((_b = (_a = result.data) == null ? void 0 : _a.value) == null ? void 0 : _b[0]) ?? null;
127
157
  },
128
158
  findMany: async ({ model, where, limit, offset, sortBy }) => {
159
+ var _a;
129
160
  const filter = parseWhere(where);
130
- const rows = await db.table(model).query({
131
- filter,
132
- top: limit,
133
- skip: offset,
134
- orderBy: sortBy
161
+ const rows = await fetch(`/${model}`, {
162
+ method: "GET",
163
+ query: {
164
+ ...filter.length > 0 ? { $filter: filter } : {},
165
+ $top: limit,
166
+ $skip: offset,
167
+ ...sortBy ? { $orderby: `"${sortBy.field}" ${sortBy.direction ?? "asc"}` } : {}
168
+ },
169
+ output: z.object({ value: z.array(z.any()) })
135
170
  });
136
- return rows.map((row) => row);
171
+ if (rows.error) {
172
+ throw new Error("Failed to find records");
173
+ }
174
+ return ((_a = rows.data) == null ? void 0 : _a.value) ?? [];
137
175
  },
138
176
  delete: async ({ model, where }) => {
139
- const rows = await db.table(model).query({
140
- filter: parseWhere(where),
141
- top: 1,
142
- select: [`"id"`]
177
+ const result = await fetch(`/${model}`, {
178
+ method: "DELETE",
179
+ query: {
180
+ ...where.length > 0 ? { $filter: parseWhere(where) } : {},
181
+ $top: 1,
182
+ $select: [`"id"`]
183
+ }
143
184
  });
144
- const row = rows[0];
145
- if (!row) return;
146
- await db.table(model).delete(row.id);
185
+ if (result.error) {
186
+ throw new Error("Failed to delete record");
187
+ }
147
188
  },
148
189
  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;
190
+ const result = await fetch(`/${model}/$count`, {
191
+ method: "DELETE",
192
+ query: {
193
+ ...where.length > 0 ? { $filter: parseWhere(where) } : {},
194
+ $top: 1,
195
+ $select: [`"id"`]
196
+ },
197
+ output: z.coerce.number()
198
+ });
199
+ if (result.error) {
200
+ throw new Error("Failed to delete record");
201
+ }
202
+ return result.data ?? 0;
153
203
  },
154
204
  update: async ({ model, where, update }) => {
155
- const rows = await db.table(model).query({
156
- filter: parseWhere(where),
157
- top: 1,
158
- select: [`"id"`]
205
+ var _a, _b;
206
+ const result = await fetch(`/${model}`, {
207
+ method: "PATCH",
208
+ query: {
209
+ ...where.length > 0 ? { $filter: parseWhere(where) } : {},
210
+ $top: 1,
211
+ $select: [`"id"`]
212
+ },
213
+ body: update,
214
+ output: z.object({ value: z.array(z.any()) })
159
215
  });
160
- const row = rows[0];
161
- if (!row) return null;
162
- const result = await db.table(model).update(row["id"], update);
163
- return result;
216
+ return ((_b = (_a = result.data) == null ? void 0 : _a.value) == null ? void 0 : _b[0]) ?? null;
164
217
  },
165
218
  updateMany: async ({ model, where, update }) => {
166
219
  const filter = parseWhere(where);
167
- const rows = await db.table(model).updateMany(filter, update);
168
- return rows.length;
220
+ const result = await fetch(`/${model}`, {
221
+ method: "PATCH",
222
+ query: {
223
+ ...where.length > 0 ? { $filter: filter } : {}
224
+ },
225
+ body: update
226
+ });
227
+ return result.data;
169
228
  }
170
229
  };
171
230
  }
@@ -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\nconst configSchema = z.object({\n debugLogs: z.unknown().optional(),\n usePlural: z.boolean().optional(),\n odata: z.object({\n serverUrl: z.url(),\n auth: z.union([\n z.object({ username: z.string(), password: z.string() }),\n z.object({ apiKey: z.string() }),\n ]),\n database: z.string().endsWith(\".fmp12\"),\n }),\n});\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: z.infer<typeof configSchema>[\"odata\"];\n}\n\nexport type AdapterOptions = {\n config: FileMakerAdapterConfig;\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,82 @@
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
+ return err(error);
21
76
  }
22
77
  }
23
78
  export {
24
- FmOdata,
25
- isOttoAPIKeyAuth
79
+ createFmOdataFetch,
80
+ validateUrl
26
81
  };
27
82
  //# 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 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,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",
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,9 +3,22 @@ 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
+ const configSchema = z.object({
10
+ debugLogs: z.unknown().optional(),
11
+ usePlural: z.boolean().optional(),
12
+ odata: z.object({
13
+ serverUrl: z.url(),
14
+ auth: z.union([
15
+ z.object({ username: z.string(), password: z.string() }),
16
+ z.object({ apiKey: z.string() }),
17
+ ]),
18
+ database: z.string().endsWith(".fmp12"),
19
+ }),
20
+ });
21
+
9
22
  interface FileMakerAdapterConfig {
10
23
  /**
11
24
  * Helps you debug issues with the adapter.
@@ -19,28 +32,18 @@ interface FileMakerAdapterConfig {
19
32
  /**
20
33
  * Connection details for the FileMaker server.
21
34
  */
22
- odata: FmOdataConfig;
35
+ odata: z.infer<typeof configSchema>["odata"];
23
36
  }
24
37
 
25
38
  export type AdapterOptions = {
26
39
  config: FileMakerAdapterConfig;
27
40
  };
28
41
 
29
- const configSchema = z.object({
30
- debugLogs: z.unknown().optional(),
31
- usePlural: z.boolean().optional(),
32
- odata: z.object({
33
- hostname: z.string(),
34
- auth: z.object({ username: z.string(), password: z.string() }),
35
- database: z.string().endsWith(".fmp12"),
36
- }),
37
- });
38
-
39
42
  const defaultConfig: Required<FileMakerAdapterConfig> = {
40
43
  debugLogs: false,
41
44
  usePlural: false,
42
45
  odata: {
43
- hostname: "",
46
+ serverUrl: "",
44
47
  auth: { username: "", password: "" },
45
48
  database: "",
46
49
  },
@@ -71,7 +74,7 @@ export function parseWhere(where?: CleanedWhere[]): string {
71
74
  if (typeof value === "boolean") return value ? "true" : "false";
72
75
  if (value instanceof Date) return `'${value.toISOString()}'`;
73
76
  if (Array.isArray(value)) return `(${value.map(formatValue).join(",")})`;
74
- return value.toString();
77
+ return value?.toString() ?? "";
75
78
  }
76
79
 
77
80
  // Map our operators to OData
@@ -139,9 +142,10 @@ export const FileMakerAdapter = (
139
142
  }
140
143
  const config = parsed.data;
141
144
 
142
- const odata =
143
- config.odata instanceof FmOdata ? config.odata : new FmOdata(config.odata);
144
- const db = odata.database;
145
+ const fetch = createFmOdataFetch({
146
+ ...config.odata,
147
+ // logging: config.debugLogs ? true : "none",
148
+ });
145
149
 
146
150
  return createAdapter({
147
151
  config: {
@@ -158,62 +162,116 @@ export const FileMakerAdapter = (
158
162
  return {
159
163
  options: { config },
160
164
  create: async ({ data, model, select }) => {
161
- const row = await db.table(model).create(data);
162
- return row as unknown as typeof data;
165
+ const result = await fetch(`/${model}`, {
166
+ method: "POST",
167
+ body: data,
168
+ output: z.looseObject({ id: z.string() }),
169
+ });
170
+
171
+ if (result.error) {
172
+ throw new Error("Failed to create record");
173
+ }
174
+
175
+ return result.data as any;
163
176
  },
164
177
  count: async ({ model, where }) => {
165
- const count = await db.table(model).count(parseWhere(where));
166
- return count;
178
+ const result = await fetch(`/${model}/$count`, {
179
+ method: "GET",
180
+ query: {
181
+ $filter: parseWhere(where),
182
+ },
183
+ output: z.object({ value: z.number() }),
184
+ });
185
+ if (!result.data) {
186
+ throw new Error("Failed to count records");
187
+ }
188
+ return result.data?.value ?? 0;
167
189
  },
168
190
  findOne: async ({ model, where }) => {
169
- const row = await db.table(model).query({
170
- filter: parseWhere(where),
171
- top: 1,
191
+ const result = await fetch(`/${model}`, {
192
+ method: "GET",
193
+ query: {
194
+ ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
195
+ $top: 1,
196
+ },
197
+ output: z.object({ value: z.array(z.any()) }),
172
198
  });
173
- return (row[0] as any) ?? null;
199
+ if (result.error) {
200
+ throw new Error("Failed to find record");
201
+ }
202
+ return result.data?.value?.[0] ?? null;
174
203
  },
175
204
  findMany: async ({ model, where, limit, offset, sortBy }) => {
176
205
  const filter = parseWhere(where);
177
206
 
178
- const rows = await db.table(model).query({
179
- filter,
180
- top: limit,
181
- skip: offset,
182
- orderBy: sortBy,
207
+ const rows = await fetch(`/${model}`, {
208
+ method: "GET",
209
+ query: {
210
+ ...(filter.length > 0 ? { $filter: filter } : {}),
211
+ $top: limit,
212
+ $skip: offset,
213
+ ...(sortBy
214
+ ? { $orderby: `"${sortBy.field}" ${sortBy.direction ?? "asc"}` }
215
+ : {}),
216
+ },
217
+ output: z.object({ value: z.array(z.any()) }),
183
218
  });
184
- return rows.map((row) => row as any);
219
+ if (rows.error) {
220
+ throw new Error("Failed to find records");
221
+ }
222
+ return rows.data?.value ?? [];
185
223
  },
186
224
  delete: async ({ model, where }) => {
187
- const rows = await db.table(model).query({
188
- filter: parseWhere(where),
189
- top: 1,
190
- select: [`"id"`],
225
+ const result = await fetch(`/${model}`, {
226
+ method: "DELETE",
227
+ query: {
228
+ ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
229
+ $top: 1,
230
+ $select: [`"id"`],
231
+ },
191
232
  });
192
- const row = rows[0] as { id: string } | undefined;
193
- if (!row) return;
194
- await db.table(model).delete(row.id);
233
+ if (result.error) {
234
+ throw new Error("Failed to delete record");
235
+ }
195
236
  },
196
237
  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;
238
+ const result = await fetch(`/${model}/$count`, {
239
+ method: "DELETE",
240
+ query: {
241
+ ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
242
+ $top: 1,
243
+ $select: [`"id"`],
244
+ },
245
+ output: z.coerce.number(),
246
+ });
247
+ if (result.error) {
248
+ throw new Error("Failed to delete record");
249
+ }
250
+ return result.data ?? 0;
201
251
  },
202
252
  update: async ({ model, where, update }) => {
203
- const rows = await db.table(model).query({
204
- filter: parseWhere(where),
205
- top: 1,
206
- select: [`"id"`],
253
+ const result = await fetch(`/${model}`, {
254
+ method: "PATCH",
255
+ query: {
256
+ ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
257
+ $top: 1,
258
+ $select: [`"id"`],
259
+ },
260
+ body: update,
261
+ output: z.object({ value: z.array(z.any()) }),
207
262
  });
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;
263
+ return result.data?.value?.[0] ?? null;
212
264
  },
213
265
  updateMany: async ({ model, where, update }) => {
214
266
  const filter = parseWhere(where);
215
- const rows = await db.table(model).updateMany(filter, update as any);
216
- return rows.length;
267
+ const result = await fetch(`/${model}`, {
268
+ method: "PATCH",
269
+ query: {
270
+ ...(where.length > 0 ? { $filter: filter } : {}),
271
+ },
272
+ body: update,
273
+ });
274
+ return result.data as any;
217
275
  },
218
276
  };
219
277
  },
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,89 @@ 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
+ return err(error);
44
111
  }
45
112
  }