@proofkit/better-auth 0.1.0 → 0.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/adapter.js +97 -41
- package/dist/esm/adapter.js.map +1 -1
- package/dist/esm/cli/index.js +9 -5
- package/dist/esm/cli/index.js.map +1 -1
- package/dist/esm/migrate.d.ts +5 -3
- package/dist/esm/migrate.js +25 -7
- package/dist/esm/migrate.js.map +1 -1
- package/dist/esm/odata/index.d.ts +90 -7
- package/dist/esm/odata/index.js +77 -21
- package/dist/esm/odata/index.js.map +1 -1
- package/package.json +6 -5
- package/src/adapter.ts +97 -42
- package/src/cli/index.ts +9 -5
- package/src/migrate.ts +31 -9
- package/src/odata/index.ts +85 -17
package/dist/esm/adapter.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createAdapter } from "better-auth/adapters";
|
|
2
|
-
import {
|
|
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
|
-
|
|
8
|
+
serverUrl: z.string(),
|
|
9
9
|
auth: z.object({ username: z.string(), password: z.string() }),
|
|
10
10
|
database: z.string().endsWith(".fmp12")
|
|
11
11
|
})
|
|
@@ -14,7 +14,7 @@ const defaultConfig = {
|
|
|
14
14
|
debugLogs: false,
|
|
15
15
|
usePlural: false,
|
|
16
16
|
odata: {
|
|
17
|
-
|
|
17
|
+
serverUrl: "",
|
|
18
18
|
auth: { username: "", password: "" },
|
|
19
19
|
database: ""
|
|
20
20
|
}
|
|
@@ -32,7 +32,7 @@ function parseWhere(where) {
|
|
|
32
32
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
33
33
|
if (value instanceof Date) return `'${value.toISOString()}'`;
|
|
34
34
|
if (Array.isArray(value)) return `(${value.map(formatValue).join(",")})`;
|
|
35
|
-
return value.toString();
|
|
35
|
+
return (value == null ? void 0 : value.toString()) ?? "";
|
|
36
36
|
}
|
|
37
37
|
const opMap = {
|
|
38
38
|
eq: "eq",
|
|
@@ -88,8 +88,10 @@ const FileMakerAdapter = (_config = defaultConfig) => {
|
|
|
88
88
|
throw new Error(`Invalid configuration: ${prettifyError(parsed.error)}`);
|
|
89
89
|
}
|
|
90
90
|
const config = parsed.data;
|
|
91
|
-
const
|
|
92
|
-
|
|
91
|
+
const fetch = createFmOdataFetch({
|
|
92
|
+
...config.odata
|
|
93
|
+
// logging: config.debugLogs ? true : "none",
|
|
94
|
+
});
|
|
93
95
|
return createAdapter({
|
|
94
96
|
config: {
|
|
95
97
|
adapterId: "filemaker",
|
|
@@ -111,61 +113,115 @@ const FileMakerAdapter = (_config = defaultConfig) => {
|
|
|
111
113
|
return {
|
|
112
114
|
options: { config },
|
|
113
115
|
create: async ({ data, model, select }) => {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
+
const result = await fetch(`/${model}`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
body: data,
|
|
119
|
+
output: z.looseObject({ id: z.string() })
|
|
120
|
+
});
|
|
121
|
+
if (result.error) {
|
|
122
|
+
throw new Error("Failed to create record");
|
|
123
|
+
}
|
|
124
|
+
return result.data;
|
|
116
125
|
},
|
|
117
126
|
count: async ({ model, where }) => {
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
var _a;
|
|
128
|
+
const result = await fetch(`/${model}/$count`, {
|
|
129
|
+
method: "GET",
|
|
130
|
+
query: {
|
|
131
|
+
$filter: parseWhere(where)
|
|
132
|
+
},
|
|
133
|
+
output: z.object({ value: z.number() })
|
|
134
|
+
});
|
|
135
|
+
if (!result.data) {
|
|
136
|
+
throw new Error("Failed to count records");
|
|
137
|
+
}
|
|
138
|
+
return ((_a = result.data) == null ? void 0 : _a.value) ?? 0;
|
|
120
139
|
},
|
|
121
140
|
findOne: async ({ model, where }) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
141
|
+
var _a, _b;
|
|
142
|
+
const result = await fetch(`/${model}`, {
|
|
143
|
+
method: "GET",
|
|
144
|
+
query: {
|
|
145
|
+
...where.length > 0 ? { $filter: parseWhere(where) } : {},
|
|
146
|
+
$top: 1
|
|
147
|
+
},
|
|
148
|
+
output: z.object({ value: z.array(z.any()) })
|
|
125
149
|
});
|
|
126
|
-
|
|
150
|
+
if (result.error) {
|
|
151
|
+
throw new Error("Failed to find record");
|
|
152
|
+
}
|
|
153
|
+
return ((_b = (_a = result.data) == null ? void 0 : _a.value) == null ? void 0 : _b[0]) ?? null;
|
|
127
154
|
},
|
|
128
155
|
findMany: async ({ model, where, limit, offset, sortBy }) => {
|
|
156
|
+
var _a;
|
|
129
157
|
const filter = parseWhere(where);
|
|
130
|
-
const rows = await
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
158
|
+
const rows = await fetch(`/${model}`, {
|
|
159
|
+
method: "GET",
|
|
160
|
+
query: {
|
|
161
|
+
...filter.length > 0 ? { $filter: filter } : {},
|
|
162
|
+
$top: limit,
|
|
163
|
+
$skip: offset,
|
|
164
|
+
...sortBy ? { $orderby: `"${sortBy.field}" ${sortBy.direction ?? "asc"}` } : {}
|
|
165
|
+
},
|
|
166
|
+
output: z.object({ value: z.array(z.any()) })
|
|
135
167
|
});
|
|
136
|
-
|
|
168
|
+
if (rows.error) {
|
|
169
|
+
throw new Error("Failed to find records");
|
|
170
|
+
}
|
|
171
|
+
return ((_a = rows.data) == null ? void 0 : _a.value) ?? [];
|
|
137
172
|
},
|
|
138
173
|
delete: async ({ model, where }) => {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
174
|
+
const result = await fetch(`/${model}`, {
|
|
175
|
+
method: "DELETE",
|
|
176
|
+
query: {
|
|
177
|
+
...where.length > 0 ? { $filter: parseWhere(where) } : {},
|
|
178
|
+
$top: 1,
|
|
179
|
+
$select: [`"id"`]
|
|
180
|
+
}
|
|
143
181
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
182
|
+
if (result.error) {
|
|
183
|
+
throw new Error("Failed to delete record");
|
|
184
|
+
}
|
|
147
185
|
},
|
|
148
186
|
deleteMany: async ({ model, where }) => {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
187
|
+
const result = await fetch(`/${model}/$count`, {
|
|
188
|
+
method: "DELETE",
|
|
189
|
+
query: {
|
|
190
|
+
...where.length > 0 ? { $filter: parseWhere(where) } : {},
|
|
191
|
+
$top: 1,
|
|
192
|
+
$select: [`"id"`]
|
|
193
|
+
},
|
|
194
|
+
output: z.coerce.number()
|
|
195
|
+
});
|
|
196
|
+
if (result.error) {
|
|
197
|
+
throw new Error("Failed to delete record");
|
|
198
|
+
}
|
|
199
|
+
return result.data ?? 0;
|
|
153
200
|
},
|
|
154
201
|
update: async ({ model, where, update }) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
var _a, _b;
|
|
203
|
+
const result = await fetch(`/${model}`, {
|
|
204
|
+
method: "PATCH",
|
|
205
|
+
query: {
|
|
206
|
+
...where.length > 0 ? { $filter: parseWhere(where) } : {},
|
|
207
|
+
$top: 1,
|
|
208
|
+
$select: [`"id"`]
|
|
209
|
+
},
|
|
210
|
+
body: update,
|
|
211
|
+
output: z.object({ value: z.array(z.any()) })
|
|
159
212
|
});
|
|
160
|
-
|
|
161
|
-
if (!row) return null;
|
|
162
|
-
const result = await db.table(model).update(row["id"], update);
|
|
163
|
-
return result;
|
|
213
|
+
return ((_b = (_a = result.data) == null ? void 0 : _a.value) == null ? void 0 : _b[0]) ?? null;
|
|
164
214
|
},
|
|
165
215
|
updateMany: async ({ model, where, update }) => {
|
|
166
216
|
const filter = parseWhere(where);
|
|
167
|
-
const
|
|
168
|
-
|
|
217
|
+
const result = await fetch(`/${model}`, {
|
|
218
|
+
method: "PATCH",
|
|
219
|
+
query: {
|
|
220
|
+
...where.length > 0 ? { $filter: filter } : {}
|
|
221
|
+
},
|
|
222
|
+
body: update
|
|
223
|
+
});
|
|
224
|
+
return result.data;
|
|
169
225
|
}
|
|
170
226
|
};
|
|
171
227
|
}
|
package/dist/esm/adapter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.js","sources":["../../src/adapter.ts"],"sourcesContent":["import {\n CleanedWhere,\n createAdapter,\n type AdapterDebugLogs,\n} from \"better-auth/adapters\";\nimport { FmOdata, type FmOdataConfig } from \"./odata\";\nimport { prettifyError, z } from \"zod/v4\";\n\ninterface FileMakerAdapterConfig {\n /**\n * Helps you debug issues with the adapter.\n */\n debugLogs?: AdapterDebugLogs;\n /**\n * If the table names in the schema are plural.\n */\n usePlural?: boolean;\n\n /**\n * Connection details for the FileMaker server.\n */\n odata: FmOdataConfig;\n}\n\nexport type AdapterOptions = {\n config: FileMakerAdapterConfig;\n};\n\nconst configSchema = z.object({\n debugLogs: z.unknown().optional(),\n usePlural: z.boolean().optional(),\n odata: z.object({\n hostname: z.string(),\n auth: z.object({ username: z.string(), password: z.string() }),\n database: z.string().endsWith(\".fmp12\"),\n }),\n});\n\nconst defaultConfig: Required<FileMakerAdapterConfig> = {\n debugLogs: false,\n usePlural: false,\n odata: {\n hostname: \"\",\n auth: { username: \"\", password: \"\" },\n database: \"\",\n },\n};\n\n/**\n * Parse the where clause to an OData filter string.\n * @param where - The where clause to parse.\n * @returns The OData filter string.\n * @internal\n */\nexport function parseWhere(where?: CleanedWhere[]): string {\n if (!where || where.length === 0) return \"\";\n\n // Helper to quote field names with special chars or if field is 'id'\n function quoteField(field: string, value?: any) {\n // Never quote for null or date values (per test expectations)\n if (value === null || value instanceof Date) return field;\n // Always quote if field is 'id' or has space or underscore\n if (field === \"id\" || /[\\s_]/.test(field)) return `\"${field}\"`;\n return field;\n }\n\n // Helper to format values for OData\n function formatValue(value: any): string {\n if (value === null) return \"null\";\n if (typeof value === \"string\") return `'${value.replace(/'/g, \"''\")}'`;\n if (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n if (value instanceof Date) return `'${value.toISOString()}'`;\n if (Array.isArray(value)) return `(${value.map(formatValue).join(\",\")})`;\n return value.toString();\n }\n\n // Map our operators to OData\n const opMap: Record<string, string> = {\n eq: \"eq\",\n ne: \"ne\",\n lt: \"lt\",\n lte: \"le\",\n gt: \"gt\",\n gte: \"ge\",\n };\n\n // Build each clause\n const clauses: string[] = [];\n for (let i = 0; i < where.length; i++) {\n const cond = where[i];\n if (!cond) continue;\n const field = quoteField(cond.field, cond.value);\n let clause = \"\";\n switch (cond.operator) {\n case \"eq\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"gt\":\n case \"gte\":\n clause = `${field} ${opMap[cond.operator!]} ${formatValue(cond.value)}`;\n break;\n case \"in\":\n if (Array.isArray(cond.value)) {\n clause = cond.value\n .map((v) => `${field} eq ${formatValue(v)}`)\n .join(\" or \");\n clause = `(${clause})`;\n }\n break;\n case \"contains\":\n clause = `contains(${field}, ${formatValue(cond.value)})`;\n break;\n case \"starts_with\":\n clause = `startswith(${field}, ${formatValue(cond.value)})`;\n break;\n case \"ends_with\":\n clause = `endswith(${field}, ${formatValue(cond.value)})`;\n break;\n default:\n clause = `${field} eq ${formatValue(cond.value)}`;\n }\n clauses.push(clause);\n // Add connector if not last\n if (i < where.length - 1) {\n clauses.push((cond.connector || \"and\").toLowerCase());\n }\n }\n return clauses.join(\" \");\n}\n\nexport const FileMakerAdapter = (\n _config: FileMakerAdapterConfig = defaultConfig,\n) => {\n const parsed = configSchema.loose().safeParse(_config);\n\n if (!parsed.success) {\n throw new Error(`Invalid configuration: ${prettifyError(parsed.error)}`);\n }\n const config = parsed.data;\n\n const odata =\n config.odata instanceof FmOdata ? config.odata : new FmOdata(config.odata);\n const db = odata.database;\n\n return createAdapter({\n config: {\n adapterId: \"filemaker\",\n adapterName: \"FileMaker\",\n usePlural: config.usePlural ?? false, // Whether the table names in the schema are plural.\n debugLogs: config.debugLogs ?? false, // Whether to enable debug logs.\n supportsJSON: false, // Whether the database supports JSON. (Default: false)\n supportsDates: false, // Whether the database supports dates. (Default: true)\n supportsBooleans: false, // Whether the database supports booleans. (Default: true)\n supportsNumericIds: false, // Whether the database supports auto-incrementing numeric IDs. (Default: true)\n },\n adapter: ({ options }) => {\n return {\n options: { config },\n create: async ({ data, model, select }) => {\n const row = await db.table(model).create(data);\n return row as unknown as typeof data;\n },\n count: async ({ model, where }) => {\n const count = await db.table(model).count(parseWhere(where));\n return count;\n },\n findOne: async ({ model, where }) => {\n const row = await db.table(model).query({\n filter: parseWhere(where),\n top: 1,\n });\n return (row[0] as any) ?? null;\n },\n findMany: async ({ model, where, limit, offset, sortBy }) => {\n const filter = parseWhere(where);\n\n const rows = await db.table(model).query({\n filter,\n top: limit,\n skip: offset,\n orderBy: sortBy,\n });\n return rows.map((row) => row as any);\n },\n delete: async ({ model, where }) => {\n const rows = await db.table(model).query({\n filter: parseWhere(where),\n top: 1,\n select: [`\"id\"`],\n });\n const row = rows[0] as { id: string } | undefined;\n if (!row) return;\n await db.table(model).delete(row.id);\n },\n deleteMany: async ({ model, where }) => {\n const filter = parseWhere(where);\n const count = await db.table(model).count(filter);\n await db.table(model).deleteMany(filter);\n return count;\n },\n update: async ({ model, where, update }) => {\n const rows = await db.table(model).query({\n filter: parseWhere(where),\n top: 1,\n select: [`\"id\"`],\n });\n const row = rows[0] as { id: string } | undefined;\n if (!row) return null;\n const result = await db.table(model).update(row[\"id\"], update as any);\n return result as any;\n },\n updateMany: async ({ model, where, update }) => {\n const filter = parseWhere(where);\n const rows = await db.table(model).updateMany(filter, update as any);\n return rows.length;\n },\n };\n },\n });\n};\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"adapter.js","sources":["../../src/adapter.ts"],"sourcesContent":["import {\n CleanedWhere,\n createAdapter,\n type AdapterDebugLogs,\n} from \"better-auth/adapters\";\nimport { createFmOdataFetch, type FmOdataConfig } from \"./odata\";\nimport { prettifyError, z } from \"zod/v4\";\n\ninterface FileMakerAdapterConfig {\n /**\n * Helps you debug issues with the adapter.\n */\n debugLogs?: AdapterDebugLogs;\n /**\n * If the table names in the schema are plural.\n */\n usePlural?: boolean;\n\n /**\n * Connection details for the FileMaker server.\n */\n odata: FmOdataConfig;\n}\n\nexport type AdapterOptions = {\n config: FileMakerAdapterConfig;\n};\n\nconst configSchema = z.object({\n debugLogs: z.unknown().optional(),\n usePlural: z.boolean().optional(),\n odata: z.object({\n serverUrl: z.string(),\n auth: z.object({ username: z.string(), password: z.string() }),\n database: z.string().endsWith(\".fmp12\"),\n }),\n});\n\nconst defaultConfig: Required<FileMakerAdapterConfig> = {\n debugLogs: false,\n usePlural: false,\n odata: {\n serverUrl: \"\",\n auth: { username: \"\", password: \"\" },\n database: \"\",\n },\n};\n\n/**\n * Parse the where clause to an OData filter string.\n * @param where - The where clause to parse.\n * @returns The OData filter string.\n * @internal\n */\nexport function parseWhere(where?: CleanedWhere[]): string {\n if (!where || where.length === 0) return \"\";\n\n // Helper to quote field names with special chars or if field is 'id'\n function quoteField(field: string, value?: any) {\n // Never quote for null or date values (per test expectations)\n if (value === null || value instanceof Date) return field;\n // Always quote if field is 'id' or has space or underscore\n if (field === \"id\" || /[\\s_]/.test(field)) return `\"${field}\"`;\n return field;\n }\n\n // Helper to format values for OData\n function formatValue(value: any): string {\n if (value === null) return \"null\";\n if (typeof value === \"string\") return `'${value.replace(/'/g, \"''\")}'`;\n if (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n if (value instanceof Date) return `'${value.toISOString()}'`;\n if (Array.isArray(value)) return `(${value.map(formatValue).join(\",\")})`;\n return value?.toString() ?? \"\";\n }\n\n // Map our operators to OData\n const opMap: Record<string, string> = {\n eq: \"eq\",\n ne: \"ne\",\n lt: \"lt\",\n lte: \"le\",\n gt: \"gt\",\n gte: \"ge\",\n };\n\n // Build each clause\n const clauses: string[] = [];\n for (let i = 0; i < where.length; i++) {\n const cond = where[i];\n if (!cond) continue;\n const field = quoteField(cond.field, cond.value);\n let clause = \"\";\n switch (cond.operator) {\n case \"eq\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"gt\":\n case \"gte\":\n clause = `${field} ${opMap[cond.operator!]} ${formatValue(cond.value)}`;\n break;\n case \"in\":\n if (Array.isArray(cond.value)) {\n clause = cond.value\n .map((v) => `${field} eq ${formatValue(v)}`)\n .join(\" or \");\n clause = `(${clause})`;\n }\n break;\n case \"contains\":\n clause = `contains(${field}, ${formatValue(cond.value)})`;\n break;\n case \"starts_with\":\n clause = `startswith(${field}, ${formatValue(cond.value)})`;\n break;\n case \"ends_with\":\n clause = `endswith(${field}, ${formatValue(cond.value)})`;\n break;\n default:\n clause = `${field} eq ${formatValue(cond.value)}`;\n }\n clauses.push(clause);\n // Add connector if not last\n if (i < where.length - 1) {\n clauses.push((cond.connector || \"and\").toLowerCase());\n }\n }\n return clauses.join(\" \");\n}\n\nexport const FileMakerAdapter = (\n _config: FileMakerAdapterConfig = defaultConfig,\n) => {\n const parsed = configSchema.loose().safeParse(_config);\n\n if (!parsed.success) {\n throw new Error(`Invalid configuration: ${prettifyError(parsed.error)}`);\n }\n const config = parsed.data;\n\n const fetch = createFmOdataFetch({\n ...config.odata,\n // logging: config.debugLogs ? true : \"none\",\n });\n\n return createAdapter({\n config: {\n adapterId: \"filemaker\",\n adapterName: \"FileMaker\",\n usePlural: config.usePlural ?? false, // Whether the table names in the schema are plural.\n debugLogs: config.debugLogs ?? false, // Whether to enable debug logs.\n supportsJSON: false, // Whether the database supports JSON. (Default: false)\n supportsDates: false, // Whether the database supports dates. (Default: true)\n supportsBooleans: false, // Whether the database supports booleans. (Default: true)\n supportsNumericIds: false, // Whether the database supports auto-incrementing numeric IDs. (Default: true)\n },\n adapter: ({ options }) => {\n return {\n options: { config },\n create: async ({ data, model, select }) => {\n const result = await fetch(`/${model}`, {\n method: \"POST\",\n body: data,\n output: z.looseObject({ id: z.string() }),\n });\n\n if (result.error) {\n throw new Error(\"Failed to create record\");\n }\n\n return result.data as any;\n },\n count: async ({ model, where }) => {\n const result = await fetch(`/${model}/$count`, {\n method: \"GET\",\n query: {\n $filter: parseWhere(where),\n },\n output: z.object({ value: z.number() }),\n });\n if (!result.data) {\n throw new Error(\"Failed to count records\");\n }\n return result.data?.value ?? 0;\n },\n findOne: async ({ model, where }) => {\n const result = await fetch(`/${model}`, {\n method: \"GET\",\n query: {\n ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),\n $top: 1,\n },\n output: z.object({ value: z.array(z.any()) }),\n });\n if (result.error) {\n throw new Error(\"Failed to find record\");\n }\n return result.data?.value?.[0] ?? null;\n },\n findMany: async ({ model, where, limit, offset, sortBy }) => {\n const filter = parseWhere(where);\n\n const rows = await fetch(`/${model}`, {\n method: \"GET\",\n query: {\n ...(filter.length > 0 ? { $filter: filter } : {}),\n $top: limit,\n $skip: offset,\n ...(sortBy\n ? { $orderby: `\"${sortBy.field}\" ${sortBy.direction ?? \"asc\"}` }\n : {}),\n },\n output: z.object({ value: z.array(z.any()) }),\n });\n if (rows.error) {\n throw new Error(\"Failed to find records\");\n }\n return rows.data?.value ?? [];\n },\n delete: async ({ model, where }) => {\n const result = await fetch(`/${model}`, {\n method: \"DELETE\",\n query: {\n ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),\n $top: 1,\n $select: [`\"id\"`],\n },\n });\n if (result.error) {\n throw new Error(\"Failed to delete record\");\n }\n },\n deleteMany: async ({ model, where }) => {\n const result = await fetch(`/${model}/$count`, {\n method: \"DELETE\",\n query: {\n ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),\n $top: 1,\n $select: [`\"id\"`],\n },\n output: z.coerce.number(),\n });\n if (result.error) {\n throw new Error(\"Failed to delete record\");\n }\n return result.data ?? 0;\n },\n update: async ({ model, where, update }) => {\n const result = await fetch(`/${model}`, {\n method: \"PATCH\",\n query: {\n ...(where.length > 0 ? { $filter: parseWhere(where) } : {}),\n $top: 1,\n $select: [`\"id\"`],\n },\n body: update,\n output: z.object({ value: z.array(z.any()) }),\n });\n return result.data?.value?.[0] ?? null;\n },\n updateMany: async ({ model, where, update }) => {\n const filter = parseWhere(where);\n const result = await fetch(`/${model}`, {\n method: \"PATCH\",\n query: {\n ...(where.length > 0 ? { $filter: filter } : {}),\n },\n body: update,\n });\n return result.data as any;\n },\n };\n },\n });\n};\n"],"names":[],"mappings":"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"}
|
package/dist/esm/cli/index.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
})
|
|
55
|
-
const migrationPlan = await planMigration(
|
|
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(
|
|
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 {
|
|
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;"}
|
package/dist/esm/migrate.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { BetterAuthDbSchema } from 'better-auth/db';
|
|
2
|
-
import {
|
|
2
|
+
import { Metadata } from 'fm-odata-client';
|
|
3
3
|
import { default as z } from 'zod/v4';
|
|
4
|
-
|
|
5
|
-
export declare function
|
|
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<{
|
package/dist/esm/migrate.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import z from "zod/v4";
|
|
3
|
-
async function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
};
|
package/dist/esm/migrate.js.map
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
14
|
+
serverUrl: string;
|
|
14
15
|
auth: ODataAuth;
|
|
15
16
|
database: string;
|
|
17
|
+
logging?: true | "verbose" | "none";
|
|
16
18
|
};
|
|
17
|
-
export declare
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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>;
|
package/dist/esm/odata/index.js
CHANGED
|
@@ -1,27 +1,83 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this.connection = new Connection(
|
|
16
|
-
args.hostname.replace(/^https?:\/\//, "").replace(/\/$/, ""),
|
|
17
|
-
new BasicAuth(args.auth.username, args.auth.password)
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
this.database = new Database(this.connection, args.database);
|
|
70
|
+
function validateUrl(input) {
|
|
71
|
+
try {
|
|
72
|
+
const url = new URL(input);
|
|
73
|
+
return ok(url);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(error);
|
|
76
|
+
return err(error);
|
|
21
77
|
}
|
|
22
78
|
}
|
|
23
79
|
export {
|
|
24
|
-
|
|
25
|
-
|
|
80
|
+
createFmOdataFetch,
|
|
81
|
+
validateUrl
|
|
26
82
|
};
|
|
27
83
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../src/odata/index.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../src/odata/index.ts"],"sourcesContent":["import { createFetch, createSchema } from \"@better-fetch/fetch\";\nimport { logger } from \"@better-fetch/logger\";\nimport { err, ok, Result } from \"neverthrow\";\nimport { z } from \"zod/v4\";\n\nexport type BasicAuthCredentials = {\n username: string;\n password: string;\n};\nexport type OttoAPIKeyAuth = {\n apiKey: string;\n};\nexport type ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;\n\nexport function isBasicAuth(auth: ODataAuth): auth is BasicAuthCredentials {\n return (\n typeof (auth as BasicAuthCredentials).username === \"string\" &&\n typeof (auth as BasicAuthCredentials).password === \"string\"\n );\n}\n\nexport function isOttoAPIKeyAuth(auth: ODataAuth): auth is OttoAPIKeyAuth {\n return typeof (auth as OttoAPIKeyAuth).apiKey === \"string\";\n}\n\nexport type FmOdataConfig = {\n serverUrl: string;\n auth: ODataAuth;\n database: string;\n logging?: true | \"verbose\" | \"none\";\n};\n\nconst schema = createSchema({\n /**\n * Create a new table\n */\n \"@post/FileMaker_Tables\": {\n input: z.object({ tableName: z.string(), fields: z.array(z.any()) }),\n },\n /**\n * Add fields to a table\n */\n \"@patch/FileMaker_Tables/:tableName\": {\n params: z.object({ tableName: z.string() }),\n input: z.object({ fields: z.array(z.any()) }),\n },\n /**\n * Delete a table\n */\n \"@delete/FileMaker_Tables/:tableName\": {\n params: z.object({ tableName: z.string() }),\n },\n /**\n * Delete a field from a table\n */\n \"@delete/FileMaker_Tables/:tableName/:fieldName\": {\n params: z.object({ tableName: z.string(), fieldName: z.string() }),\n },\n});\n\nexport function createFmOdataFetch(args: FmOdataConfig) {\n const result = validateUrl(args.serverUrl);\n\n if (result.isErr()) {\n throw new Error(\"Invalid server URL\");\n }\n let baseURL = result.value.origin;\n if (\"apiKey\" in args.auth) {\n baseURL += `/otto`;\n }\n baseURL += `/fmi/odata/v4/${args.database}`;\n\n return createFetch({\n baseURL,\n auth:\n \"apiKey\" in args.auth\n ? { type: \"Bearer\", token: args.auth.apiKey }\n : {\n type: \"Basic\",\n username: args.auth.username,\n password: args.auth.password,\n },\n onError: (error) => {\n console.error(\"url\", error.request.url.toString());\n console.log(error.error);\n console.log(\"error.request.body\", JSON.stringify(error.request.body));\n },\n schema,\n plugins: [\n logger({\n verbose: args.logging === \"verbose\",\n enabled: args.logging === \"verbose\" || !!args.logging,\n console: {\n fail: (...args) => console.error(...args),\n success: (...args) => console.log(...args),\n log: (...args) => console.log(...args),\n error: (...args) => console.error(...args),\n warn: (...args) => console.warn(...args),\n },\n }),\n ],\n });\n}\n\nexport function validateUrl(input: string): Result<URL, unknown> {\n try {\n const url = new URL(input);\n return ok(url);\n } catch (error) {\n console.error(error);\n return err(error);\n }\n}\n"],"names":["args"],"mappings":";;;;AAgCA,MAAM,SAAS,aAAa;AAAA;AAAA;AAAA;AAAA,EAI1B,0BAA0B;AAAA,IACxB,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,QAAQ,EAAE,MAAM,EAAE,IAAK,CAAA,EAAG,CAAA;AAAA,EACrE;AAAA;AAAA;AAAA;AAAA,EAIA,sCAAsC;AAAA,IACpC,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,OAAA,GAAU;AAAA,IAC1C,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAA,CAAK,EAAG,CAAA;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA,EAIA,uCAAuC;AAAA,IACrC,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,SAAU,CAAA;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA,EAIA,kDAAkD;AAAA,IAChD,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,GAAG,WAAW,EAAE,SAAU,CAAA;AAAA,EAAA;AAErE,CAAC;AAEM,SAAS,mBAAmB,MAAqB;AAChD,QAAA,SAAS,YAAY,KAAK,SAAS;AAErC,MAAA,OAAO,SAAS;AACZ,UAAA,IAAI,MAAM,oBAAoB;AAAA,EAAA;AAElC,MAAA,UAAU,OAAO,MAAM;AACvB,MAAA,YAAY,KAAK,MAAM;AACd,eAAA;AAAA,EAAA;AAEF,aAAA,iBAAiB,KAAK,QAAQ;AAEzC,SAAO,YAAY;AAAA,IACjB;AAAA,IACA,MACE,YAAY,KAAK,OACb,EAAE,MAAM,UAAU,OAAO,KAAK,KAAK,OAAA,IACnC;AAAA,MACE,MAAM;AAAA,MACN,UAAU,KAAK,KAAK;AAAA,MACpB,UAAU,KAAK,KAAK;AAAA,IACtB;AAAA,IACN,SAAS,CAAC,UAAU;AAClB,cAAQ,MAAM,OAAO,MAAM,QAAQ,IAAI,UAAU;AACzC,cAAA,IAAI,MAAM,KAAK;AACvB,cAAQ,IAAI,sBAAsB,KAAK,UAAU,MAAM,QAAQ,IAAI,CAAC;AAAA,IACtE;AAAA,IACA;AAAA,IACA,SAAS;AAAA,MACP,OAAO;AAAA,QACL,SAAS,KAAK,YAAY;AAAA,QAC1B,SAAS,KAAK,YAAY,aAAa,CAAC,CAAC,KAAK;AAAA,QAC9C,SAAS;AAAA,UACP,MAAM,IAAIA,UAAS,QAAQ,MAAM,GAAGA,KAAI;AAAA,UACxC,SAAS,IAAIA,UAAS,QAAQ,IAAI,GAAGA,KAAI;AAAA,UACzC,KAAK,IAAIA,UAAS,QAAQ,IAAI,GAAGA,KAAI;AAAA,UACrC,OAAO,IAAIA,UAAS,QAAQ,MAAM,GAAGA,KAAI;AAAA,UACzC,MAAM,IAAIA,UAAS,QAAQ,KAAK,GAAGA,KAAI;AAAA,QAAA;AAAA,MAE1C,CAAA;AAAA,IAAA;AAAA,EACH,CACD;AACH;AAEO,SAAS,YAAY,OAAqC;AAC3D,MAAA;AACI,UAAA,MAAM,IAAI,IAAI,KAAK;AACzB,WAAO,GAAG,GAAG;AAAA,WACN,OAAO;AACd,YAAQ,MAAM,KAAK;AACnB,WAAO,IAAI,KAAK;AAAA,EAAA;AAEpB;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proofkit/better-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.0",
|
|
4
4
|
"description": "FileMaker adapter for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/esm/index.js",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@babel/preset-react": "^7.27.1",
|
|
40
40
|
"@babel/preset-typescript": "^7.27.1",
|
|
41
|
+
"@better-fetch/fetch": "1.1.17",
|
|
42
|
+
"@better-fetch/logger": "^1.1.18",
|
|
41
43
|
"@commander-js/extra-typings": "^14.0.0",
|
|
42
44
|
"@tanstack/vite-config": "^0.2.0",
|
|
43
45
|
"better-auth": "^1.2.10",
|
|
@@ -45,17 +47,16 @@
|
|
|
45
47
|
"chalk": "5.4.1",
|
|
46
48
|
"commander": "^14.0.0",
|
|
47
49
|
"dotenv": "^16.5.0",
|
|
48
|
-
"execa": "^9.5.3",
|
|
49
|
-
"fm-odata-client": "^3.0.1",
|
|
50
50
|
"fs-extra": "^11.3.0",
|
|
51
|
+
"neverthrow": "^8.2.0",
|
|
51
52
|
"prompts": "^2.4.2",
|
|
52
53
|
"vite": "^6.3.4",
|
|
53
|
-
"zod": "3.25.64"
|
|
54
|
-
"@proofkit/fmdapi": "5.0.0"
|
|
54
|
+
"zod": "3.25.64"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/fs-extra": "^11.0.4",
|
|
58
58
|
"@types/prompts": "^2.4.9",
|
|
59
|
+
"fm-odata-client": "^3.0.1",
|
|
59
60
|
"publint": "^0.3.12",
|
|
60
61
|
"typescript": "^5.8.3",
|
|
61
62
|
"vitest": "^3.2.3"
|
package/src/adapter.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
createAdapter,
|
|
4
4
|
type AdapterDebugLogs,
|
|
5
5
|
} from "better-auth/adapters";
|
|
6
|
-
import {
|
|
6
|
+
import { createFmOdataFetch, type FmOdataConfig } from "./odata";
|
|
7
7
|
import { prettifyError, z } from "zod/v4";
|
|
8
8
|
|
|
9
9
|
interface FileMakerAdapterConfig {
|
|
@@ -30,7 +30,7 @@ const configSchema = z.object({
|
|
|
30
30
|
debugLogs: z.unknown().optional(),
|
|
31
31
|
usePlural: z.boolean().optional(),
|
|
32
32
|
odata: z.object({
|
|
33
|
-
|
|
33
|
+
serverUrl: z.string(),
|
|
34
34
|
auth: z.object({ username: z.string(), password: z.string() }),
|
|
35
35
|
database: z.string().endsWith(".fmp12"),
|
|
36
36
|
}),
|
|
@@ -40,7 +40,7 @@ const defaultConfig: Required<FileMakerAdapterConfig> = {
|
|
|
40
40
|
debugLogs: false,
|
|
41
41
|
usePlural: false,
|
|
42
42
|
odata: {
|
|
43
|
-
|
|
43
|
+
serverUrl: "",
|
|
44
44
|
auth: { username: "", password: "" },
|
|
45
45
|
database: "",
|
|
46
46
|
},
|
|
@@ -71,7 +71,7 @@ export function parseWhere(where?: CleanedWhere[]): string {
|
|
|
71
71
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
72
72
|
if (value instanceof Date) return `'${value.toISOString()}'`;
|
|
73
73
|
if (Array.isArray(value)) return `(${value.map(formatValue).join(",")})`;
|
|
74
|
-
return value
|
|
74
|
+
return value?.toString() ?? "";
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// Map our operators to OData
|
|
@@ -139,9 +139,10 @@ export const FileMakerAdapter = (
|
|
|
139
139
|
}
|
|
140
140
|
const config = parsed.data;
|
|
141
141
|
|
|
142
|
-
const
|
|
143
|
-
config.odata
|
|
144
|
-
|
|
142
|
+
const fetch = createFmOdataFetch({
|
|
143
|
+
...config.odata,
|
|
144
|
+
// logging: config.debugLogs ? true : "none",
|
|
145
|
+
});
|
|
145
146
|
|
|
146
147
|
return createAdapter({
|
|
147
148
|
config: {
|
|
@@ -158,62 +159,116 @@ export const FileMakerAdapter = (
|
|
|
158
159
|
return {
|
|
159
160
|
options: { config },
|
|
160
161
|
create: async ({ data, model, select }) => {
|
|
161
|
-
const
|
|
162
|
-
|
|
162
|
+
const result = await fetch(`/${model}`, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
body: data,
|
|
165
|
+
output: z.looseObject({ id: z.string() }),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (result.error) {
|
|
169
|
+
throw new Error("Failed to create record");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return result.data as any;
|
|
163
173
|
},
|
|
164
174
|
count: async ({ model, where }) => {
|
|
165
|
-
const
|
|
166
|
-
|
|
175
|
+
const result = await fetch(`/${model}/$count`, {
|
|
176
|
+
method: "GET",
|
|
177
|
+
query: {
|
|
178
|
+
$filter: parseWhere(where),
|
|
179
|
+
},
|
|
180
|
+
output: z.object({ value: z.number() }),
|
|
181
|
+
});
|
|
182
|
+
if (!result.data) {
|
|
183
|
+
throw new Error("Failed to count records");
|
|
184
|
+
}
|
|
185
|
+
return result.data?.value ?? 0;
|
|
167
186
|
},
|
|
168
187
|
findOne: async ({ model, where }) => {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
188
|
+
const result = await fetch(`/${model}`, {
|
|
189
|
+
method: "GET",
|
|
190
|
+
query: {
|
|
191
|
+
...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
|
|
192
|
+
$top: 1,
|
|
193
|
+
},
|
|
194
|
+
output: z.object({ value: z.array(z.any()) }),
|
|
172
195
|
});
|
|
173
|
-
|
|
196
|
+
if (result.error) {
|
|
197
|
+
throw new Error("Failed to find record");
|
|
198
|
+
}
|
|
199
|
+
return result.data?.value?.[0] ?? null;
|
|
174
200
|
},
|
|
175
201
|
findMany: async ({ model, where, limit, offset, sortBy }) => {
|
|
176
202
|
const filter = parseWhere(where);
|
|
177
203
|
|
|
178
|
-
const rows = await
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
204
|
+
const rows = await fetch(`/${model}`, {
|
|
205
|
+
method: "GET",
|
|
206
|
+
query: {
|
|
207
|
+
...(filter.length > 0 ? { $filter: filter } : {}),
|
|
208
|
+
$top: limit,
|
|
209
|
+
$skip: offset,
|
|
210
|
+
...(sortBy
|
|
211
|
+
? { $orderby: `"${sortBy.field}" ${sortBy.direction ?? "asc"}` }
|
|
212
|
+
: {}),
|
|
213
|
+
},
|
|
214
|
+
output: z.object({ value: z.array(z.any()) }),
|
|
183
215
|
});
|
|
184
|
-
|
|
216
|
+
if (rows.error) {
|
|
217
|
+
throw new Error("Failed to find records");
|
|
218
|
+
}
|
|
219
|
+
return rows.data?.value ?? [];
|
|
185
220
|
},
|
|
186
221
|
delete: async ({ model, where }) => {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
222
|
+
const result = await fetch(`/${model}`, {
|
|
223
|
+
method: "DELETE",
|
|
224
|
+
query: {
|
|
225
|
+
...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
|
|
226
|
+
$top: 1,
|
|
227
|
+
$select: [`"id"`],
|
|
228
|
+
},
|
|
191
229
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
230
|
+
if (result.error) {
|
|
231
|
+
throw new Error("Failed to delete record");
|
|
232
|
+
}
|
|
195
233
|
},
|
|
196
234
|
deleteMany: async ({ model, where }) => {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
235
|
+
const result = await fetch(`/${model}/$count`, {
|
|
236
|
+
method: "DELETE",
|
|
237
|
+
query: {
|
|
238
|
+
...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
|
|
239
|
+
$top: 1,
|
|
240
|
+
$select: [`"id"`],
|
|
241
|
+
},
|
|
242
|
+
output: z.coerce.number(),
|
|
243
|
+
});
|
|
244
|
+
if (result.error) {
|
|
245
|
+
throw new Error("Failed to delete record");
|
|
246
|
+
}
|
|
247
|
+
return result.data ?? 0;
|
|
201
248
|
},
|
|
202
249
|
update: async ({ model, where, update }) => {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
250
|
+
const result = await fetch(`/${model}`, {
|
|
251
|
+
method: "PATCH",
|
|
252
|
+
query: {
|
|
253
|
+
...(where.length > 0 ? { $filter: parseWhere(where) } : {}),
|
|
254
|
+
$top: 1,
|
|
255
|
+
$select: [`"id"`],
|
|
256
|
+
},
|
|
257
|
+
body: update,
|
|
258
|
+
output: z.object({ value: z.array(z.any()) }),
|
|
207
259
|
});
|
|
208
|
-
|
|
209
|
-
if (!row) return null;
|
|
210
|
-
const result = await db.table(model).update(row["id"], update as any);
|
|
211
|
-
return result as any;
|
|
260
|
+
return result.data?.value?.[0] ?? null;
|
|
212
261
|
},
|
|
213
262
|
updateMany: async ({ model, where, update }) => {
|
|
214
263
|
const filter = parseWhere(where);
|
|
215
|
-
const
|
|
216
|
-
|
|
264
|
+
const result = await fetch(`/${model}`, {
|
|
265
|
+
method: "PATCH",
|
|
266
|
+
query: {
|
|
267
|
+
...(where.length > 0 ? { $filter: filter } : {}),
|
|
268
|
+
},
|
|
269
|
+
body: update,
|
|
270
|
+
});
|
|
271
|
+
return result.data as any;
|
|
217
272
|
},
|
|
218
273
|
};
|
|
219
274
|
},
|
package/src/cli/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { BasicAuth, Connection, Database } from "fm-odata-client";
|
|
|
14
14
|
import prompts from "prompts";
|
|
15
15
|
import chalk from "chalk";
|
|
16
16
|
import { AdapterOptions } from "../adapter";
|
|
17
|
-
import {
|
|
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
|
|
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
|
-
})
|
|
77
|
+
});
|
|
78
78
|
|
|
79
|
-
const migrationPlan = await planMigration(
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
23
|
+
fetch: ReturnType<typeof createFmOdataFetch>,
|
|
8
24
|
betterAuthSchema: BetterAuthDbSchema,
|
|
25
|
+
databaseName: string,
|
|
9
26
|
): Promise<MigrationPlan> {
|
|
10
|
-
const metadata = await
|
|
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
|
-
|
|
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
|
|
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
|
|
168
|
+
await fetch("@post/FileMaker_Tables/:tableName", {
|
|
169
|
+
params: { tableName: step.tableName },
|
|
170
|
+
body: { fields: step.fields },
|
|
171
|
+
});
|
|
150
172
|
}
|
|
151
173
|
}
|
|
152
174
|
}
|
package/src/odata/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createFetch, createSchema } from "@better-fetch/fetch";
|
|
2
|
+
import { logger } from "@better-fetch/logger";
|
|
3
|
+
import { err, ok, Result } from "neverthrow";
|
|
4
|
+
import { z } from "zod/v4";
|
|
2
5
|
|
|
3
6
|
export type BasicAuthCredentials = {
|
|
4
7
|
username: string;
|
|
@@ -21,25 +24,90 @@ export function isOttoAPIKeyAuth(auth: ODataAuth): auth is OttoAPIKeyAuth {
|
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export type FmOdataConfig = {
|
|
24
|
-
|
|
27
|
+
serverUrl: string;
|
|
25
28
|
auth: ODataAuth;
|
|
26
29
|
database: string;
|
|
30
|
+
logging?: true | "verbose" | "none";
|
|
27
31
|
};
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
const schema = createSchema({
|
|
34
|
+
/**
|
|
35
|
+
* Create a new table
|
|
36
|
+
*/
|
|
37
|
+
"@post/FileMaker_Tables": {
|
|
38
|
+
input: z.object({ tableName: z.string(), fields: z.array(z.any()) }),
|
|
39
|
+
},
|
|
40
|
+
/**
|
|
41
|
+
* Add fields to a table
|
|
42
|
+
*/
|
|
43
|
+
"@patch/FileMaker_Tables/:tableName": {
|
|
44
|
+
params: z.object({ tableName: z.string() }),
|
|
45
|
+
input: z.object({ fields: z.array(z.any()) }),
|
|
46
|
+
},
|
|
47
|
+
/**
|
|
48
|
+
* Delete a table
|
|
49
|
+
*/
|
|
50
|
+
"@delete/FileMaker_Tables/:tableName": {
|
|
51
|
+
params: z.object({ tableName: z.string() }),
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* Delete a field from a table
|
|
55
|
+
*/
|
|
56
|
+
"@delete/FileMaker_Tables/:tableName/:fieldName": {
|
|
57
|
+
params: z.object({ tableName: z.string(), fieldName: z.string() }),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export function createFmOdataFetch(args: FmOdataConfig) {
|
|
62
|
+
const result = validateUrl(args.serverUrl);
|
|
63
|
+
|
|
64
|
+
if (result.isErr()) {
|
|
65
|
+
throw new Error("Invalid server URL");
|
|
66
|
+
}
|
|
67
|
+
let baseURL = result.value.origin;
|
|
68
|
+
if ("apiKey" in args.auth) {
|
|
69
|
+
baseURL += `/otto`;
|
|
70
|
+
}
|
|
71
|
+
baseURL += `/fmi/odata/v4/${args.database}`;
|
|
72
|
+
|
|
73
|
+
return createFetch({
|
|
74
|
+
baseURL,
|
|
75
|
+
auth:
|
|
76
|
+
"apiKey" in args.auth
|
|
77
|
+
? { type: "Bearer", token: args.auth.apiKey }
|
|
78
|
+
: {
|
|
79
|
+
type: "Basic",
|
|
80
|
+
username: args.auth.username,
|
|
81
|
+
password: args.auth.password,
|
|
82
|
+
},
|
|
83
|
+
onError: (error) => {
|
|
84
|
+
console.error("url", error.request.url.toString());
|
|
85
|
+
console.log(error.error);
|
|
86
|
+
console.log("error.request.body", JSON.stringify(error.request.body));
|
|
87
|
+
},
|
|
88
|
+
schema,
|
|
89
|
+
plugins: [
|
|
90
|
+
logger({
|
|
91
|
+
verbose: args.logging === "verbose",
|
|
92
|
+
enabled: args.logging === "verbose" || !!args.logging,
|
|
93
|
+
console: {
|
|
94
|
+
fail: (...args) => console.error(...args),
|
|
95
|
+
success: (...args) => console.log(...args),
|
|
96
|
+
log: (...args) => console.log(...args),
|
|
97
|
+
error: (...args) => console.error(...args),
|
|
98
|
+
warn: (...args) => console.warn(...args),
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function validateUrl(input: string): Result<URL, unknown> {
|
|
106
|
+
try {
|
|
107
|
+
const url = new URL(input);
|
|
108
|
+
return ok(url);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(error);
|
|
111
|
+
return err(error);
|
|
44
112
|
}
|
|
45
113
|
}
|