@proofkit/better-auth 0.2.4 → 0.3.1-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.d.ts +3 -3
- package/dist/esm/adapter.js +141 -76
- package/dist/esm/adapter.js.map +1 -1
- package/dist/esm/better-auth-cli/utils/add-svelte-kit-env-modules.js +4 -12
- package/dist/esm/better-auth-cli/utils/add-svelte-kit-env-modules.js.map +1 -1
- package/dist/esm/better-auth-cli/utils/get-config.js +12 -21
- package/dist/esm/better-auth-cli/utils/get-config.js.map +1 -1
- package/dist/esm/better-auth-cli/utils/get-tsconfig-info.js +4 -11
- package/dist/esm/better-auth-cli/utils/get-tsconfig-info.js.map +1 -1
- package/dist/esm/cli/index.js +14 -26
- package/dist/esm/cli/index.js.map +1 -1
- package/dist/esm/migrate.d.ts +6 -6
- package/dist/esm/migrate.js +72 -44
- package/dist/esm/migrate.js.map +1 -1
- package/dist/esm/odata/index.d.ts +17 -91
- package/dist/esm/odata/index.js +141 -67
- package/dist/esm/odata/index.js.map +1 -1
- package/package.json +9 -7
- package/src/adapter.ts +171 -93
- package/src/better-auth-cli/utils/add-svelte-kit-env-modules.ts +68 -80
- package/src/better-auth-cli/utils/get-config.ts +16 -30
- package/src/better-auth-cli/utils/get-tsconfig-info.ts +12 -20
- package/src/cli/index.ts +13 -31
- package/src/index.ts +1 -0
- package/src/migrate.ts +87 -74
- package/src/odata/index.ts +195 -78
|
@@ -1,26 +1,18 @@
|
|
|
1
|
-
import path from "path";
|
|
1
|
+
import path from "node:path";
|
|
2
2
|
import fs from "fs-extra";
|
|
3
3
|
|
|
4
4
|
export function stripJsonComments(jsonString: string): string {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
)
|
|
9
|
-
.replace(/,(?=\s*[}\]])/g, "");
|
|
5
|
+
return jsonString
|
|
6
|
+
.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => (g ? "" : m))
|
|
7
|
+
.replace(/,(?=\s*[}\]])/g, "");
|
|
10
8
|
}
|
|
11
9
|
export function getTsconfigInfo(cwd?: string, flatPath?: string) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
const text = fs.readFileSync(tsConfigPath, "utf-8");
|
|
22
|
-
return JSON.parse(stripJsonComments(text));
|
|
23
|
-
} catch (error) {
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
10
|
+
let tsConfigPath: string;
|
|
11
|
+
if (flatPath) {
|
|
12
|
+
tsConfigPath = flatPath;
|
|
13
|
+
} else {
|
|
14
|
+
tsConfigPath = cwd ? path.join(cwd, "tsconfig.json") : path.join("tsconfig.json");
|
|
15
|
+
}
|
|
16
|
+
const text = fs.readFileSync(tsConfigPath, "utf-8");
|
|
17
|
+
return JSON.parse(stripJsonComments(text));
|
|
26
18
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node --no-warnings
|
|
2
2
|
import { Command } from "@commander-js/extra-typings";
|
|
3
|
-
import fs from "fs-extra";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
executeMigration,
|
|
7
|
-
planMigration,
|
|
8
|
-
prettyPrintMigrationPlan,
|
|
9
|
-
} from "../migrate";
|
|
10
|
-
import { getAdapter, getAuthTables } from "better-auth/db";
|
|
11
|
-
import { getConfig } from "../better-auth-cli/utils/get-config";
|
|
12
3
|
import { logger } from "better-auth";
|
|
13
|
-
import
|
|
4
|
+
import { getAdapter, getAuthTables } from "better-auth/db";
|
|
14
5
|
import chalk from "chalk";
|
|
15
|
-
import
|
|
16
|
-
import
|
|
6
|
+
import fs from "fs-extra";
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
import type { AdapterOptions } from "../adapter";
|
|
9
|
+
import { getConfig } from "../better-auth-cli/utils/get-config";
|
|
10
|
+
import { executeMigration, planMigration, prettyPrintMigrationPlan } from "../migrate";
|
|
11
|
+
import { createRawFetch } from "../odata";
|
|
17
12
|
import "dotenv/config";
|
|
18
13
|
|
|
19
14
|
async function main() {
|
|
@@ -21,11 +16,7 @@ async function main() {
|
|
|
21
16
|
|
|
22
17
|
program
|
|
23
18
|
.command("migrate", { isDefault: true })
|
|
24
|
-
.option(
|
|
25
|
-
"--cwd <path>",
|
|
26
|
-
"Path to the current working directory",
|
|
27
|
-
process.cwd(),
|
|
28
|
-
)
|
|
19
|
+
.option("--cwd <path>", "Path to the current working directory", process.cwd())
|
|
29
20
|
.option("--config <path>", "Path to the config file")
|
|
30
21
|
.option("-u, --username <username>", "Full Access Username")
|
|
31
22
|
.option("-p, --password <password>", "Full Access Password")
|
|
@@ -55,16 +46,14 @@ async function main() {
|
|
|
55
46
|
});
|
|
56
47
|
|
|
57
48
|
if (adapter.id !== "filemaker") {
|
|
58
|
-
logger.error(
|
|
59
|
-
"This generator is only compatible with the FileMaker adapter.",
|
|
60
|
-
);
|
|
49
|
+
logger.error("This generator is only compatible with the FileMaker adapter.");
|
|
61
50
|
return;
|
|
62
51
|
}
|
|
63
52
|
|
|
64
53
|
const betterAuthSchema = getAuthTables(config);
|
|
65
54
|
|
|
66
55
|
const adapterConfig = (adapter.options as AdapterOptions).config;
|
|
67
|
-
const fetch =
|
|
56
|
+
const { fetch } = createRawFetch({
|
|
68
57
|
...adapterConfig.odata,
|
|
69
58
|
auth:
|
|
70
59
|
// If the username and password are provided in the CLI, use them to authenticate instead of what's in the config file.
|
|
@@ -74,13 +63,10 @@ async function main() {
|
|
|
74
63
|
password: options.password,
|
|
75
64
|
}
|
|
76
65
|
: adapterConfig.odata.auth,
|
|
66
|
+
logging: "verbose", // Enable logging for CLI operations
|
|
77
67
|
});
|
|
78
68
|
|
|
79
|
-
const migrationPlan = await planMigration(
|
|
80
|
-
fetch,
|
|
81
|
-
betterAuthSchema,
|
|
82
|
-
adapterConfig.odata.database,
|
|
83
|
-
);
|
|
69
|
+
const migrationPlan = await planMigration(fetch, betterAuthSchema, adapterConfig.odata.database);
|
|
84
70
|
|
|
85
71
|
if (migrationPlan.length === 0) {
|
|
86
72
|
logger.info("No changes to apply. Database is up to date.");
|
|
@@ -91,11 +77,7 @@ async function main() {
|
|
|
91
77
|
prettyPrintMigrationPlan(migrationPlan);
|
|
92
78
|
|
|
93
79
|
if (migrationPlan.length > 0) {
|
|
94
|
-
console.log(
|
|
95
|
-
chalk.gray(
|
|
96
|
-
"💡 Tip: You can use the --yes flag to skip this confirmation.",
|
|
97
|
-
),
|
|
98
|
-
);
|
|
80
|
+
console.log(chalk.gray("💡 Tip: You can use the --yes flag to skip this confirmation."));
|
|
99
81
|
}
|
|
100
82
|
|
|
101
83
|
const { confirm } = await prompts({
|
package/src/index.ts
CHANGED
package/src/migrate.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type Metadata } from "fm-odata-client";
|
|
1
|
+
import type { BetterAuthDbSchema } from "better-auth/db";
|
|
3
2
|
import chalk from "chalk";
|
|
3
|
+
import type { Metadata } from "fm-odata-client";
|
|
4
4
|
import z from "zod/v4";
|
|
5
|
-
import {
|
|
5
|
+
import type { createRawFetch } from "./odata";
|
|
6
6
|
|
|
7
|
-
export async function getMetadata(
|
|
8
|
-
fetch: ReturnType<typeof createFmOdataFetch>,
|
|
9
|
-
databaseName: string,
|
|
10
|
-
) {
|
|
7
|
+
export async function getMetadata(fetch: ReturnType<typeof createRawFetch>["fetch"], databaseName: string) {
|
|
11
8
|
console.log("getting metadata...");
|
|
12
9
|
const result = await fetch("/$metadata", {
|
|
13
10
|
method: "GET",
|
|
@@ -21,18 +18,23 @@ export async function getMetadata(
|
|
|
21
18
|
.catch(null),
|
|
22
19
|
});
|
|
23
20
|
|
|
21
|
+
if (result.error) {
|
|
22
|
+
console.error("Failed to get metadata:", result.error);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
24
26
|
return (result.data?.[databaseName] ?? null) as Metadata | null;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export async function planMigration(
|
|
28
|
-
fetch: ReturnType<typeof
|
|
30
|
+
fetch: ReturnType<typeof createRawFetch>["fetch"],
|
|
29
31
|
betterAuthSchema: BetterAuthDbSchema,
|
|
30
32
|
databaseName: string,
|
|
31
33
|
): Promise<MigrationPlan> {
|
|
32
34
|
const metadata = await getMetadata(fetch, databaseName);
|
|
33
35
|
|
|
34
36
|
// Build a map from entity set name to entity type key
|
|
35
|
-
|
|
37
|
+
const entitySetToType: Record<string, string> = {};
|
|
36
38
|
if (metadata) {
|
|
37
39
|
for (const [key, value] of Object.entries(metadata)) {
|
|
38
40
|
if (value.$Kind === "EntitySet" && value.$Type) {
|
|
@@ -47,27 +49,32 @@ export async function planMigration(
|
|
|
47
49
|
? Object.entries(entitySetToType).reduce(
|
|
48
50
|
(acc, [entitySetName, entityTypeKey]) => {
|
|
49
51
|
const entityType = metadata[entityTypeKey];
|
|
50
|
-
if (!entityType)
|
|
52
|
+
if (!entityType) {
|
|
53
|
+
return acc;
|
|
54
|
+
}
|
|
51
55
|
const fields = Object.entries(entityType)
|
|
52
56
|
.filter(
|
|
53
|
-
([
|
|
54
|
-
typeof fieldValue === "object" &&
|
|
55
|
-
fieldValue !== null &&
|
|
56
|
-
"$Type" in fieldValue,
|
|
57
|
+
([_fieldKey, fieldValue]) =>
|
|
58
|
+
typeof fieldValue === "object" && fieldValue !== null && "$Type" in fieldValue,
|
|
57
59
|
)
|
|
58
|
-
.map(([fieldKey, fieldValue]) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
60
|
+
.map(([fieldKey, fieldValue]) => {
|
|
61
|
+
let type = "varchar";
|
|
62
|
+
if (fieldValue.$Type === "Edm.String") {
|
|
63
|
+
type = "varchar";
|
|
64
|
+
} else if (fieldValue.$Type === "Edm.DateTimeOffset") {
|
|
65
|
+
type = "timestamp";
|
|
66
|
+
} else if (
|
|
67
|
+
fieldValue.$Type === "Edm.Decimal" ||
|
|
68
|
+
fieldValue.$Type === "Edm.Int32" ||
|
|
69
|
+
fieldValue.$Type === "Edm.Int64"
|
|
70
|
+
) {
|
|
71
|
+
type = "numeric";
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
name: fieldKey,
|
|
75
|
+
type,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
71
78
|
acc[entitySetName] = fields;
|
|
72
79
|
return acc;
|
|
73
80
|
},
|
|
@@ -85,42 +92,24 @@ export async function planMigration(
|
|
|
85
92
|
const migrationPlan: MigrationPlan = [];
|
|
86
93
|
|
|
87
94
|
for (const baTable of baTables) {
|
|
88
|
-
const fields: FmField[] = Object.entries(baTable.fields).map(
|
|
89
|
-
|
|
95
|
+
const fields: FmField[] = Object.entries(baTable.fields).map(([key, field]) => {
|
|
96
|
+
let type: "varchar" | "numeric" | "timestamp" = "varchar";
|
|
97
|
+
if (field.type === "boolean" || field.type.includes("number")) {
|
|
98
|
+
type = "numeric";
|
|
99
|
+
} else if (field.type === "date") {
|
|
100
|
+
type = "timestamp";
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
90
103
|
name: field.fieldName ?? key,
|
|
91
|
-
type
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
: field.type === "date"
|
|
95
|
-
? "timestamp"
|
|
96
|
-
: "varchar",
|
|
97
|
-
}),
|
|
98
|
-
);
|
|
104
|
+
type,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
99
107
|
|
|
100
108
|
// get existing table or create it
|
|
101
|
-
const tableExists =
|
|
102
|
-
existingTables,
|
|
103
|
-
baTable.modelName,
|
|
104
|
-
);
|
|
109
|
+
const tableExists = baTable.modelName in existingTables;
|
|
105
110
|
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
tableName: baTable.modelName,
|
|
109
|
-
operation: "create",
|
|
110
|
-
fields: [
|
|
111
|
-
{
|
|
112
|
-
name: "id",
|
|
113
|
-
type: "varchar",
|
|
114
|
-
primary: true,
|
|
115
|
-
unique: true,
|
|
116
|
-
},
|
|
117
|
-
...fields,
|
|
118
|
-
],
|
|
119
|
-
});
|
|
120
|
-
} else {
|
|
121
|
-
const existingFields = (existingTables[baTable.modelName] || []).map(
|
|
122
|
-
(f) => f.name,
|
|
123
|
-
);
|
|
111
|
+
if (tableExists) {
|
|
112
|
+
const existingFields = (existingTables[baTable.modelName] || []).map((f) => f.name);
|
|
124
113
|
const existingFieldMap = (existingTables[baTable.modelName] || []).reduce(
|
|
125
114
|
(acc, f) => {
|
|
126
115
|
acc[f.name] = f.type;
|
|
@@ -129,19 +118,14 @@ export async function planMigration(
|
|
|
129
118
|
{} as Record<string, string>,
|
|
130
119
|
);
|
|
131
120
|
// Warn about type mismatches (optional, not in plan)
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
existingFields.includes(field.name) &&
|
|
135
|
-
existingFieldMap[field.name] !== field.type
|
|
136
|
-
) {
|
|
121
|
+
for (const field of fields) {
|
|
122
|
+
if (existingFields.includes(field.name) && existingFieldMap[field.name] !== field.type) {
|
|
137
123
|
console.warn(
|
|
138
124
|
`⚠️ 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.`,
|
|
139
125
|
);
|
|
140
126
|
}
|
|
141
|
-
}
|
|
142
|
-
const fieldsToAdd = fields.filter(
|
|
143
|
-
(f) => !existingFields.includes(f.name),
|
|
144
|
-
);
|
|
127
|
+
}
|
|
128
|
+
const fieldsToAdd = fields.filter((f) => !existingFields.includes(f.name));
|
|
145
129
|
if (fieldsToAdd.length > 0) {
|
|
146
130
|
migrationPlan.push({
|
|
147
131
|
tableName: baTable.modelName,
|
|
@@ -149,6 +133,20 @@ export async function planMigration(
|
|
|
149
133
|
fields: fieldsToAdd,
|
|
150
134
|
});
|
|
151
135
|
}
|
|
136
|
+
} else {
|
|
137
|
+
migrationPlan.push({
|
|
138
|
+
tableName: baTable.modelName,
|
|
139
|
+
operation: "create",
|
|
140
|
+
fields: [
|
|
141
|
+
{
|
|
142
|
+
name: "id",
|
|
143
|
+
type: "varchar",
|
|
144
|
+
primary: true,
|
|
145
|
+
unique: true,
|
|
146
|
+
},
|
|
147
|
+
...fields,
|
|
148
|
+
],
|
|
149
|
+
});
|
|
152
150
|
}
|
|
153
151
|
}
|
|
154
152
|
|
|
@@ -156,24 +154,35 @@ export async function planMigration(
|
|
|
156
154
|
}
|
|
157
155
|
|
|
158
156
|
export async function executeMigration(
|
|
159
|
-
fetch: ReturnType<typeof
|
|
157
|
+
fetch: ReturnType<typeof createRawFetch>["fetch"],
|
|
160
158
|
migrationPlan: MigrationPlan,
|
|
161
159
|
) {
|
|
162
160
|
for (const step of migrationPlan) {
|
|
163
161
|
if (step.operation === "create") {
|
|
164
162
|
console.log("Creating table:", step.tableName);
|
|
165
|
-
await fetch("
|
|
163
|
+
const result = await fetch("/FileMaker_Tables", {
|
|
164
|
+
method: "POST",
|
|
166
165
|
body: {
|
|
167
166
|
tableName: step.tableName,
|
|
168
167
|
fields: step.fields,
|
|
169
168
|
},
|
|
170
169
|
});
|
|
170
|
+
|
|
171
|
+
if (result.error) {
|
|
172
|
+
console.error(`Failed to create table ${step.tableName}:`, result.error);
|
|
173
|
+
throw new Error(`Migration failed: ${result.error}`);
|
|
174
|
+
}
|
|
171
175
|
} else if (step.operation === "update") {
|
|
172
176
|
console.log("Adding fields to table:", step.tableName);
|
|
173
|
-
await fetch(
|
|
174
|
-
|
|
177
|
+
const result = await fetch(`/FileMaker_Tables/${step.tableName}`, {
|
|
178
|
+
method: "PATCH",
|
|
175
179
|
body: { fields: step.fields },
|
|
176
180
|
});
|
|
181
|
+
|
|
182
|
+
if (result.error) {
|
|
183
|
+
console.error(`Failed to update table ${step.tableName}:`, result.error);
|
|
184
|
+
throw new Error(`Migration failed: ${result.error}`);
|
|
185
|
+
}
|
|
177
186
|
}
|
|
178
187
|
}
|
|
179
188
|
}
|
|
@@ -252,8 +261,12 @@ export function prettyPrintMigrationPlan(migrationPlan: MigrationPlan) {
|
|
|
252
261
|
if (step.fields.length) {
|
|
253
262
|
for (const field of step.fields) {
|
|
254
263
|
let fieldDesc = ` - ${field.name} (${field.type}`;
|
|
255
|
-
if (field.primary)
|
|
256
|
-
|
|
264
|
+
if (field.primary) {
|
|
265
|
+
fieldDesc += ", primary";
|
|
266
|
+
}
|
|
267
|
+
if (field.unique) {
|
|
268
|
+
fieldDesc += ", unique";
|
|
269
|
+
}
|
|
257
270
|
fieldDesc += ")";
|
|
258
271
|
console.log(fieldDesc);
|
|
259
272
|
}
|
package/src/odata/index.ts
CHANGED
|
@@ -1,102 +1,219 @@
|
|
|
1
|
-
|
|
2
|
-
import { logger } from "@better-fetch/logger";
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: library code */
|
|
3
2
|
import { logger as betterAuthLogger } from "better-auth";
|
|
4
|
-
import { err, ok, Result } from "neverthrow";
|
|
5
|
-
import { z } from "zod/v4";
|
|
3
|
+
import { err, ok, type Result } from "neverthrow";
|
|
4
|
+
import type { z } from "zod/v4";
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
interface BasicAuthCredentials {
|
|
8
7
|
username: string;
|
|
9
8
|
password: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
9
|
+
}
|
|
10
|
+
interface OttoAPIKeyAuth {
|
|
12
11
|
apiKey: string;
|
|
13
|
-
}
|
|
12
|
+
}
|
|
14
13
|
type ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;
|
|
15
14
|
|
|
16
|
-
export
|
|
15
|
+
export interface FmOdataConfig {
|
|
17
16
|
serverUrl: string;
|
|
18
17
|
auth: ODataAuth;
|
|
19
18
|
database: string;
|
|
20
19
|
logging?: true | "verbose" | "none";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"@patch/FileMaker_Tables/:tableName": {
|
|
34
|
-
params: z.object({ tableName: z.string() }),
|
|
35
|
-
input: z.object({ fields: z.array(z.any()) }),
|
|
36
|
-
},
|
|
37
|
-
/**
|
|
38
|
-
* Delete a table
|
|
39
|
-
*/
|
|
40
|
-
"@delete/FileMaker_Tables/:tableName": {
|
|
41
|
-
params: z.object({ tableName: z.string() }),
|
|
42
|
-
},
|
|
43
|
-
/**
|
|
44
|
-
* Delete a field from a table
|
|
45
|
-
*/
|
|
46
|
-
"@delete/FileMaker_Tables/:tableName/:fieldName": {
|
|
47
|
-
params: z.object({ tableName: z.string(), fieldName: z.string() }),
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
export function createFmOdataFetch(args: FmOdataConfig) {
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validateUrl(input: string): Result<URL, unknown> {
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(input);
|
|
25
|
+
return ok(url);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return err(error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createRawFetch(args: FmOdataConfig) {
|
|
52
32
|
const result = validateUrl(args.serverUrl);
|
|
53
33
|
|
|
54
34
|
if (result.isErr()) {
|
|
55
35
|
throw new Error("Invalid server URL");
|
|
56
36
|
}
|
|
37
|
+
|
|
57
38
|
let baseURL = result.value.origin;
|
|
58
39
|
if ("apiKey" in args.auth) {
|
|
59
|
-
baseURL +=
|
|
40
|
+
baseURL += "/otto";
|
|
60
41
|
}
|
|
61
42
|
baseURL += `/fmi/odata/v4/${args.database}`;
|
|
62
43
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
44
|
+
// Create authentication headers
|
|
45
|
+
const authHeaders: Record<string, string> = {};
|
|
46
|
+
if ("apiKey" in args.auth) {
|
|
47
|
+
authHeaders.Authorization = `Bearer ${args.auth.apiKey}`;
|
|
48
|
+
} else {
|
|
49
|
+
const credentials = btoa(`${args.auth.username}:${args.auth.password}`);
|
|
50
|
+
authHeaders.Authorization = `Basic ${credentials}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Enhanced fetch function with body handling, validation, and structured responses
|
|
54
|
+
const wrappedFetch = async <TOutput = any>(
|
|
55
|
+
input: string | URL | Request,
|
|
56
|
+
options?: Omit<RequestInit, "body"> & {
|
|
57
|
+
body?: any; // Allow any type for body
|
|
58
|
+
output?: z.ZodSchema<TOutput>; // Optional schema for validation
|
|
77
59
|
},
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
verbose: args.logging === "verbose",
|
|
82
|
-
enabled: args.logging === "verbose" || !!args.logging,
|
|
83
|
-
console: {
|
|
84
|
-
fail: (...args) => betterAuthLogger.error("better-fetch", ...args),
|
|
85
|
-
success: (...args) => betterAuthLogger.info("better-fetch", ...args),
|
|
86
|
-
log: (...args) => betterAuthLogger.info("better-fetch", ...args),
|
|
87
|
-
error: (...args) => betterAuthLogger.error("better-fetch", ...args),
|
|
88
|
-
warn: (...args) => betterAuthLogger.warn("better-fetch", ...args),
|
|
89
|
-
},
|
|
90
|
-
}),
|
|
91
|
-
],
|
|
92
|
-
});
|
|
93
|
-
}
|
|
60
|
+
): Promise<{ data?: TOutput; error?: string; response?: Response }> => {
|
|
61
|
+
try {
|
|
62
|
+
let url: string;
|
|
94
63
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
64
|
+
// Handle different input types
|
|
65
|
+
if (typeof input === "string") {
|
|
66
|
+
// If it's already a full URL, use as-is, otherwise prepend baseURL
|
|
67
|
+
url = input.startsWith("http") ? input : `${baseURL}${input.startsWith("/") ? input : `/${input}`}`;
|
|
68
|
+
} else if (input instanceof URL) {
|
|
69
|
+
url = input.toString();
|
|
70
|
+
} else if (input instanceof Request) {
|
|
71
|
+
url = input.url;
|
|
72
|
+
} else {
|
|
73
|
+
url = String(input);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle body serialization
|
|
77
|
+
let processedBody = options?.body;
|
|
78
|
+
if (
|
|
79
|
+
processedBody &&
|
|
80
|
+
typeof processedBody === "object" &&
|
|
81
|
+
!(processedBody instanceof FormData) &&
|
|
82
|
+
!(processedBody instanceof URLSearchParams) &&
|
|
83
|
+
!(processedBody instanceof ReadableStream)
|
|
84
|
+
) {
|
|
85
|
+
processedBody = JSON.stringify(processedBody);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Merge headers
|
|
89
|
+
const headers = {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
...authHeaders,
|
|
92
|
+
...(options?.headers || {}),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const requestInit: RequestInit = {
|
|
96
|
+
...options,
|
|
97
|
+
headers,
|
|
98
|
+
body: processedBody,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Optional logging
|
|
102
|
+
if (args.logging === "verbose" || args.logging === true) {
|
|
103
|
+
betterAuthLogger.info("raw-fetch", `${requestInit.method || "GET"} ${url}`);
|
|
104
|
+
if (requestInit.body) {
|
|
105
|
+
betterAuthLogger.info("raw-fetch", "Request body:", requestInit.body);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const response = await fetch(url, requestInit);
|
|
110
|
+
|
|
111
|
+
// Optional logging for response details
|
|
112
|
+
if (args.logging === "verbose" || args.logging === true) {
|
|
113
|
+
betterAuthLogger.info("raw-fetch", `Response status: ${response.status} ${response.statusText}`);
|
|
114
|
+
betterAuthLogger.info("raw-fetch", "Response headers:", Object.fromEntries(response.headers.entries()));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if response is ok
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
120
|
+
if (args.logging === "verbose" || args.logging === true) {
|
|
121
|
+
betterAuthLogger.error("raw-fetch", `HTTP Error ${response.status}: ${errorText}`);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
error: `HTTP ${response.status}: ${errorText}`,
|
|
125
|
+
response,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Parse response based on content type
|
|
130
|
+
let responseData: any;
|
|
131
|
+
const contentType = response.headers.get("content-type");
|
|
132
|
+
|
|
133
|
+
if (args.logging === "verbose" || args.logging === true) {
|
|
134
|
+
betterAuthLogger.info("raw-fetch", `Response content-type: ${contentType || "none"}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (contentType?.includes("application/json")) {
|
|
138
|
+
try {
|
|
139
|
+
const responseText = await response.text();
|
|
140
|
+
if (args.logging === "verbose" || args.logging === true) {
|
|
141
|
+
betterAuthLogger.info("raw-fetch", `Raw response text: "${responseText}"`);
|
|
142
|
+
betterAuthLogger.info("raw-fetch", `Response text length: ${responseText.length}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle empty responses
|
|
146
|
+
if (responseText.trim() === "") {
|
|
147
|
+
if (args.logging === "verbose" || args.logging === true) {
|
|
148
|
+
betterAuthLogger.info("raw-fetch", "Empty JSON response, returning null");
|
|
149
|
+
}
|
|
150
|
+
responseData = null;
|
|
151
|
+
} else {
|
|
152
|
+
responseData = JSON.parse(responseText);
|
|
153
|
+
if (args.logging === "verbose" || args.logging === true) {
|
|
154
|
+
betterAuthLogger.info("raw-fetch", "Successfully parsed JSON response");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (parseError) {
|
|
158
|
+
if (args.logging === "verbose" || args.logging === true) {
|
|
159
|
+
betterAuthLogger.error("raw-fetch", "JSON parse error:", parseError);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
error: `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : "Unknown parse error"}`,
|
|
163
|
+
response,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
} else if (contentType?.includes("text/")) {
|
|
167
|
+
// Handle text responses (text/plain, text/html, etc.)
|
|
168
|
+
responseData = await response.text();
|
|
169
|
+
if (args.logging === "verbose" || args.logging === true) {
|
|
170
|
+
betterAuthLogger.info("raw-fetch", `Text response: "${responseData}"`);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// For other content types, try to get text but don't fail if it's binary
|
|
174
|
+
try {
|
|
175
|
+
responseData = await response.text();
|
|
176
|
+
if (args.logging === "verbose" || args.logging === true) {
|
|
177
|
+
betterAuthLogger.info("raw-fetch", `Unknown content-type response as text: "${responseData}"`);
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// If text parsing fails (e.g., binary data), return null
|
|
181
|
+
responseData = null;
|
|
182
|
+
if (args.logging === "verbose" || args.logging === true) {
|
|
183
|
+
betterAuthLogger.info("raw-fetch", "Could not parse response as text, returning null");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Validate output if schema provided
|
|
189
|
+
if (options?.output) {
|
|
190
|
+
const validation = options.output.safeParse(responseData);
|
|
191
|
+
if (validation.success) {
|
|
192
|
+
return {
|
|
193
|
+
data: validation.data,
|
|
194
|
+
response,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
error: `Validation failed: ${validation.error.message}`,
|
|
199
|
+
response,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Return unvalidated data
|
|
204
|
+
return {
|
|
205
|
+
data: responseData as TOutput,
|
|
206
|
+
response,
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return {
|
|
210
|
+
error: error instanceof Error ? error.message : "Unknown error occurred",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
baseURL,
|
|
217
|
+
fetch: wrappedFetch,
|
|
218
|
+
};
|
|
102
219
|
}
|