@proofkit/better-auth 0.3.1-beta.0 → 0.4.0-beta.10
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/bin/intent.js +20 -0
- package/dist/esm/adapter.d.ts +6 -10
- package/dist/esm/adapter.js +58 -93
- package/dist/esm/adapter.js.map +1 -1
- 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.map +1 -1
- package/dist/esm/better-auth-cli/utils/get-tsconfig-info.js.map +1 -1
- package/dist/esm/cli/index.js +66 -23
- package/dist/esm/cli/index.js.map +1 -1
- package/dist/esm/migrate.d.ts +26 -84
- package/dist/esm/migrate.js +92 -100
- package/dist/esm/migrate.js.map +1 -1
- package/package.json +24 -19
- package/skills/better-auth-setup/SKILL.md +233 -0
- package/src/adapter.ts +84 -107
- package/src/cli/index.ts +88 -28
- package/src/migrate.ts +143 -157
- 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/cli/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node --no-warnings
|
|
2
2
|
import { Command } from "@commander-js/extra-typings";
|
|
3
|
+
import type { Database, FFetchOptions } from "@proofkit/fmodata";
|
|
4
|
+
import { FMServerConnection } from "@proofkit/fmodata";
|
|
3
5
|
import { logger } from "better-auth";
|
|
4
|
-
import {
|
|
6
|
+
import { getSchema } from "better-auth/db";
|
|
5
7
|
import chalk from "chalk";
|
|
6
8
|
import fs from "fs-extra";
|
|
7
9
|
import prompts from "prompts";
|
|
8
|
-
import type { AdapterOptions } from "../adapter";
|
|
9
10
|
import { getConfig } from "../better-auth-cli/utils/get-config";
|
|
10
11
|
import { executeMigration, planMigration, prettyPrintMigrationPlan } from "../migrate";
|
|
11
|
-
import { createRawFetch } from "../odata";
|
|
12
12
|
import "dotenv/config";
|
|
13
13
|
|
|
14
14
|
async function main() {
|
|
@@ -40,33 +40,87 @@ async function main() {
|
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Resolve adapter directly (getAdapter removed in Better Auth 1.5)
|
|
44
|
+
const databaseFactory = config.database;
|
|
45
|
+
if (!databaseFactory || typeof databaseFactory !== "function") {
|
|
46
|
+
logger.error("No database adapter found in auth config.");
|
|
45
47
|
process.exit(1);
|
|
46
|
-
}
|
|
48
|
+
}
|
|
49
|
+
let adapter: { id?: string; database?: unknown };
|
|
50
|
+
try {
|
|
51
|
+
adapter = (databaseFactory as (opts: unknown) => { id?: string; database?: unknown })(config);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
logger.error(e instanceof Error ? e.message : String(e));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
47
56
|
|
|
48
|
-
if (adapter
|
|
57
|
+
if (adapter?.id !== "filemaker") {
|
|
49
58
|
logger.error("This generator is only compatible with the FileMaker adapter.");
|
|
50
59
|
return;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
const betterAuthSchema =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
62
|
+
const betterAuthSchema = getSchema(config);
|
|
63
|
+
|
|
64
|
+
// Extract Database from the adapter factory or resolved adapter.
|
|
65
|
+
// config.database is the FileMakerAdapter factory function (has .database set on it).
|
|
66
|
+
// adapter is the resolved adapter after getAdapter() calls the factory (also has .database).
|
|
67
|
+
// Try both: adapter first (post-call), then config.database (pre-call / factory function).
|
|
68
|
+
const configDb =
|
|
69
|
+
(adapter as unknown as { database?: Database }).database ??
|
|
70
|
+
(config.database as unknown as { database?: Database } | undefined)?.database;
|
|
71
|
+
if (!configDb || typeof configDb !== "object" || !("schema" in configDb)) {
|
|
72
|
+
logger.error(
|
|
73
|
+
"Could not extract Database instance from adapter. Ensure your auth.ts uses FileMakerAdapter with an fmodata Database.",
|
|
74
|
+
);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
let db: Database = configDb;
|
|
78
|
+
|
|
79
|
+
// Extract database name and server URL for display.
|
|
80
|
+
// Try the public getter first (_getDatabaseName), fall back to the private field (databaseName).
|
|
81
|
+
const dbObj = configDb as unknown as {
|
|
82
|
+
_getDatabaseName?: string;
|
|
83
|
+
databaseName?: string;
|
|
84
|
+
context?: { _getBaseUrl?: () => string; _fetchClientOptions?: unknown };
|
|
85
|
+
};
|
|
86
|
+
const dbName: string = dbObj._getDatabaseName ?? dbObj.databaseName ?? "";
|
|
87
|
+
const baseUrl: string | undefined = dbObj.context?._getBaseUrl?.();
|
|
88
|
+
const serverUrl = baseUrl ? new URL(baseUrl).origin : undefined;
|
|
89
|
+
|
|
90
|
+
// If CLI credential overrides are provided, construct a new connection
|
|
91
|
+
if (options.username && options.password) {
|
|
92
|
+
if (!dbName) {
|
|
93
|
+
logger.error("Could not determine database filename from adapter config.");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
68
96
|
|
|
69
|
-
|
|
97
|
+
if (!baseUrl) {
|
|
98
|
+
logger.error(
|
|
99
|
+
"Could not determine server URL from adapter config. Ensure your auth.ts uses FMServerConnection.",
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fetchClientOptions = dbObj.context?._fetchClientOptions as FFetchOptions | undefined;
|
|
105
|
+
const connection = new FMServerConnection({
|
|
106
|
+
serverUrl: serverUrl as string,
|
|
107
|
+
auth: {
|
|
108
|
+
username: options.username,
|
|
109
|
+
password: options.password,
|
|
110
|
+
},
|
|
111
|
+
fetchClientOptions,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
db = connection.database(dbName);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let migrationPlan: Awaited<ReturnType<typeof planMigration>>;
|
|
118
|
+
try {
|
|
119
|
+
migrationPlan = await planMigration(db, betterAuthSchema);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
logger.error(`Failed to read database schema: ${err instanceof Error ? err.message : err}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
70
124
|
|
|
71
125
|
if (migrationPlan.length === 0) {
|
|
72
126
|
logger.info("No changes to apply. Database is up to date.");
|
|
@@ -74,7 +128,7 @@ async function main() {
|
|
|
74
128
|
}
|
|
75
129
|
|
|
76
130
|
if (!options.yes) {
|
|
77
|
-
prettyPrintMigrationPlan(migrationPlan);
|
|
131
|
+
prettyPrintMigrationPlan(migrationPlan, { serverUrl, fileName: dbName });
|
|
78
132
|
|
|
79
133
|
if (migrationPlan.length > 0) {
|
|
80
134
|
console.log(chalk.gray("💡 Tip: You can use the --yes flag to skip this confirmation."));
|
|
@@ -91,12 +145,18 @@ async function main() {
|
|
|
91
145
|
}
|
|
92
146
|
}
|
|
93
147
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
148
|
+
try {
|
|
149
|
+
await executeMigration(db, migrationPlan);
|
|
150
|
+
logger.info("Migration applied successfully.");
|
|
151
|
+
} catch {
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
97
154
|
});
|
|
98
155
|
await program.parseAsync(process.argv);
|
|
99
156
|
process.exit(0);
|
|
100
157
|
}
|
|
101
158
|
|
|
102
|
-
main().catch(
|
|
159
|
+
main().catch((err) => {
|
|
160
|
+
logger.error(err.message ?? err);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
package/src/migrate.ts
CHANGED
|
@@ -1,111 +1,103 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Database, Field, Metadata } from "@proofkit/fmodata";
|
|
2
|
+
import { isFMODataError, isODataError } from "@proofkit/fmodata";
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
const result = await fetch("/$metadata", {
|
|
10
|
-
method: "GET",
|
|
11
|
-
headers: { accept: "application/json" },
|
|
12
|
-
output: z
|
|
13
|
-
.looseObject({
|
|
14
|
-
$Version: z.string(),
|
|
15
|
-
"@ServerVersion": z.string(),
|
|
16
|
-
})
|
|
17
|
-
.or(z.null())
|
|
18
|
-
.catch(null),
|
|
19
|
-
});
|
|
6
|
+
/** Schema type returned by better-auth's getSchema function */
|
|
7
|
+
type BetterAuthSchema = Record<string, { fields: Record<string, DBFieldAttribute>; order: number }>;
|
|
20
8
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return
|
|
9
|
+
function normalizeBetterAuthFieldType(fieldType: unknown): string {
|
|
10
|
+
if (typeof fieldType === "string") {
|
|
11
|
+
return fieldType;
|
|
24
12
|
}
|
|
13
|
+
if (Array.isArray(fieldType)) {
|
|
14
|
+
return fieldType.map(String).join("|");
|
|
15
|
+
}
|
|
16
|
+
return String(fieldType);
|
|
17
|
+
}
|
|
25
18
|
|
|
26
|
-
|
|
19
|
+
export async function getMetadata(db: Database): Promise<Metadata> {
|
|
20
|
+
const metadata = await db.getMetadata({ format: "json" });
|
|
21
|
+
return metadata;
|
|
22
|
+
}
|
|
23
|
+
|
|
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";
|
|
28
|
+
}
|
|
29
|
+
if (t.includes("date")) {
|
|
30
|
+
return "timestamp";
|
|
31
|
+
}
|
|
32
|
+
return "string";
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
export async function planMigration(
|
|
30
|
-
|
|
31
|
-
betterAuthSchema: BetterAuthDbSchema,
|
|
32
|
-
databaseName: string,
|
|
33
|
-
): Promise<MigrationPlan> {
|
|
34
|
-
const metadata = await getMetadata(fetch, databaseName);
|
|
35
|
+
export async function planMigration(db: Database, betterAuthSchema: BetterAuthSchema): Promise<MigrationPlan> {
|
|
36
|
+
const metadata = await getMetadata(db);
|
|
35
37
|
|
|
36
38
|
// Build a map from entity set name to entity type key
|
|
37
39
|
const entitySetToType: Record<string, string> = {};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
entitySetToType[key] = typeKey || key;
|
|
44
|
-
}
|
|
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;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const existingTables =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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";
|
|
54
70
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
});
|
|
78
|
-
acc[entitySetName] = fields;
|
|
79
|
-
return acc;
|
|
80
|
-
},
|
|
81
|
-
{} as Record<string, { name: string; type: string }[]>,
|
|
82
|
-
)
|
|
83
|
-
: {};
|
|
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
|
+
);
|
|
84
81
|
|
|
85
82
|
const baTables = Object.entries(betterAuthSchema)
|
|
86
83
|
.sort((a, b) => (a[1].order ?? 0) - (b[1].order ?? 0))
|
|
87
84
|
.map(([key, value]) => ({
|
|
88
85
|
...value,
|
|
89
|
-
|
|
86
|
+
modelName: key,
|
|
90
87
|
}));
|
|
91
88
|
|
|
92
89
|
const migrationPlan: MigrationPlan = [];
|
|
93
90
|
|
|
94
91
|
for (const baTable of baTables) {
|
|
95
92
|
const fields: FmField[] = Object.entries(baTable.fields).map(([key, field]) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
type = "numeric";
|
|
99
|
-
} else if (field.type === "date") {
|
|
100
|
-
type = "timestamp";
|
|
101
|
-
}
|
|
93
|
+
const t = normalizeBetterAuthFieldType(field.type);
|
|
94
|
+
const type = mapFieldType(t);
|
|
102
95
|
return {
|
|
103
96
|
name: field.fieldName ?? key,
|
|
104
97
|
type,
|
|
105
98
|
};
|
|
106
99
|
});
|
|
107
100
|
|
|
108
|
-
// get existing table or create it
|
|
109
101
|
const tableExists = baTable.modelName in existingTables;
|
|
110
102
|
|
|
111
103
|
if (tableExists) {
|
|
@@ -117,7 +109,6 @@ export async function planMigration(
|
|
|
117
109
|
},
|
|
118
110
|
{} as Record<string, string>,
|
|
119
111
|
);
|
|
120
|
-
// Warn about type mismatches (optional, not in plan)
|
|
121
112
|
for (const field of fields) {
|
|
122
113
|
if (existingFields.includes(field.name) && existingFieldMap[field.name] !== field.type) {
|
|
123
114
|
console.warn(
|
|
@@ -140,7 +131,7 @@ export async function planMigration(
|
|
|
140
131
|
fields: [
|
|
141
132
|
{
|
|
142
133
|
name: "id",
|
|
143
|
-
type: "
|
|
134
|
+
type: "string",
|
|
144
135
|
primary: true,
|
|
145
136
|
unique: true,
|
|
146
137
|
},
|
|
@@ -153,106 +144,101 @@ export async function planMigration(
|
|
|
153
144
|
return migrationPlan;
|
|
154
145
|
}
|
|
155
146
|
|
|
156
|
-
export async function executeMigration(
|
|
157
|
-
fetch: ReturnType<typeof createRawFetch>["fetch"],
|
|
158
|
-
migrationPlan: MigrationPlan,
|
|
159
|
-
) {
|
|
147
|
+
export async function executeMigration(db: Database, migrationPlan: MigrationPlan) {
|
|
160
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
|
+
|
|
161
157
|
if (step.operation === "create") {
|
|
162
158
|
console.log("Creating table:", step.tableName);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
fields: step.fields,
|
|
168
|
-
},
|
|
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}`);
|
|
159
|
+
try {
|
|
160
|
+
await db.schema.createTable(step.tableName, fmodataFields);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw migrationError("create", step.tableName, error);
|
|
174
163
|
}
|
|
175
164
|
} else if (step.operation === "update") {
|
|
176
165
|
console.log("Adding fields to table:", step.tableName);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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}`);
|
|
166
|
+
try {
|
|
167
|
+
await db.schema.addFields(step.tableName, fmodataFields);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw migrationError("update", step.tableName, error);
|
|
185
170
|
}
|
|
186
171
|
}
|
|
187
172
|
}
|
|
188
173
|
}
|
|
189
174
|
|
|
190
|
-
|
|
191
|
-
name:
|
|
192
|
-
|
|
193
|
-
primary
|
|
194
|
-
unique
|
|
195
|
-
|
|
196
|
-
repetitions: z.number().optional(),
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const stringFieldSchema = genericFieldSchema.extend({
|
|
200
|
-
type: z.literal("varchar"),
|
|
201
|
-
maxLength: z.number().optional(),
|
|
202
|
-
default: z.enum(["USER", "USERNAME", "CURRENT_USER"]).optional(),
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const numericFieldSchema = genericFieldSchema.extend({
|
|
206
|
-
type: z.literal("numeric"),
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const dateFieldSchema = genericFieldSchema.extend({
|
|
210
|
-
type: z.literal("date"),
|
|
211
|
-
default: z.enum(["CURRENT_DATE", "CURDATE"]).optional(),
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const timeFieldSchema = genericFieldSchema.extend({
|
|
215
|
-
type: z.literal("time"),
|
|
216
|
-
default: z.enum(["CURRENT_TIME", "CURTIME"]).optional(),
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const timestampFieldSchema = genericFieldSchema.extend({
|
|
220
|
-
type: z.literal("timestamp"),
|
|
221
|
-
default: z.enum(["CURRENT_TIMESTAMP", "CURTIMESTAMP"]).optional(),
|
|
222
|
-
});
|
|
175
|
+
interface FmField {
|
|
176
|
+
name: string;
|
|
177
|
+
type: "string" | "numeric" | "timestamp";
|
|
178
|
+
primary?: boolean;
|
|
179
|
+
unique?: boolean;
|
|
180
|
+
}
|
|
223
181
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
182
|
+
const migrationStepTypes = ["create", "update"] as const;
|
|
183
|
+
interface MigrationStep {
|
|
184
|
+
tableName: string;
|
|
185
|
+
operation: (typeof migrationStepTypes)[number];
|
|
186
|
+
fields: FmField[];
|
|
187
|
+
}
|
|
228
188
|
|
|
229
|
-
|
|
230
|
-
stringFieldSchema,
|
|
231
|
-
numericFieldSchema,
|
|
232
|
-
dateFieldSchema,
|
|
233
|
-
timeFieldSchema,
|
|
234
|
-
timestampFieldSchema,
|
|
235
|
-
containerFieldSchema,
|
|
236
|
-
]);
|
|
189
|
+
export type MigrationPlan = MigrationStep[];
|
|
237
190
|
|
|
238
|
-
|
|
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
|
+
}
|
|
239
204
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
operation: z.enum(["create", "update"]),
|
|
244
|
-
fields: z.array(fieldSchema),
|
|
245
|
-
})
|
|
246
|
-
.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}"`;
|
|
247
208
|
|
|
248
|
-
|
|
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
|
+
}
|
|
249
222
|
|
|
250
|
-
export function prettyPrintMigrationPlan(
|
|
223
|
+
export function prettyPrintMigrationPlan(
|
|
224
|
+
migrationPlan: MigrationPlan,
|
|
225
|
+
target?: { serverUrl?: string; fileName?: string },
|
|
226
|
+
) {
|
|
251
227
|
if (!migrationPlan.length) {
|
|
252
228
|
console.log("No changes to apply. Database is up to date.");
|
|
253
229
|
return;
|
|
254
230
|
}
|
|
255
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
|
+
}
|
|
256
242
|
for (const step of migrationPlan) {
|
|
257
243
|
const emoji = step.operation === "create" ? "✅" : "✏️";
|
|
258
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 {};
|