@proofkit/better-auth 0.1.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/LICENSE.md +21 -0
- package/README.md +11 -0
- package/dist/esm/adapter.d.ts +13 -0
- package/dist/esm/adapter.js +178 -0
- package/dist/esm/adapter.js.map +1 -0
- package/dist/esm/better-auth-cli/utils/add-svelte-kit-env-modules.d.ts +3 -0
- package/dist/esm/better-auth-cli/utils/add-svelte-kit-env-modules.js +101 -0
- package/dist/esm/better-auth-cli/utils/add-svelte-kit-env-modules.js.map +1 -0
- package/dist/esm/better-auth-cli/utils/get-config.d.ts +8 -0
- package/dist/esm/better-auth-cli/utils/get-config.js +177 -0
- package/dist/esm/better-auth-cli/utils/get-config.js.map +1 -0
- package/dist/esm/better-auth-cli/utils/get-tsconfig-info.d.ts +2 -0
- package/dist/esm/better-auth-cli/utils/get-tsconfig-info.js +25 -0
- package/dist/esm/better-auth-cli/utils/get-tsconfig-info.js.map +1 -0
- package/dist/esm/cli/index.d.ts +2 -0
- package/dist/esm/cli/index.js +86 -0
- package/dist/esm/cli/index.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/migrate.d.ts +82 -0
- package/dist/esm/migrate.js +181 -0
- package/dist/esm/migrate.js.map +1 -0
- package/dist/esm/odata/index.d.ts +21 -0
- package/dist/esm/odata/index.js +27 -0
- package/dist/esm/odata/index.js.map +1 -0
- package/package.json +70 -0
- package/src/adapter.ts +221 -0
- package/src/better-auth-cli/README.md +1 -0
- package/src/better-auth-cli/utils/add-svelte-kit-env-modules.ts +108 -0
- package/src/better-auth-cli/utils/get-config.ts +220 -0
- package/src/better-auth-cli/utils/get-tsconfig-info.ts +26 -0
- package/src/cli/index.ts +116 -0
- package/src/index.ts +1 -0
- package/src/migrate.ts +236 -0
- package/src/odata/index.ts +45 -0
package/src/migrate.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { type BetterAuthDbSchema } from "better-auth/db";
|
|
2
|
+
import { Database, type Field as FmField } from "fm-odata-client";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import z from "zod/v4";
|
|
5
|
+
|
|
6
|
+
export async function planMigration(
|
|
7
|
+
db: Database,
|
|
8
|
+
betterAuthSchema: BetterAuthDbSchema,
|
|
9
|
+
): Promise<MigrationPlan> {
|
|
10
|
+
const metadata = await db.getMetadata().catch((error) => {
|
|
11
|
+
console.error("Failed to get metadata from database", error);
|
|
12
|
+
return null;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Build a map from entity set name to entity type key
|
|
16
|
+
let entitySetToType: Record<string, string> = {};
|
|
17
|
+
if (metadata) {
|
|
18
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
19
|
+
if (value.$Kind === "EntitySet" && value.$Type) {
|
|
20
|
+
// $Type is like 'betterauth_test.fmp12.proofkit_user_'
|
|
21
|
+
const typeKey = value.$Type.split(".").pop(); // e.g., 'proofkit_user_'
|
|
22
|
+
entitySetToType[key] = typeKey || key;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const existingTables = metadata
|
|
28
|
+
? Object.entries(entitySetToType).reduce(
|
|
29
|
+
(acc, [entitySetName, entityTypeKey]) => {
|
|
30
|
+
const entityType = metadata[entityTypeKey];
|
|
31
|
+
if (!entityType) return acc;
|
|
32
|
+
const fields = Object.entries(entityType)
|
|
33
|
+
.filter(
|
|
34
|
+
([fieldKey, fieldValue]) =>
|
|
35
|
+
typeof fieldValue === "object" &&
|
|
36
|
+
fieldValue !== null &&
|
|
37
|
+
"$Type" in fieldValue,
|
|
38
|
+
)
|
|
39
|
+
.map(([fieldKey, fieldValue]) => ({
|
|
40
|
+
name: fieldKey,
|
|
41
|
+
type:
|
|
42
|
+
fieldValue.$Type === "Edm.String"
|
|
43
|
+
? "string"
|
|
44
|
+
: fieldValue.$Type === "Edm.DateTimeOffset"
|
|
45
|
+
? "timestamp"
|
|
46
|
+
: fieldValue.$Type === "Edm.Decimal" ||
|
|
47
|
+
fieldValue.$Type === "Edm.Int32" ||
|
|
48
|
+
fieldValue.$Type === "Edm.Int64"
|
|
49
|
+
? "numeric"
|
|
50
|
+
: "string",
|
|
51
|
+
}));
|
|
52
|
+
acc[entitySetName] = fields;
|
|
53
|
+
return acc;
|
|
54
|
+
},
|
|
55
|
+
{} as Record<string, { name: string; type: string }[]>,
|
|
56
|
+
)
|
|
57
|
+
: {};
|
|
58
|
+
|
|
59
|
+
const baTables = Object.entries(betterAuthSchema)
|
|
60
|
+
.sort((a, b) => (a[1].order ?? 0) - (b[1].order ?? 0))
|
|
61
|
+
.map(([key, value]) => ({
|
|
62
|
+
...value,
|
|
63
|
+
keyName: key,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
const migrationPlan: MigrationPlan = [];
|
|
67
|
+
|
|
68
|
+
for (const baTable of baTables) {
|
|
69
|
+
const fields: FmField[] = Object.entries(baTable.fields).map(
|
|
70
|
+
([key, field]) => ({
|
|
71
|
+
name: field.fieldName ?? key,
|
|
72
|
+
type:
|
|
73
|
+
field.type === "boolean" || field.type.includes("number")
|
|
74
|
+
? "numeric"
|
|
75
|
+
: field.type === "date"
|
|
76
|
+
? "timestamp"
|
|
77
|
+
: "string",
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// get existing table or create it
|
|
82
|
+
const tableExists = Object.prototype.hasOwnProperty.call(
|
|
83
|
+
existingTables,
|
|
84
|
+
baTable.modelName,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!tableExists) {
|
|
88
|
+
migrationPlan.push({
|
|
89
|
+
tableName: baTable.modelName,
|
|
90
|
+
operation: "create",
|
|
91
|
+
fields: [
|
|
92
|
+
{
|
|
93
|
+
name: "id",
|
|
94
|
+
type: "string",
|
|
95
|
+
primary: true,
|
|
96
|
+
unique: true,
|
|
97
|
+
},
|
|
98
|
+
...fields,
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
const existingFields = (existingTables[baTable.modelName] || []).map(
|
|
103
|
+
(f) => f.name,
|
|
104
|
+
);
|
|
105
|
+
const existingFieldMap = (existingTables[baTable.modelName] || []).reduce(
|
|
106
|
+
(acc, f) => {
|
|
107
|
+
acc[f.name] = f.type;
|
|
108
|
+
return acc;
|
|
109
|
+
},
|
|
110
|
+
{} as Record<string, string>,
|
|
111
|
+
);
|
|
112
|
+
// Warn about type mismatches (optional, not in plan)
|
|
113
|
+
fields.forEach((field) => {
|
|
114
|
+
if (
|
|
115
|
+
existingFields.includes(field.name) &&
|
|
116
|
+
existingFieldMap[field.name] !== field.type
|
|
117
|
+
) {
|
|
118
|
+
console.warn(
|
|
119
|
+
`⚠️ 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.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const fieldsToAdd = fields.filter(
|
|
124
|
+
(f) => !existingFields.includes(f.name),
|
|
125
|
+
);
|
|
126
|
+
if (fieldsToAdd.length > 0) {
|
|
127
|
+
migrationPlan.push({
|
|
128
|
+
tableName: baTable.modelName,
|
|
129
|
+
operation: "update",
|
|
130
|
+
fields: fieldsToAdd,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return migrationPlan;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function executeMigration(
|
|
140
|
+
db: Database,
|
|
141
|
+
migrationPlan: MigrationPlan,
|
|
142
|
+
) {
|
|
143
|
+
for (const step of migrationPlan) {
|
|
144
|
+
if (step.operation === "create") {
|
|
145
|
+
console.log("Creating table:", step.tableName);
|
|
146
|
+
await db.schemaManager().createTable(step.tableName, step.fields);
|
|
147
|
+
} else if (step.operation === "update") {
|
|
148
|
+
console.log("Adding fields to table:", step.tableName);
|
|
149
|
+
await db.schemaManager().addFields(step.tableName, step.fields);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const genericFieldSchema = z.object({
|
|
155
|
+
name: z.string(),
|
|
156
|
+
nullable: z.boolean().optional(),
|
|
157
|
+
primary: z.boolean().optional(),
|
|
158
|
+
unique: z.boolean().optional(),
|
|
159
|
+
global: z.boolean().optional(),
|
|
160
|
+
repetitions: z.number().optional(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const stringFieldSchema = genericFieldSchema.extend({
|
|
164
|
+
type: z.literal("string"),
|
|
165
|
+
maxLength: z.number().optional(),
|
|
166
|
+
default: z.enum(["USER", "USERNAME", "CURRENT_USER"]).optional(),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const numericFieldSchema = genericFieldSchema.extend({
|
|
170
|
+
type: z.literal("numeric"),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const dateFieldSchema = genericFieldSchema.extend({
|
|
174
|
+
type: z.literal("date"),
|
|
175
|
+
default: z.enum(["CURRENT_DATE", "CURDATE"]).optional(),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const timeFieldSchema = genericFieldSchema.extend({
|
|
179
|
+
type: z.literal("time"),
|
|
180
|
+
default: z.enum(["CURRENT_TIME", "CURTIME"]).optional(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const timestampFieldSchema = genericFieldSchema.extend({
|
|
184
|
+
type: z.literal("timestamp"),
|
|
185
|
+
default: z.enum(["CURRENT_TIMESTAMP", "CURTIMESTAMP"]).optional(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const containerFieldSchema = genericFieldSchema.extend({
|
|
189
|
+
type: z.literal("container"),
|
|
190
|
+
externalSecurePath: z.string().optional(),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const fieldSchema = z.discriminatedUnion("type", [
|
|
194
|
+
stringFieldSchema,
|
|
195
|
+
numericFieldSchema,
|
|
196
|
+
dateFieldSchema,
|
|
197
|
+
timeFieldSchema,
|
|
198
|
+
timestampFieldSchema,
|
|
199
|
+
containerFieldSchema,
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
export const migrationPlanSchema = z
|
|
203
|
+
.object({
|
|
204
|
+
tableName: z.string(),
|
|
205
|
+
operation: z.enum(["create", "update"]),
|
|
206
|
+
fields: z.array(fieldSchema),
|
|
207
|
+
})
|
|
208
|
+
.array();
|
|
209
|
+
|
|
210
|
+
export type MigrationPlan = z.infer<typeof migrationPlanSchema>;
|
|
211
|
+
|
|
212
|
+
export function prettyPrintMigrationPlan(migrationPlan: MigrationPlan) {
|
|
213
|
+
if (!migrationPlan.length) {
|
|
214
|
+
console.log("No changes to apply. Database is up to date.");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
console.log(chalk.bold.green("Migration plan:"));
|
|
218
|
+
for (const step of migrationPlan) {
|
|
219
|
+
const emoji = step.operation === "create" ? "✅" : "✏️";
|
|
220
|
+
console.log(
|
|
221
|
+
`\n${emoji} ${step.operation === "create" ? chalk.bold.green("Create table") : chalk.bold.yellow("Update table")}: ${step.tableName}`,
|
|
222
|
+
);
|
|
223
|
+
if (step.fields.length) {
|
|
224
|
+
for (const field of step.fields) {
|
|
225
|
+
let fieldDesc = ` - ${field.name} (${field.type}`;
|
|
226
|
+
if (field.primary) fieldDesc += ", primary";
|
|
227
|
+
if (field.unique) fieldDesc += ", unique";
|
|
228
|
+
fieldDesc += ")";
|
|
229
|
+
console.log(fieldDesc);
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
console.log(" (No fields to add)");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
console.log("");
|
|
236
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { BasicAuth, Connection, Database } from "fm-odata-client";
|
|
2
|
+
|
|
3
|
+
export type BasicAuthCredentials = {
|
|
4
|
+
username: string;
|
|
5
|
+
password: string;
|
|
6
|
+
};
|
|
7
|
+
export type OttoAPIKeyAuth = {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
};
|
|
10
|
+
export type ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;
|
|
11
|
+
|
|
12
|
+
export function isBasicAuth(auth: ODataAuth): auth is BasicAuthCredentials {
|
|
13
|
+
return (
|
|
14
|
+
typeof (auth as BasicAuthCredentials).username === "string" &&
|
|
15
|
+
typeof (auth as BasicAuthCredentials).password === "string"
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isOttoAPIKeyAuth(auth: ODataAuth): auth is OttoAPIKeyAuth {
|
|
20
|
+
return typeof (auth as OttoAPIKeyAuth).apiKey === "string";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type FmOdataConfig = {
|
|
24
|
+
hostname: string;
|
|
25
|
+
auth: ODataAuth;
|
|
26
|
+
database: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class FmOdata {
|
|
30
|
+
public connection: Connection;
|
|
31
|
+
public database: Database;
|
|
32
|
+
|
|
33
|
+
constructor(args: FmOdataConfig) {
|
|
34
|
+
if (isOttoAPIKeyAuth(args.auth)) {
|
|
35
|
+
throw new Error("Otto API key auth is yet not supported");
|
|
36
|
+
} else {
|
|
37
|
+
this.connection = new Connection(
|
|
38
|
+
args.hostname.replace(/^https?:\/\//, "").replace(/\/$/, ""),
|
|
39
|
+
new BasicAuth(args.auth.username, args.auth.password),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.database = new Database(this.connection, args.database);
|
|
44
|
+
}
|
|
45
|
+
}
|