@proofkit/better-auth 0.3.1-beta.1 → 0.4.0-beta.3
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.d.ts +3 -6
- package/dist/esm/adapter.js +56 -93
- package/dist/esm/adapter.js.map +1 -1
- package/dist/esm/cli/index.js +52 -17
- package/dist/esm/cli/index.js.map +1 -1
- package/dist/esm/migrate.d.ts +21 -83
- package/dist/esm/migrate.js +81 -100
- package/dist/esm/migrate.js.map +1 -1
- package/package.json +2 -5
- package/src/adapter.ts +78 -105
- package/src/cli/index.ts +72 -21
- package/src/migrate.ts +129 -160
- package/dist/esm/odata/index.d.ts +0 -29
- package/dist/esm/odata/index.js +0 -157
- package/dist/esm/odata/index.js.map +0 -1
- package/src/odata/index.ts +0 -219
package/src/migrate.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import type { Database, Field, Metadata } from "@proofkit/fmodata";
|
|
2
|
+
import { isFMODataError, isODataError } from "@proofkit/fmodata";
|
|
1
3
|
import type { DBFieldAttribute } from "better-auth/db";
|
|
2
4
|
import chalk from "chalk";
|
|
3
|
-
import type { Metadata } from "fm-odata-client";
|
|
4
|
-
import z from "zod/v4";
|
|
5
|
-
import type { createRawFetch } from "./odata";
|
|
6
5
|
|
|
7
6
|
/** Schema type returned by better-auth's getSchema function */
|
|
8
7
|
type BetterAuthSchema = Record<string, { fields: Record<string, DBFieldAttribute>; order: number }>;
|
|
@@ -17,112 +16,88 @@ function normalizeBetterAuthFieldType(fieldType: unknown): string {
|
|
|
17
16
|
return String(fieldType);
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
export async function getMetadata(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
headers: { accept: "application/json" },
|
|
25
|
-
output: z
|
|
26
|
-
.looseObject({
|
|
27
|
-
$Version: z.string(),
|
|
28
|
-
"@ServerVersion": z.string(),
|
|
29
|
-
})
|
|
30
|
-
.or(z.null())
|
|
31
|
-
.catch(null),
|
|
32
|
-
});
|
|
19
|
+
export async function getMetadata(db: Database): Promise<Metadata> {
|
|
20
|
+
const metadata = await db.getMetadata({ format: "json" });
|
|
21
|
+
return metadata;
|
|
22
|
+
}
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
/** Map a better-auth field type string to an fmodata Field type */
|
|
25
|
+
function mapFieldType(t: string): "string" | "numeric" | "timestamp" {
|
|
26
|
+
if (t.includes("boolean") || t.includes("number")) {
|
|
27
|
+
return "numeric";
|
|
37
28
|
}
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
if (t.includes("date")) {
|
|
30
|
+
return "timestamp";
|
|
31
|
+
}
|
|
32
|
+
return "string";
|
|
40
33
|
}
|
|
41
34
|
|
|
42
|
-
export async function planMigration(
|
|
43
|
-
|
|
44
|
-
betterAuthSchema: BetterAuthSchema,
|
|
45
|
-
databaseName: string,
|
|
46
|
-
): Promise<MigrationPlan> {
|
|
47
|
-
const metadata = await getMetadata(fetch, databaseName);
|
|
35
|
+
export async function planMigration(db: Database, betterAuthSchema: BetterAuthSchema): Promise<MigrationPlan> {
|
|
36
|
+
const metadata = await getMetadata(db);
|
|
48
37
|
|
|
49
38
|
// Build a map from entity set name to entity type key
|
|
50
39
|
const entitySetToType: Record<string, string> = {};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
entitySetToType[key] = typeKey || key;
|
|
57
|
-
}
|
|
40
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
41
|
+
if (value.$Kind === "EntitySet" && value.$Type) {
|
|
42
|
+
// $Type is like 'betterauth_test.fmp12.proofkit_user_'
|
|
43
|
+
const typeKey = value.$Type.split(".").pop(); // e.g., 'proofkit_user_'
|
|
44
|
+
entitySetToType[key] = typeKey || key;
|
|
58
45
|
}
|
|
59
46
|
}
|
|
60
47
|
|
|
61
|
-
const existingTables =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
const existingTables = Object.entries(entitySetToType).reduce(
|
|
49
|
+
(acc, [entitySetName, entityTypeKey]) => {
|
|
50
|
+
const entityType = metadata[entityTypeKey];
|
|
51
|
+
if (!entityType) {
|
|
52
|
+
return acc;
|
|
53
|
+
}
|
|
54
|
+
const fields = Object.entries(entityType)
|
|
55
|
+
.filter(
|
|
56
|
+
([_fieldKey, fieldValue]) => typeof fieldValue === "object" && fieldValue !== null && "$Type" in fieldValue,
|
|
57
|
+
)
|
|
58
|
+
.map(([fieldKey, fieldValue]) => {
|
|
59
|
+
let type = "string";
|
|
60
|
+
if (fieldValue.$Type === "Edm.String") {
|
|
61
|
+
type = "string";
|
|
62
|
+
} else if (fieldValue.$Type === "Edm.DateTimeOffset") {
|
|
63
|
+
type = "timestamp";
|
|
64
|
+
} else if (
|
|
65
|
+
fieldValue.$Type === "Edm.Decimal" ||
|
|
66
|
+
fieldValue.$Type === "Edm.Int32" ||
|
|
67
|
+
fieldValue.$Type === "Edm.Int64"
|
|
68
|
+
) {
|
|
69
|
+
type = "numeric";
|
|
67
70
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
type = "timestamp";
|
|
79
|
-
} else if (
|
|
80
|
-
fieldValue.$Type === "Edm.Decimal" ||
|
|
81
|
-
fieldValue.$Type === "Edm.Int32" ||
|
|
82
|
-
fieldValue.$Type === "Edm.Int64"
|
|
83
|
-
) {
|
|
84
|
-
type = "numeric";
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
name: fieldKey,
|
|
88
|
-
type,
|
|
89
|
-
};
|
|
90
|
-
});
|
|
91
|
-
acc[entitySetName] = fields;
|
|
92
|
-
return acc;
|
|
93
|
-
},
|
|
94
|
-
{} as Record<string, { name: string; type: string }[]>,
|
|
95
|
-
)
|
|
96
|
-
: {};
|
|
71
|
+
return {
|
|
72
|
+
name: fieldKey,
|
|
73
|
+
type,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
acc[entitySetName] = fields;
|
|
77
|
+
return acc;
|
|
78
|
+
},
|
|
79
|
+
{} as Record<string, { name: string; type: string }[]>,
|
|
80
|
+
);
|
|
97
81
|
|
|
98
82
|
const baTables = Object.entries(betterAuthSchema)
|
|
99
83
|
.sort((a, b) => (a[1].order ?? 0) - (b[1].order ?? 0))
|
|
100
84
|
.map(([key, value]) => ({
|
|
101
85
|
...value,
|
|
102
|
-
modelName: key,
|
|
86
|
+
modelName: key,
|
|
103
87
|
}));
|
|
104
88
|
|
|
105
89
|
const migrationPlan: MigrationPlan = [];
|
|
106
90
|
|
|
107
91
|
for (const baTable of baTables) {
|
|
108
92
|
const fields: FmField[] = Object.entries(baTable.fields).map(([key, field]) => {
|
|
109
|
-
// Better Auth's FieldType can be a string literal union or arrays.
|
|
110
|
-
// Normalize it to a string so our FM mapping logic remains stable.
|
|
111
|
-
// Use .includes() for all checks to handle array types like ["boolean", "null"] → "boolean|null"
|
|
112
93
|
const t = normalizeBetterAuthFieldType(field.type);
|
|
113
|
-
|
|
114
|
-
if (t.includes("boolean") || t.includes("number")) {
|
|
115
|
-
type = "numeric";
|
|
116
|
-
} else if (t.includes("date")) {
|
|
117
|
-
type = "timestamp";
|
|
118
|
-
}
|
|
94
|
+
const type = mapFieldType(t);
|
|
119
95
|
return {
|
|
120
96
|
name: field.fieldName ?? key,
|
|
121
97
|
type,
|
|
122
98
|
};
|
|
123
99
|
});
|
|
124
100
|
|
|
125
|
-
// get existing table or create it
|
|
126
101
|
const tableExists = baTable.modelName in existingTables;
|
|
127
102
|
|
|
128
103
|
if (tableExists) {
|
|
@@ -134,7 +109,6 @@ export async function planMigration(
|
|
|
134
109
|
},
|
|
135
110
|
{} as Record<string, string>,
|
|
136
111
|
);
|
|
137
|
-
// Warn about type mismatches (optional, not in plan)
|
|
138
112
|
for (const field of fields) {
|
|
139
113
|
if (existingFields.includes(field.name) && existingFieldMap[field.name] !== field.type) {
|
|
140
114
|
console.warn(
|
|
@@ -157,7 +131,7 @@ export async function planMigration(
|
|
|
157
131
|
fields: [
|
|
158
132
|
{
|
|
159
133
|
name: "id",
|
|
160
|
-
type: "
|
|
134
|
+
type: "string",
|
|
161
135
|
primary: true,
|
|
162
136
|
unique: true,
|
|
163
137
|
},
|
|
@@ -170,106 +144,101 @@ export async function planMigration(
|
|
|
170
144
|
return migrationPlan;
|
|
171
145
|
}
|
|
172
146
|
|
|
173
|
-
export async function executeMigration(
|
|
174
|
-
fetch: ReturnType<typeof createRawFetch>["fetch"],
|
|
175
|
-
migrationPlan: MigrationPlan,
|
|
176
|
-
) {
|
|
147
|
+
export async function executeMigration(db: Database, migrationPlan: MigrationPlan) {
|
|
177
148
|
for (const step of migrationPlan) {
|
|
149
|
+
// Convert plan fields to fmodata Field type
|
|
150
|
+
const fmodataFields: Field[] = step.fields.map((f) => ({
|
|
151
|
+
name: f.name,
|
|
152
|
+
type: f.type,
|
|
153
|
+
...(f.primary ? { primary: true } : {}),
|
|
154
|
+
...(f.unique ? { unique: true } : {}),
|
|
155
|
+
}));
|
|
156
|
+
|
|
178
157
|
if (step.operation === "create") {
|
|
179
158
|
console.log("Creating table:", step.tableName);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
fields: step.fields,
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
if (result.error) {
|
|
189
|
-
console.error(`Failed to create table ${step.tableName}:`, result.error);
|
|
190
|
-
throw new Error(`Migration failed: ${result.error}`);
|
|
159
|
+
try {
|
|
160
|
+
await db.schema.createTable(step.tableName, fmodataFields);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw migrationError("create", step.tableName, error);
|
|
191
163
|
}
|
|
192
164
|
} else if (step.operation === "update") {
|
|
193
165
|
console.log("Adding fields to table:", step.tableName);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (result.error) {
|
|
200
|
-
console.error(`Failed to update table ${step.tableName}:`, result.error);
|
|
201
|
-
throw new Error(`Migration failed: ${result.error}`);
|
|
166
|
+
try {
|
|
167
|
+
await db.schema.addFields(step.tableName, fmodataFields);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw migrationError("update", step.tableName, error);
|
|
202
170
|
}
|
|
203
171
|
}
|
|
204
172
|
}
|
|
205
173
|
}
|
|
206
174
|
|
|
207
|
-
|
|
208
|
-
name:
|
|
209
|
-
|
|
210
|
-
primary
|
|
211
|
-
unique
|
|
212
|
-
|
|
213
|
-
repetitions: z.number().optional(),
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
const stringFieldSchema = genericFieldSchema.extend({
|
|
217
|
-
type: z.literal("varchar"),
|
|
218
|
-
maxLength: z.number().optional(),
|
|
219
|
-
default: z.enum(["USER", "USERNAME", "CURRENT_USER"]).optional(),
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const numericFieldSchema = genericFieldSchema.extend({
|
|
223
|
-
type: z.literal("numeric"),
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const dateFieldSchema = genericFieldSchema.extend({
|
|
227
|
-
type: z.literal("date"),
|
|
228
|
-
default: z.enum(["CURRENT_DATE", "CURDATE"]).optional(),
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
const timeFieldSchema = genericFieldSchema.extend({
|
|
232
|
-
type: z.literal("time"),
|
|
233
|
-
default: z.enum(["CURRENT_TIME", "CURTIME"]).optional(),
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const timestampFieldSchema = genericFieldSchema.extend({
|
|
237
|
-
type: z.literal("timestamp"),
|
|
238
|
-
default: z.enum(["CURRENT_TIMESTAMP", "CURTIMESTAMP"]).optional(),
|
|
239
|
-
});
|
|
175
|
+
interface FmField {
|
|
176
|
+
name: string;
|
|
177
|
+
type: "string" | "numeric" | "timestamp";
|
|
178
|
+
primary?: boolean;
|
|
179
|
+
unique?: boolean;
|
|
180
|
+
}
|
|
240
181
|
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
182
|
+
const migrationStepTypes = ["create", "update"] as const;
|
|
183
|
+
interface MigrationStep {
|
|
184
|
+
tableName: string;
|
|
185
|
+
operation: (typeof migrationStepTypes)[number];
|
|
186
|
+
fields: FmField[];
|
|
187
|
+
}
|
|
245
188
|
|
|
246
|
-
|
|
247
|
-
stringFieldSchema,
|
|
248
|
-
numericFieldSchema,
|
|
249
|
-
dateFieldSchema,
|
|
250
|
-
timeFieldSchema,
|
|
251
|
-
timestampFieldSchema,
|
|
252
|
-
containerFieldSchema,
|
|
253
|
-
]);
|
|
189
|
+
export type MigrationPlan = MigrationStep[];
|
|
254
190
|
|
|
255
|
-
|
|
191
|
+
function formatError(error: unknown): string {
|
|
192
|
+
if (isODataError(error)) {
|
|
193
|
+
const code = error.code ? ` (${error.code})` : "";
|
|
194
|
+
return `${error.message}${code}`;
|
|
195
|
+
}
|
|
196
|
+
if (isFMODataError(error)) {
|
|
197
|
+
return error.message;
|
|
198
|
+
}
|
|
199
|
+
if (error instanceof Error) {
|
|
200
|
+
return error.message;
|
|
201
|
+
}
|
|
202
|
+
return String(error);
|
|
203
|
+
}
|
|
256
204
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
operation: z.enum(["create", "update"]),
|
|
261
|
-
fields: z.array(fieldSchema),
|
|
262
|
-
})
|
|
263
|
-
.array();
|
|
205
|
+
function migrationError(operation: string, tableName: string, error: unknown): Error {
|
|
206
|
+
const action = operation === "create" ? "create table" : "update table";
|
|
207
|
+
const base = `Failed to ${action} "${tableName}"`;
|
|
264
208
|
|
|
265
|
-
|
|
209
|
+
if (isODataError(error) && error.code === "207") {
|
|
210
|
+
console.error(
|
|
211
|
+
chalk.red(`\n${base}: Cannot modify schema.`),
|
|
212
|
+
chalk.yellow("\nThe account used does not have schema modification privileges."),
|
|
213
|
+
chalk.gray(
|
|
214
|
+
"\nUse --username and --password to provide Full Access credentials, or grant schema modification privileges to the current account.",
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
console.error(chalk.red(`\n${base}:`), formatError(error));
|
|
219
|
+
}
|
|
220
|
+
return new Error(`Migration failed: ${formatError(error)}`);
|
|
221
|
+
}
|
|
266
222
|
|
|
267
|
-
export function prettyPrintMigrationPlan(
|
|
223
|
+
export function prettyPrintMigrationPlan(
|
|
224
|
+
migrationPlan: MigrationPlan,
|
|
225
|
+
target?: { serverUrl?: string; fileName?: string },
|
|
226
|
+
) {
|
|
268
227
|
if (!migrationPlan.length) {
|
|
269
228
|
console.log("No changes to apply. Database is up to date.");
|
|
270
229
|
return;
|
|
271
230
|
}
|
|
272
231
|
console.log(chalk.bold.green("Migration plan:"));
|
|
232
|
+
if (target?.serverUrl || target?.fileName) {
|
|
233
|
+
const parts: string[] = [];
|
|
234
|
+
if (target.fileName) {
|
|
235
|
+
parts.push(chalk.cyan(target.fileName));
|
|
236
|
+
}
|
|
237
|
+
if (target.serverUrl) {
|
|
238
|
+
parts.push(chalk.gray(target.serverUrl));
|
|
239
|
+
}
|
|
240
|
+
console.log(` Target: ${parts.join(" @ ")}`);
|
|
241
|
+
}
|
|
273
242
|
for (const step of migrationPlan) {
|
|
274
243
|
const emoji = step.operation === "create" ? "✅" : "✏️";
|
|
275
244
|
console.log(
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Result } from 'neverthrow';
|
|
2
|
-
import { z } from 'zod/v4';
|
|
3
|
-
interface BasicAuthCredentials {
|
|
4
|
-
username: string;
|
|
5
|
-
password: string;
|
|
6
|
-
}
|
|
7
|
-
interface OttoAPIKeyAuth {
|
|
8
|
-
apiKey: string;
|
|
9
|
-
}
|
|
10
|
-
type ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;
|
|
11
|
-
export interface FmOdataConfig {
|
|
12
|
-
serverUrl: string;
|
|
13
|
-
auth: ODataAuth;
|
|
14
|
-
database: string;
|
|
15
|
-
logging?: true | "verbose" | "none";
|
|
16
|
-
}
|
|
17
|
-
export declare function validateUrl(input: string): Result<URL, unknown>;
|
|
18
|
-
export declare function createRawFetch(args: FmOdataConfig): {
|
|
19
|
-
baseURL: string;
|
|
20
|
-
fetch: <TOutput = any>(input: string | URL | Request, options?: Omit<RequestInit, "body"> & {
|
|
21
|
-
body?: any;
|
|
22
|
-
output?: z.ZodSchema<TOutput>;
|
|
23
|
-
}) => Promise<{
|
|
24
|
-
data?: TOutput;
|
|
25
|
-
error?: string;
|
|
26
|
-
response?: Response;
|
|
27
|
-
}>;
|
|
28
|
-
};
|
|
29
|
-
export {};
|
package/dist/esm/odata/index.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { logger } from "better-auth";
|
|
2
|
-
import { ok, err } from "neverthrow";
|
|
3
|
-
function validateUrl(input) {
|
|
4
|
-
try {
|
|
5
|
-
const url = new URL(input);
|
|
6
|
-
return ok(url);
|
|
7
|
-
} catch (error) {
|
|
8
|
-
return err(error);
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
function createRawFetch(args) {
|
|
12
|
-
const result = validateUrl(args.serverUrl);
|
|
13
|
-
if (result.isErr()) {
|
|
14
|
-
throw new Error("Invalid server URL");
|
|
15
|
-
}
|
|
16
|
-
let baseURL = result.value.origin;
|
|
17
|
-
if ("apiKey" in args.auth) {
|
|
18
|
-
baseURL += "/otto";
|
|
19
|
-
}
|
|
20
|
-
baseURL += `/fmi/odata/v4/${args.database}`;
|
|
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}`;
|
|
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("raw-fetch", `${requestInit.method || "GET"} ${url}`);
|
|
56
|
-
if (requestInit.body) {
|
|
57
|
-
logger.info("raw-fetch", "Request body:", requestInit.body);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const response = await fetch(url, requestInit);
|
|
61
|
-
if (args.logging === "verbose" || args.logging === true) {
|
|
62
|
-
logger.info("raw-fetch", `Response status: ${response.status} ${response.statusText}`);
|
|
63
|
-
logger.info("raw-fetch", "Response headers:", Object.fromEntries(response.headers.entries()));
|
|
64
|
-
}
|
|
65
|
-
if (!response.ok) {
|
|
66
|
-
const errorText = await response.text().catch(() => "Unknown error");
|
|
67
|
-
if (args.logging === "verbose" || args.logging === true) {
|
|
68
|
-
logger.error("raw-fetch", `HTTP Error ${response.status}: ${errorText}`);
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
error: `HTTP ${response.status}: ${errorText}`,
|
|
72
|
-
response
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
let responseData;
|
|
76
|
-
const contentType = response.headers.get("content-type");
|
|
77
|
-
if (args.logging === "verbose" || args.logging === true) {
|
|
78
|
-
logger.info("raw-fetch", `Response content-type: ${contentType || "none"}`);
|
|
79
|
-
}
|
|
80
|
-
if (contentType == null ? void 0 : contentType.includes("application/json")) {
|
|
81
|
-
try {
|
|
82
|
-
const responseText = await response.text();
|
|
83
|
-
if (args.logging === "verbose" || args.logging === true) {
|
|
84
|
-
logger.info("raw-fetch", `Raw response text: "${responseText}"`);
|
|
85
|
-
logger.info("raw-fetch", `Response text length: ${responseText.length}`);
|
|
86
|
-
}
|
|
87
|
-
if (responseText.trim() === "") {
|
|
88
|
-
if (args.logging === "verbose" || args.logging === true) {
|
|
89
|
-
logger.info("raw-fetch", "Empty JSON response, returning null");
|
|
90
|
-
}
|
|
91
|
-
responseData = null;
|
|
92
|
-
} else {
|
|
93
|
-
responseData = JSON.parse(responseText);
|
|
94
|
-
if (args.logging === "verbose" || args.logging === true) {
|
|
95
|
-
logger.info("raw-fetch", "Successfully parsed JSON response");
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} catch (parseError) {
|
|
99
|
-
if (args.logging === "verbose" || args.logging === true) {
|
|
100
|
-
logger.error("raw-fetch", "JSON parse error:", parseError);
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
error: `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : "Unknown parse error"}`,
|
|
104
|
-
response
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
} else if (contentType == null ? void 0 : contentType.includes("text/")) {
|
|
108
|
-
responseData = await response.text();
|
|
109
|
-
if (args.logging === "verbose" || args.logging === true) {
|
|
110
|
-
logger.info("raw-fetch", `Text response: "${responseData}"`);
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
try {
|
|
114
|
-
responseData = await response.text();
|
|
115
|
-
if (args.logging === "verbose" || args.logging === true) {
|
|
116
|
-
logger.info("raw-fetch", `Unknown content-type response as text: "${responseData}"`);
|
|
117
|
-
}
|
|
118
|
-
} catch {
|
|
119
|
-
responseData = null;
|
|
120
|
-
if (args.logging === "verbose" || args.logging === true) {
|
|
121
|
-
logger.info("raw-fetch", "Could not parse response as text, returning null");
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (options == null ? void 0 : options.output) {
|
|
126
|
-
const validation = options.output.safeParse(responseData);
|
|
127
|
-
if (validation.success) {
|
|
128
|
-
return {
|
|
129
|
-
data: validation.data,
|
|
130
|
-
response
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
return {
|
|
134
|
-
error: `Validation failed: ${validation.error.message}`,
|
|
135
|
-
response
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
data: responseData,
|
|
140
|
-
response
|
|
141
|
-
};
|
|
142
|
-
} catch (error) {
|
|
143
|
-
return {
|
|
144
|
-
error: error instanceof Error ? error.message : "Unknown error occurred"
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
return {
|
|
149
|
-
baseURL,
|
|
150
|
-
fetch: wrappedFetch
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
export {
|
|
154
|
-
createRawFetch,
|
|
155
|
-
validateUrl
|
|
156
|
-
};
|
|
157
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../src/odata/index.ts"],"sourcesContent":["/** biome-ignore-all lint/suspicious/noExplicitAny: library code */\nimport { logger as betterAuthLogger } from \"better-auth\";\nimport { err, ok, type Result } from \"neverthrow\";\nimport type { z } from \"zod/v4\";\n\ninterface BasicAuthCredentials {\n username: string;\n password: string;\n}\ninterface OttoAPIKeyAuth {\n apiKey: string;\n}\ntype ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;\n\nexport interface 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\") ? input : `${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(\"raw-fetch\", `${requestInit.method || \"GET\"} ${url}`);\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(\"raw-fetch\", `Response status: ${response.status} ${response.statusText}`);\n betterAuthLogger.info(\"raw-fetch\", \"Response headers:\", Object.fromEntries(response.headers.entries()));\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(\"raw-fetch\", `HTTP Error ${response.status}: ${errorText}`);\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(\"raw-fetch\", `Response content-type: ${contentType || \"none\"}`);\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(\"raw-fetch\", `Raw response text: \"${responseText}\"`);\n betterAuthLogger.info(\"raw-fetch\", `Response text length: ${responseText.length}`);\n }\n\n // Handle empty responses\n if (responseText.trim() === \"\") {\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.info(\"raw-fetch\", \"Empty JSON response, returning null\");\n }\n responseData = null;\n } else {\n responseData = JSON.parse(responseText);\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.info(\"raw-fetch\", \"Successfully parsed JSON response\");\n }\n }\n } catch (parseError) {\n if (args.logging === \"verbose\" || args.logging === true) {\n betterAuthLogger.error(\"raw-fetch\", \"JSON parse error:\", parseError);\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(\"raw-fetch\", `Text response: \"${responseData}\"`);\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(\"raw-fetch\", `Unknown content-type response as text: \"${responseData}\"`);\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(\"raw-fetch\", \"Could not parse response as text, returning null\");\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 }\n return {\n error: `Validation failed: ${validation.error.message}`,\n response,\n };\n }\n\n // Return unvalidated data\n return {\n data: responseData as TOutput,\n response,\n };\n } catch (error) {\n return {\n error: 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":";;AAqBO,SAAS,YAAY,OAAqC;AAC/D,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,KAAK;AACzB,WAAO,GAAG,GAAG;AAAA,EACf,SAAS,OAAO;AACd,WAAO,IAAI,KAAK;AAAA,EAClB;AACF;AAEO,SAAS,eAAe,MAAqB;AAClD,QAAM,SAAS,YAAY,KAAK,SAAS;AAEzC,MAAI,OAAO,SAAS;AAClB,UAAM,IAAI,MAAM,oBAAoB;AAAA,EACtC;AAEA,MAAI,UAAU,OAAO,MAAM;AAC3B,MAAI,YAAY,KAAK,MAAM;AACzB,eAAW;AAAA,EACb;AACA,aAAW,iBAAiB,KAAK,QAAQ;AAGzC,QAAM,cAAsC,CAAA;AAC5C,MAAI,YAAY,KAAK,MAAM;AACzB,gBAAY,gBAAgB,UAAU,KAAK,KAAK,MAAM;AAAA,EACxD,OAAO;AACL,UAAM,cAAc,KAAK,GAAG,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,QAAQ,EAAE;AACtE,gBAAY,gBAAgB,SAAS,WAAW;AAAA,EAClD;AAGA,QAAM,eAAe,OACnB,OACA,YAIqE;AACrE,QAAI;AACF,UAAI;AAGJ,UAAI,OAAO,UAAU,UAAU;AAE7B,cAAM,MAAM,WAAW,MAAM,IAAI,QAAQ,GAAG,OAAO,GAAG,MAAM,WAAW,GAAG,IAAI,QAAQ,IAAI,KAAK,EAAE;AAAA,MACnG,WAAW,iBAAiB,KAAK;AAC/B,cAAM,MAAM,SAAA;AAAA,MACd,WAAW,iBAAiB,SAAS;AACnC,cAAM,MAAM;AAAA,MACd,OAAO;AACL,cAAM,OAAO,KAAK;AAAA,MACpB;AAGA,UAAI,gBAAgB,mCAAS;AAC7B,UACE,iBACA,OAAO,kBAAkB,YACzB,EAAE,yBAAyB,aAC3B,EAAE,yBAAyB,oBAC3B,EAAE,yBAAyB,iBAC3B;AACA,wBAAgB,KAAK,UAAU,aAAa;AAAA,MAC9C;AAGA,YAAM,UAAU;AAAA,QACd,gBAAgB;AAAA,QAChB,GAAG;AAAA,QACH,IAAI,mCAAS,YAAW,CAAA;AAAA,MAAC;AAG3B,YAAM,cAA2B;AAAA,QAC/B,GAAG;AAAA,QACH;AAAA,QACA,MAAM;AAAA,MAAA;AAIR,UAAI,KAAK,YAAY,aAAa,KAAK,YAAY,MAAM;AACvDA,eAAiB,KAAK,aAAa,GAAG,YAAY,UAAU,KAAK,IAAI,GAAG,EAAE;AAC1E,YAAI,YAAY,MAAM;AACpBA,iBAAiB,KAAK,aAAa,iBAAiB,YAAY,IAAI;AAAA,QACtE;AAAA,MACF;AAEA,YAAM,WAAW,MAAM,MAAM,KAAK,WAAW;AAG7C,UAAI,KAAK,YAAY,aAAa,KAAK,YAAY,MAAM;AACvDA,eAAiB,KAAK,aAAa,oBAAoB,SAAS,MAAM,IAAI,SAAS,UAAU,EAAE;AAC/FA,eAAiB,KAAK,aAAa,qBAAqB,OAAO,YAAY,SAAS,QAAQ,QAAA,CAAS,CAAC;AAAA,MACxG;AAGA,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,YAAY,MAAM,SAAS,OAAO,MAAM,MAAM,eAAe;AACnE,YAAI,KAAK,YAAY,aAAa,KAAK,YAAY,MAAM;AACvDA,iBAAiB,MAAM,aAAa,cAAc,SAAS,MAAM,KAAK,SAAS,EAAE;AAAA,QACnF;AACA,eAAO;AAAA,UACL,OAAO,QAAQ,SAAS,MAAM,KAAK,SAAS;AAAA,UAC5C;AAAA,QAAA;AAAA,MAEJ;AAGA,UAAI;AACJ,YAAM,cAAc,SAAS,QAAQ,IAAI,cAAc;AAEvD,UAAI,KAAK,YAAY,aAAa,KAAK,YAAY,MAAM;AACvDA,eAAiB,KAAK,aAAa,0BAA0B,eAAe,MAAM,EAAE;AAAA,MACtF;AAEA,UAAI,2CAAa,SAAS,qBAAqB;AAC7C,YAAI;AACF,gBAAM,eAAe,MAAM,SAAS,KAAA;AACpC,cAAI,KAAK,YAAY,aAAa,KAAK,YAAY,MAAM;AACvDA,mBAAiB,KAAK,aAAa,uBAAuB,YAAY,GAAG;AACzEA,mBAAiB,KAAK,aAAa,yBAAyB,aAAa,MAAM,EAAE;AAAA,UACnF;AAGA,cAAI,aAAa,KAAA,MAAW,IAAI;AAC9B,gBAAI,KAAK,YAAY,aAAa,KAAK,YAAY,MAAM;AACvDA,qBAAiB,KAAK,aAAa,qCAAqC;AAAA,YAC1E;AACA,2BAAe;AAAA,UACjB,OAAO;AACL,2BAAe,KAAK,MAAM,YAAY;AACtC,gBAAI,KAAK,YAAY,aAAa,KAAK,YAAY,MAAM;AACvDA,qBAAiB,KAAK,aAAa,mCAAmC;AAAA,YACxE;AAAA,UACF;AAAA,QACF,SAAS,YAAY;AACnB,cAAI,KAAK,YAAY,aAAa,KAAK,YAAY,MAAM;AACvDA,mBAAiB,MAAM,aAAa,qBAAqB,UAAU;AAAA,UACrE;AACA,iBAAO;AAAA,YACL,OAAO,kCAAkC,sBAAsB,QAAQ,WAAW,UAAU,qBAAqB;AAAA,YACjH;AAAA,UAAA;AAAA,QAEJ;AAAA,MACF,WAAW,2CAAa,SAAS,UAAU;AAEzC,uBAAe,MAAM,SAAS,KAAA;AAC9B,YAAI,KAAK,YAAY,aAAa,KAAK,YAAY,MAAM;AACvDA,iBAAiB,KAAK,aAAa,mBAAmB,YAAY,GAAG;AAAA,QACvE;AAAA,MACF,OAAO;AAEL,YAAI;AACF,yBAAe,MAAM,SAAS,KAAA;AAC9B,cAAI,KAAK,YAAY,aAAa,KAAK,YAAY,MAAM;AACvDA,mBAAiB,KAAK,aAAa,2CAA2C,YAAY,GAAG;AAAA,UAC/F;AAAA,QACF,QAAQ;AAEN,yBAAe;AACf,cAAI,KAAK,YAAY,aAAa,KAAK,YAAY,MAAM;AACvDA,mBAAiB,KAAK,aAAa,kDAAkD;AAAA,UACvF;AAAA,QACF;AAAA,MACF;AAGA,UAAI,mCAAS,QAAQ;AACnB,cAAM,aAAa,QAAQ,OAAO,UAAU,YAAY;AACxD,YAAI,WAAW,SAAS;AACtB,iBAAO;AAAA,YACL,MAAM,WAAW;AAAA,YACjB;AAAA,UAAA;AAAA,QAEJ;AACA,eAAO;AAAA,UACL,OAAO,sBAAsB,WAAW,MAAM,OAAO;AAAA,UACrD;AAAA,QAAA;AAAA,MAEJ;AAGA,aAAO;AAAA,QACL,MAAM;AAAA,QACN;AAAA,MAAA;AAAA,IAEJ,SAAS,OAAO;AACd,aAAO;AAAA,QACL,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAAA;AAAA,IAEpD;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,OAAO;AAAA,EAAA;AAEX;"}
|