@proofkit/better-auth 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,36 +1,14 @@
1
- import { createSchema, createFetch } from "@better-fetch/fetch";
2
- import { logger } from "@better-fetch/logger";
3
- import { logger as logger$1 } from "better-auth";
1
+ import { logger } from "better-auth";
4
2
  import { ok, err } from "neverthrow";
5
- import { z } from "zod/v4";
6
- const schema = createSchema({
7
- /**
8
- * Create a new table
9
- */
10
- "@post/FileMaker_Tables": {
11
- input: z.object({ tableName: z.string(), fields: z.array(z.any()) })
12
- },
13
- /**
14
- * Add fields to a table
15
- */
16
- "@patch/FileMaker_Tables/:tableName": {
17
- params: z.object({ tableName: z.string() }),
18
- input: z.object({ fields: z.array(z.any()) })
19
- },
20
- /**
21
- * Delete a table
22
- */
23
- "@delete/FileMaker_Tables/:tableName": {
24
- params: z.object({ tableName: z.string() })
25
- },
26
- /**
27
- * Delete a field from a table
28
- */
29
- "@delete/FileMaker_Tables/:tableName/:fieldName": {
30
- params: z.object({ tableName: z.string(), fieldName: z.string() })
3
+ function validateUrl(input) {
4
+ try {
5
+ const url = new URL(input);
6
+ return ok(url);
7
+ } catch (error) {
8
+ return err(error);
31
9
  }
32
- });
33
- function createFmOdataFetch(args) {
10
+ }
11
+ function createRawFetch(args) {
34
12
  const result = validateUrl(args.serverUrl);
35
13
  if (result.isErr()) {
36
14
  throw new Error("Invalid server URL");
@@ -40,44 +18,182 @@ function createFmOdataFetch(args) {
40
18
  baseURL += `/otto`;
41
19
  }
42
20
  baseURL += `/fmi/odata/v4/${args.database}`;
43
- return createFetch({
44
- baseURL,
45
- auth: "apiKey" in args.auth ? { type: "Bearer", token: args.auth.apiKey } : {
46
- type: "Basic",
47
- username: args.auth.username,
48
- password: args.auth.password
49
- },
50
- onError: (error) => {
51
- console.error("url", error.request.url.toString());
52
- console.log(error.error);
53
- console.log("error.request.body", JSON.stringify(error.request.body));
54
- },
55
- schema,
56
- plugins: [
57
- logger({
58
- verbose: args.logging === "verbose",
59
- enabled: args.logging === "verbose" || !!args.logging,
60
- console: {
61
- fail: (...args2) => logger$1.error("better-fetch", ...args2),
62
- success: (...args2) => logger$1.info("better-fetch", ...args2),
63
- log: (...args2) => logger$1.info("better-fetch", ...args2),
64
- error: (...args2) => logger$1.error("better-fetch", ...args2),
65
- warn: (...args2) => logger$1.warn("better-fetch", ...args2)
66
- }
67
- })
68
- ]
69
- });
70
- }
71
- function validateUrl(input) {
72
- try {
73
- const url = new URL(input);
74
- return ok(url);
75
- } catch (error) {
76
- return err(error);
21
+ const authHeaders = {};
22
+ if ("apiKey" in args.auth) {
23
+ authHeaders.Authorization = `Bearer ${args.auth.apiKey}`;
24
+ } else {
25
+ const credentials = btoa(`${args.auth.username}:${args.auth.password}`);
26
+ authHeaders.Authorization = `Basic ${credentials}`;
77
27
  }
28
+ const wrappedFetch = async (input, options) => {
29
+ try {
30
+ let url;
31
+ if (typeof input === "string") {
32
+ url = input.startsWith("http") ? input : `${baseURL}${input.startsWith("/") ? input : `/${input}`}`;
33
+ } else if (input instanceof URL) {
34
+ url = input.toString();
35
+ } else if (input instanceof Request) {
36
+ url = input.url;
37
+ } else {
38
+ url = String(input);
39
+ }
40
+ let processedBody = options == null ? void 0 : options.body;
41
+ if (processedBody && typeof processedBody === "object" && !(processedBody instanceof FormData) && !(processedBody instanceof URLSearchParams) && !(processedBody instanceof ReadableStream)) {
42
+ processedBody = JSON.stringify(processedBody);
43
+ }
44
+ const headers = {
45
+ "Content-Type": "application/json",
46
+ ...authHeaders,
47
+ ...(options == null ? void 0 : options.headers) || {}
48
+ };
49
+ const requestInit = {
50
+ ...options,
51
+ headers,
52
+ body: processedBody
53
+ };
54
+ if (args.logging === "verbose" || args.logging === true) {
55
+ logger.info(
56
+ "raw-fetch",
57
+ `${requestInit.method || "GET"} ${url}`
58
+ );
59
+ if (requestInit.body) {
60
+ logger.info("raw-fetch", "Request body:", requestInit.body);
61
+ }
62
+ }
63
+ const response = await fetch(url, requestInit);
64
+ if (args.logging === "verbose" || args.logging === true) {
65
+ logger.info(
66
+ "raw-fetch",
67
+ `Response status: ${response.status} ${response.statusText}`
68
+ );
69
+ logger.info(
70
+ "raw-fetch",
71
+ `Response headers:`,
72
+ Object.fromEntries(response.headers.entries())
73
+ );
74
+ }
75
+ if (!response.ok) {
76
+ const errorText = await response.text().catch(() => "Unknown error");
77
+ if (args.logging === "verbose" || args.logging === true) {
78
+ logger.error(
79
+ "raw-fetch",
80
+ `HTTP Error ${response.status}: ${errorText}`
81
+ );
82
+ }
83
+ return {
84
+ error: `HTTP ${response.status}: ${errorText}`,
85
+ response
86
+ };
87
+ }
88
+ let responseData;
89
+ const contentType = response.headers.get("content-type");
90
+ if (args.logging === "verbose" || args.logging === true) {
91
+ logger.info(
92
+ "raw-fetch",
93
+ `Response content-type: ${contentType || "none"}`
94
+ );
95
+ }
96
+ if (contentType == null ? void 0 : contentType.includes("application/json")) {
97
+ try {
98
+ const responseText = await response.text();
99
+ if (args.logging === "verbose" || args.logging === true) {
100
+ logger.info(
101
+ "raw-fetch",
102
+ `Raw response text: "${responseText}"`
103
+ );
104
+ logger.info(
105
+ "raw-fetch",
106
+ `Response text length: ${responseText.length}`
107
+ );
108
+ }
109
+ if (responseText.trim() === "") {
110
+ if (args.logging === "verbose" || args.logging === true) {
111
+ logger.info(
112
+ "raw-fetch",
113
+ "Empty JSON response, returning null"
114
+ );
115
+ }
116
+ responseData = null;
117
+ } else {
118
+ responseData = JSON.parse(responseText);
119
+ if (args.logging === "verbose" || args.logging === true) {
120
+ logger.info(
121
+ "raw-fetch",
122
+ "Successfully parsed JSON response"
123
+ );
124
+ }
125
+ }
126
+ } catch (parseError) {
127
+ if (args.logging === "verbose" || args.logging === true) {
128
+ logger.error(
129
+ "raw-fetch",
130
+ "JSON parse error:",
131
+ parseError
132
+ );
133
+ }
134
+ return {
135
+ error: `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : "Unknown parse error"}`,
136
+ response
137
+ };
138
+ }
139
+ } else if (contentType == null ? void 0 : contentType.includes("text/")) {
140
+ responseData = await response.text();
141
+ if (args.logging === "verbose" || args.logging === true) {
142
+ logger.info(
143
+ "raw-fetch",
144
+ `Text response: "${responseData}"`
145
+ );
146
+ }
147
+ } else {
148
+ try {
149
+ responseData = await response.text();
150
+ if (args.logging === "verbose" || args.logging === true) {
151
+ logger.info(
152
+ "raw-fetch",
153
+ `Unknown content-type response as text: "${responseData}"`
154
+ );
155
+ }
156
+ } catch {
157
+ responseData = null;
158
+ if (args.logging === "verbose" || args.logging === true) {
159
+ logger.info(
160
+ "raw-fetch",
161
+ "Could not parse response as text, returning null"
162
+ );
163
+ }
164
+ }
165
+ }
166
+ if (options == null ? void 0 : options.output) {
167
+ const validation = options.output.safeParse(responseData);
168
+ if (validation.success) {
169
+ return {
170
+ data: validation.data,
171
+ response
172
+ };
173
+ } else {
174
+ return {
175
+ error: `Validation failed: ${validation.error.message}`,
176
+ response
177
+ };
178
+ }
179
+ }
180
+ return {
181
+ data: responseData,
182
+ response
183
+ };
184
+ } catch (error) {
185
+ return {
186
+ error: error instanceof Error ? error.message : "Unknown error occurred"
187
+ };
188
+ }
189
+ };
190
+ return {
191
+ baseURL,
192
+ fetch: wrappedFetch
193
+ };
78
194
  }
79
195
  export {
80
- createFmOdataFetch,
196
+ createRawFetch,
81
197
  validateUrl
82
198
  };
83
199
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
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 { logger as betterAuthLogger } from \"better-auth\";\nimport { err, ok, Result } from \"neverthrow\";\nimport { z } from \"zod/v4\";\n\ntype BasicAuthCredentials = {\n username: string;\n password: string;\n};\ntype OttoAPIKeyAuth = {\n apiKey: string;\n};\ntype ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;\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) => betterAuthLogger.error(\"better-fetch\", ...args),\n success: (...args) => betterAuthLogger.info(\"better-fetch\", ...args),\n log: (...args) => betterAuthLogger.info(\"better-fetch\", ...args),\n error: (...args) => betterAuthLogger.error(\"better-fetch\", ...args),\n warn: (...args) => betterAuthLogger.warn(\"better-fetch\", ...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","betterAuthLogger"],"mappings":";;;;;AAsBA,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,UAASC,SAAiB,MAAM,gBAAgB,GAAGD,KAAI;AAAA,UACjE,SAAS,IAAIA,UAASC,SAAiB,KAAK,gBAAgB,GAAGD,KAAI;AAAA,UACnE,KAAK,IAAIA,UAASC,SAAiB,KAAK,gBAAgB,GAAGD,KAAI;AAAA,UAC/D,OAAO,IAAIA,UAASC,SAAiB,MAAM,gBAAgB,GAAGD,KAAI;AAAA,UAClE,MAAM,IAAIA,UAASC,SAAiB,KAAK,gBAAgB,GAAGD,KAAI;AAAA,QAAA;AAAA,MAEnE,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;"}
1
+ {"version":3,"file":"index.js","sources":["../../../src/odata/index.ts"],"sourcesContent":["import { logger as betterAuthLogger } from \"better-auth\";\nimport { err, ok, Result } from \"neverthrow\";\nimport { z } from \"zod/v4\";\n\ntype BasicAuthCredentials = {\n username: string;\n password: string;\n};\ntype OttoAPIKeyAuth = {\n apiKey: string;\n};\ntype ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;\n\nexport type FmOdataConfig = {\n serverUrl: string;\n auth: ODataAuth;\n database: string;\n logging?: true | \"verbose\" | \"none\";\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\nexport function createRawFetch(args: FmOdataConfig) {\n const result = validateUrl(args.serverUrl);\n\n if (result.isErr()) {\n throw new Error(\"Invalid server URL\");\n }\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 // Create authentication headers\n const authHeaders: Record<string, string> = {};\n if (\"apiKey\" in args.auth) {\n authHeaders.Authorization = `Bearer ${args.auth.apiKey}`;\n } else {\n const credentials = btoa(`${args.auth.username}:${args.auth.password}`);\n authHeaders.Authorization = `Basic ${credentials}`;\n }\n\n // Enhanced fetch function with body handling, validation, and structured responses\n const wrappedFetch = async <TOutput = any>(\n input: string | URL | Request,\n options?: Omit<RequestInit, \"body\"> & {\n body?: any; // Allow any type for body\n output?: z.ZodSchema<TOutput>; // Optional schema for validation\n },\n ): Promise<{ data?: TOutput; error?: string; response?: Response }> => {\n try {\n let url: string;\n\n // Handle different input types\n if (typeof input === \"string\") {\n // If it's already a full URL, use as-is, otherwise prepend baseURL\n url = input.startsWith(\"http\")\n ? input\n : `${baseURL}${input.startsWith(\"/\") ? input : `/${input}`}`;\n } else if (input instanceof URL) {\n url = input.toString();\n } else if (input instanceof Request) {\n url = input.url;\n } else {\n url = String(input);\n }\n\n // Handle body serialization\n let processedBody = options?.body;\n if (\n processedBody &&\n typeof processedBody === \"object\" &&\n !(processedBody instanceof FormData) &&\n !(processedBody instanceof URLSearchParams) &&\n !(processedBody instanceof ReadableStream)\n ) {\n processedBody = JSON.stringify(processedBody);\n }\n\n // Merge headers\n const headers = {\n \"Content-Type\": \"application/json\",\n ...authHeaders,\n ...(options?.headers || {}),\n };\n\n const requestInit: RequestInit = {\n ...options,\n headers,\n body: processedBody,\n };\n\n // Optional logging\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.info(\n \"raw-fetch\",\n `${requestInit.method || \"GET\"} ${url}`,\n );\n if (requestInit.body) {\n betterAuthLogger.info(\"raw-fetch\", \"Request body:\", requestInit.body);\n }\n }\n\n const response = await fetch(url, requestInit);\n\n // Optional logging for response details\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.info(\n \"raw-fetch\",\n `Response status: ${response.status} ${response.statusText}`,\n );\n betterAuthLogger.info(\n \"raw-fetch\",\n `Response headers:`,\n Object.fromEntries(response.headers.entries()),\n );\n }\n\n // Check if response is ok\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"Unknown error\");\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.error(\n \"raw-fetch\",\n `HTTP Error ${response.status}: ${errorText}`,\n );\n }\n return {\n error: `HTTP ${response.status}: ${errorText}`,\n response,\n };\n }\n\n // Parse response based on content type\n let responseData: any;\n const contentType = response.headers.get(\"content-type\");\n\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.info(\n \"raw-fetch\",\n `Response content-type: ${contentType || \"none\"}`,\n );\n }\n\n if (contentType?.includes(\"application/json\")) {\n try {\n const responseText = await response.text();\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.info(\n \"raw-fetch\",\n `Raw response text: \"${responseText}\"`,\n );\n betterAuthLogger.info(\n \"raw-fetch\",\n `Response text length: ${responseText.length}`,\n );\n }\n\n // Handle empty responses\n if (responseText.trim() === \"\") {\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.info(\n \"raw-fetch\",\n \"Empty JSON response, returning null\",\n );\n }\n responseData = null;\n } else {\n responseData = JSON.parse(responseText);\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.info(\n \"raw-fetch\",\n \"Successfully parsed JSON response\",\n );\n }\n }\n } catch (parseError) {\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.error(\n \"raw-fetch\",\n \"JSON parse error:\",\n parseError,\n );\n }\n return {\n error: `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : \"Unknown parse error\"}`,\n response,\n };\n }\n } else if (contentType?.includes(\"text/\")) {\n // Handle text responses (text/plain, text/html, etc.)\n responseData = await response.text();\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.info(\n \"raw-fetch\",\n `Text response: \"${responseData}\"`,\n );\n }\n } else {\n // For other content types, try to get text but don't fail if it's binary\n try {\n responseData = await response.text();\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.info(\n \"raw-fetch\",\n `Unknown content-type response as text: \"${responseData}\"`,\n );\n }\n } catch {\n // If text parsing fails (e.g., binary data), return null\n responseData = null;\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.info(\n \"raw-fetch\",\n \"Could not parse response as text, returning null\",\n );\n }\n }\n }\n\n // Validate output if schema provided\n if (options?.output) {\n const validation = options.output.safeParse(responseData);\n if (validation.success) {\n return {\n data: validation.data,\n response,\n };\n } else {\n return {\n error: `Validation failed: ${validation.error.message}`,\n response,\n };\n }\n }\n\n // Return unvalidated data\n return {\n data: responseData as TOutput,\n response,\n };\n } catch (error) {\n return {\n error:\n error instanceof Error ? error.message : \"Unknown error occurred\",\n };\n }\n };\n\n return {\n baseURL,\n fetch: wrappedFetch,\n };\n}\n"],"names":["betterAuthLogger"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proofkit/better-auth",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "FileMaker adapter for Better Auth",
5
5
  "type": "module",
6
6
  "main": "dist/esm/index.js",
@@ -38,8 +38,6 @@
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",
43
41
  "@commander-js/extra-typings": "^14.0.0",
44
42
  "@tanstack/vite-config": "^0.2.0",
45
43
  "better-auth": "^1.2.10",
@@ -49,6 +47,7 @@
49
47
  "dotenv": "^16.5.0",
50
48
  "fs-extra": "^11.3.0",
51
49
  "neverthrow": "^8.2.0",
50
+ "odata-query": "^8.0.4",
52
51
  "prompts": "^2.4.2",
53
52
  "vite": "^6.3.4",
54
53
  "zod": "3.25.64"
@@ -56,10 +55,11 @@
56
55
  "devDependencies": {
57
56
  "@types/fs-extra": "^11.0.4",
58
57
  "@types/prompts": "^2.4.9",
58
+ "@vitest/ui": "^3.2.4",
59
59
  "fm-odata-client": "^3.0.1",
60
60
  "publint": "^0.3.12",
61
- "typescript": "^5.8.3",
62
- "vitest": "^3.2.3"
61
+ "typescript": "^5.9.2",
62
+ "vitest": "^3.2.4"
63
63
  },
64
64
  "scripts": {
65
65
  "dev": "pnpm build:watch",
package/src/adapter.ts CHANGED
@@ -3,9 +3,10 @@ import {
3
3
  createAdapter,
4
4
  type AdapterDebugLogs,
5
5
  } from "better-auth/adapters";
6
- import { createFmOdataFetch, type FmOdataConfig } from "./odata";
6
+ import { createRawFetch, type FmOdataConfig } from "./odata";
7
7
  import { prettifyError, z } from "zod/v4";
8
8
  import { logger } from "better-auth";
9
+ import buildQuery from "odata-query";
9
10
 
10
11
  const configSchema = z.object({
11
12
  debugLogs: z.unknown().optional(),
@@ -153,7 +154,7 @@ export const FileMakerAdapter = (
153
154
  }
154
155
  const config = parsed.data;
155
156
 
156
- const fetch = createFmOdataFetch({
157
+ const { fetch, baseURL } = createRawFetch({
157
158
  ...config.odata,
158
159
  logging: config.debugLogs ? "verbose" : "none",
159
160
  });
@@ -192,114 +193,177 @@ export const FileMakerAdapter = (
192
193
  count: async ({ model, where }) => {
193
194
  const filter = parseWhere(where);
194
195
  logger.debug("$filter", filter);
195
- const result = await fetch(`/${model}/$count`, {
196
+
197
+ const query = buildQuery({
198
+ filter: filter.length > 0 ? filter : undefined,
199
+ });
200
+
201
+ const result = await fetch(`/${model}/$count${query}`, {
196
202
  method: "GET",
197
- query: {
198
- $filter: filter,
199
- },
200
203
  output: z.object({ value: z.number() }),
201
204
  });
202
205
  if (!result.data) {
203
206
  throw new Error("Failed to count records");
204
207
  }
205
- return result.data?.value ?? 0;
208
+ return (result.data?.value as any) ?? 0;
206
209
  },
207
210
  findOne: async ({ model, where }) => {
208
211
  const filter = parseWhere(where);
209
212
  logger.debug("$filter", filter);
210
- const result = await fetch(`/${model}`, {
213
+
214
+ const query = buildQuery({
215
+ top: 1,
216
+ filter: filter.length > 0 ? filter : undefined,
217
+ });
218
+
219
+ const result = await fetch(`/${model}${query}`, {
211
220
  method: "GET",
212
- query: {
213
- ...(filter.length > 0 ? { $filter: filter } : {}),
214
- $top: 1,
215
- },
216
221
  output: z.object({ value: z.array(z.any()) }),
217
222
  });
218
223
  if (result.error) {
219
224
  throw new Error("Failed to find record");
220
225
  }
221
- return result.data?.value?.[0] ?? null;
226
+ return (result.data?.value?.[0] as any) ?? null;
222
227
  },
223
228
  findMany: async ({ model, where, limit, offset, sortBy }) => {
224
229
  const filter = parseWhere(where);
225
- logger.debug("$filter", filter);
230
+ logger.debug("FIND MANY", { where, filter });
231
+
232
+ const query = buildQuery({
233
+ top: limit,
234
+ skip: offset,
235
+ orderBy: sortBy
236
+ ? `${sortBy.field} ${sortBy.direction ?? "asc"}`
237
+ : undefined,
238
+ filter: filter.length > 0 ? filter : undefined,
239
+ });
240
+ logger.debug("QUERY", query);
226
241
 
227
- const rows = await fetch(`/${model}`, {
242
+ const result = await fetch(`/${model}${query}`, {
228
243
  method: "GET",
229
- query: {
230
- ...(filter.length > 0 ? { $filter: filter } : {}),
231
- $top: limit,
232
- $skip: offset,
233
- ...(sortBy
234
- ? { $orderby: `"${sortBy.field}" ${sortBy.direction ?? "asc"}` }
235
- : {}),
236
- },
237
244
  output: z.object({ value: z.array(z.any()) }),
238
245
  });
239
- if (rows.error) {
246
+ logger.debug("RESULT", result);
247
+
248
+ if (result.error) {
240
249
  throw new Error("Failed to find records");
241
250
  }
242
- return rows.data?.value ?? [];
251
+
252
+ return (result.data?.value as any) ?? [];
243
253
  },
244
254
  delete: async ({ model, where }) => {
245
255
  const filter = parseWhere(where);
256
+ console.log("DELETE", { model, where, filter });
246
257
  logger.debug("$filter", filter);
247
- console.log("delete", model, where, filter);
248
- const result = await fetch(`/${model}`, {
258
+
259
+ // Find a single id matching the filter
260
+ const query = buildQuery({
261
+ top: 1,
262
+ select: [`"id"`],
263
+ filter: filter.length > 0 ? filter : undefined,
264
+ });
265
+
266
+ const toDelete = await fetch(`/${model}${query}`, {
267
+ method: "GET",
268
+ output: z.object({ value: z.array(z.object({ id: z.string() })) }),
269
+ });
270
+
271
+ const id = toDelete.data?.value?.[0]?.id;
272
+ if (!id) {
273
+ // Nothing to delete
274
+ return;
275
+ }
276
+
277
+ const result = await fetch(`/${model}('${id}')`, {
249
278
  method: "DELETE",
250
- query: {
251
- ...(where.length > 0 ? { $filter: filter } : {}),
252
- $top: 1,
253
- },
254
279
  });
255
280
  if (result.error) {
281
+ console.log("DELETE ERROR", result.error);
256
282
  throw new Error("Failed to delete record");
257
283
  }
258
284
  },
259
285
  deleteMany: async ({ model, where }) => {
260
286
  const filter = parseWhere(where);
261
- logger.debug(
262
- where
263
- .map((o) => `typeof ${o.value} is ${typeof o.value}`)
264
- .join("\n"),
265
- );
266
- logger.debug("$filter", filter);
287
+ console.log("DELETE MANY", { model, where, filter });
267
288
 
268
- const result = await fetch(`/${model}/$count`, {
269
- method: "DELETE",
270
- query: {
271
- ...(where.length > 0 ? { $filter: filter } : {}),
272
- },
273
- output: z.coerce.number(),
289
+ // Find all ids matching the filter
290
+ const query = buildQuery({
291
+ select: [`"id"`],
292
+ filter: filter.length > 0 ? filter : undefined,
274
293
  });
275
- if (result.error) {
276
- throw new Error("Failed to delete record");
294
+
295
+ const rows = await fetch(`/${model}${query}`, {
296
+ method: "GET",
297
+ output: z.object({ value: z.array(z.object({ id: z.string() })) }),
298
+ });
299
+
300
+ const ids = rows.data?.value?.map((r: any) => r.id) ?? [];
301
+ let deleted = 0;
302
+ for (const id of ids) {
303
+ const res = await fetch(`/${model}('${id}')`, {
304
+ method: "DELETE",
305
+ });
306
+ if (!res.error) deleted++;
277
307
  }
278
- return result.data ?? 0;
308
+ return deleted;
279
309
  },
280
310
  update: async ({ model, where, update }) => {
281
- const result = await fetch(`/${model}`, {
311
+ const filter = parseWhere(where);
312
+ logger.debug("UPDATE", { model, where, update });
313
+ logger.debug("$filter", filter);
314
+ // Find one id to update
315
+ const query = buildQuery({
316
+ select: [`"id"`],
317
+ filter: filter.length > 0 ? filter : undefined,
318
+ });
319
+
320
+ const existing = await fetch(`/${model}${query}`, {
321
+ method: "GET",
322
+ output: z.object({ value: z.array(z.object({ id: z.string() })) }),
323
+ });
324
+ logger.debug("EXISTING", existing.data);
325
+
326
+ const id = existing.data?.value?.[0]?.id;
327
+ if (!id) return null;
328
+
329
+ const patchRes = await fetch(`/${model}('${id}')`, {
282
330
  method: "PATCH",
283
- query: {
284
- ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
285
- $top: 1,
286
- $select: [`"id"`],
287
- },
288
331
  body: update,
289
- output: z.object({ value: z.array(z.any()) }),
290
332
  });
291
- return result.data?.value?.[0] ?? null;
333
+ logger.debug("PATCH RES", patchRes.data);
334
+ if (patchRes.error) return null;
335
+
336
+ // Read back the updated record
337
+ const readBack = await fetch(`/${model}('${id}')`, {
338
+ method: "GET",
339
+ output: z.record(z.string(), z.unknown()),
340
+ });
341
+ logger.debug("READ BACK", readBack.data);
342
+ return (readBack.data as any) ?? null;
292
343
  },
293
344
  updateMany: async ({ model, where, update }) => {
294
345
  const filter = parseWhere(where);
295
- const result = await fetch(`/${model}`, {
296
- method: "PATCH",
297
- query: {
298
- ...(where.length > 0 ? { $filter: filter } : {}),
299
- },
300
- body: update,
346
+ // Find all ids matching the filter
347
+ const query = buildQuery({
348
+ select: [`"id"`],
349
+ filter: filter.length > 0 ? filter : undefined,
301
350
  });
302
- return result.data as any;
351
+
352
+ const rows = await fetch(`/${model}${query}`, {
353
+ method: "GET",
354
+ output: z.object({ value: z.array(z.object({ id: z.string() })) }),
355
+ });
356
+
357
+ const ids = rows.data?.value?.map((r: any) => r.id) ?? [];
358
+ let updated = 0;
359
+ for (const id of ids) {
360
+ const res = await fetch(`/${model}('${id}')`, {
361
+ method: "PATCH",
362
+ body: update,
363
+ });
364
+ if (!res.error) updated++;
365
+ }
366
+ return updated as any;
303
367
  },
304
368
  };
305
369
  },
package/src/cli/index.ts CHANGED
@@ -13,7 +13,8 @@ import { logger } from "better-auth";
13
13
  import prompts from "prompts";
14
14
  import chalk from "chalk";
15
15
  import { AdapterOptions } from "../adapter";
16
- import { createFmOdataFetch } from "../odata";
16
+ import { createRawFetch } from "../odata";
17
+ import "dotenv/config";
17
18
 
18
19
  async function main() {
19
20
  const program = new Command();
@@ -63,7 +64,7 @@ async function main() {
63
64
  const betterAuthSchema = getAuthTables(config);
64
65
 
65
66
  const adapterConfig = (adapter.options as AdapterOptions).config;
66
- const fetch = createFmOdataFetch({
67
+ const { fetch } = createRawFetch({
67
68
  ...adapterConfig.odata,
68
69
  auth:
69
70
  // If the username and password are provided in the CLI, use them to authenticate instead of what's in the config file.
@@ -73,6 +74,7 @@ async function main() {
73
74
  password: options.password,
74
75
  }
75
76
  : adapterConfig.odata.auth,
77
+ logging: "verbose", // Enable logging for CLI operations
76
78
  });
77
79
 
78
80
  const migrationPlan = await planMigration(