better-auth 1.7.0 → 1.7.1
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/db/get-migration.d.mts +29 -2
- package/dist/db/get-migration.mjs +86 -24
- package/dist/package.mjs +1 -1
- package/package.json +8 -8
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
import { BetterAuthOptions } from "@better-auth/core";
|
|
2
2
|
import { DBFieldAttribute, DBFieldType } from "@better-auth/core/db";
|
|
3
|
+
import { BetterAuthError } from "@better-auth/core/error";
|
|
3
4
|
import { KyselyDatabaseType } from "@better-auth/kysely-adapter";
|
|
4
5
|
import { ResolvedDBTableIndex } from "@better-auth/core/db/internal";
|
|
5
6
|
|
|
6
7
|
//#region src/db/get-migration.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Thrown when {@link getMigrations} refuses to add a required column with no
|
|
10
|
+
* default value to a populated table. Distinct from the plain
|
|
11
|
+
* {@link BetterAuthError} thrown for index-definition conflicts, so callers
|
|
12
|
+
* can tell the two apart without matching on message text.
|
|
13
|
+
*/
|
|
14
|
+
declare class UnsafeMigrationError extends BetterAuthError {}
|
|
7
15
|
declare function matchType(columnDataType: string, fieldType: DBFieldType, dbType: KyselyDatabaseType): boolean;
|
|
8
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Build the migration plan that `auth migrate` executes and `auth generate`
|
|
18
|
+
* prints for the Kysely adapter.
|
|
19
|
+
*
|
|
20
|
+
* Adding a required column without a default to a populated table is refused:
|
|
21
|
+
* existing rows have no value to backfill. `throwOnUnsafe` picks how that
|
|
22
|
+
* refusal is delivered: executing callers get an {@link UnsafeMigrationError},
|
|
23
|
+
* read-only callers get the plan plus the same message in `unsafeChanges`.
|
|
24
|
+
*
|
|
25
|
+
* @throws {UnsafeMigrationError} when a required column cannot be migrated
|
|
26
|
+
* safely and `throwOnUnsafe` is left on.
|
|
27
|
+
* @throws {BetterAuthError} when an index definition conflicts with an
|
|
28
|
+
* existing or already-planned index.
|
|
29
|
+
*/
|
|
30
|
+
declare function getMigrations(config: BetterAuthOptions, {
|
|
31
|
+
throwOnUnsafe
|
|
32
|
+
}?: {
|
|
33
|
+
throwOnUnsafe?: boolean;
|
|
34
|
+
}): Promise<{
|
|
9
35
|
toBeCreated: {
|
|
10
36
|
table: string;
|
|
11
37
|
fields: Record<string, DBFieldAttribute>;
|
|
@@ -21,8 +47,9 @@ declare function getMigrations(config: BetterAuthOptions): Promise<{
|
|
|
21
47
|
index: ResolvedDBTableIndex;
|
|
22
48
|
name: string;
|
|
23
49
|
}[];
|
|
50
|
+
unsafeChanges: string[];
|
|
24
51
|
runMigrations: () => Promise<void>;
|
|
25
52
|
compileMigrations: () => Promise<string>;
|
|
26
53
|
}>;
|
|
27
54
|
//#endregion
|
|
28
|
-
export { getMigrations, matchType };
|
|
55
|
+
export { UnsafeMigrationError, getMigrations, matchType };
|
|
@@ -278,6 +278,24 @@ function assertExistingTableIndexFits({ columnBounds, dbType, existingColumns, f
|
|
|
278
278
|
}
|
|
279
279
|
if (requiredBytes > byteBudget) throw new BetterAuthError(`Cannot create database index "${index.name}" on existing table "${table}" because its columns can exceed ${dbType === "mysql" ? "MySQL" : "SQL Server"}'s ${byteBudget}-byte index-key limit. Bound the indexed string columns to the generated schema lengths, resolve oversized values, then run the migration again.`);
|
|
280
280
|
}
|
|
281
|
+
const columnBackfillGuideUrl = "https://better-auth.com/docs/guides/1-7-upgrade-guide#account-identity-is-scoped-by-issuer";
|
|
282
|
+
/**
|
|
283
|
+
* Thrown when {@link getMigrations} refuses to add a required column with no
|
|
284
|
+
* default value to a populated table. Distinct from the plain
|
|
285
|
+
* {@link BetterAuthError} thrown for index-definition conflicts, so callers
|
|
286
|
+
* can tell the two apart without matching on message text.
|
|
287
|
+
*/
|
|
288
|
+
var UnsafeMigrationError = class extends BetterAuthError {};
|
|
289
|
+
function hasTimestampColumnDefault(field, dbType) {
|
|
290
|
+
return field.type === "date" && typeof field.defaultValue === "function" && (dbType === "postgres" || dbType === "mysql" || dbType === "mssql");
|
|
291
|
+
}
|
|
292
|
+
function hasStaticColumnDefault(field) {
|
|
293
|
+
return !(field.unique && field.required === false) && (field.type === "string" || field.type === "number" || field.type === "boolean") && field.defaultValue !== void 0 && field.defaultValue !== null && typeof field.defaultValue !== "function";
|
|
294
|
+
}
|
|
295
|
+
async function tableHasRows(db, dbType, table) {
|
|
296
|
+
const probe = db.selectFrom(table).select(sql`1`.as("present"));
|
|
297
|
+
return (await (dbType === "mssql" ? probe.top(1) : probe.limit(1)).execute()).length > 0;
|
|
298
|
+
}
|
|
281
299
|
function matchType(columnDataType, fieldType, dbType) {
|
|
282
300
|
function normalize(type) {
|
|
283
301
|
return type.toLowerCase().split("(")[0].trim();
|
|
@@ -307,9 +325,34 @@ async function getMssqlSchema(db) {
|
|
|
307
325
|
return "dbo";
|
|
308
326
|
}
|
|
309
327
|
}
|
|
310
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Build the migration plan that `auth migrate` executes and `auth generate`
|
|
330
|
+
* prints for the Kysely adapter.
|
|
331
|
+
*
|
|
332
|
+
* Adding a required column without a default to a populated table is refused:
|
|
333
|
+
* existing rows have no value to backfill. `throwOnUnsafe` picks how that
|
|
334
|
+
* refusal is delivered: executing callers get an {@link UnsafeMigrationError},
|
|
335
|
+
* read-only callers get the plan plus the same message in `unsafeChanges`.
|
|
336
|
+
*
|
|
337
|
+
* @throws {UnsafeMigrationError} when a required column cannot be migrated
|
|
338
|
+
* safely and `throwOnUnsafe` is left on.
|
|
339
|
+
* @throws {BetterAuthError} when an index definition conflicts with an
|
|
340
|
+
* existing or already-planned index.
|
|
341
|
+
*/
|
|
342
|
+
async function getMigrations(config, { throwOnUnsafe = true } = {}) {
|
|
311
343
|
const betterAuthSchema = getSchema(config);
|
|
344
|
+
const authTables = getAuthTables(config);
|
|
345
|
+
const accountIssuer = authTables.account && {
|
|
346
|
+
table: authTables.account.modelName,
|
|
347
|
+
column: authTables.account.fields.issuer?.fieldName || "issuer"
|
|
348
|
+
};
|
|
349
|
+
const isAccountIssuerColumn = (table, column) => table === accountIssuer?.table && column === accountIssuer.column;
|
|
312
350
|
const logger = createLogger(config.logger);
|
|
351
|
+
const unsafeChanges = [];
|
|
352
|
+
const reportUnsafeChange = (message) => {
|
|
353
|
+
if (throwOnUnsafe) throw new UnsafeMigrationError(message);
|
|
354
|
+
unsafeChanges.push(message);
|
|
355
|
+
};
|
|
313
356
|
let { kysely: db, databaseType: dbType } = await createKyselyAdapter(config);
|
|
314
357
|
if (!dbType) {
|
|
315
358
|
logger.warn("Could not determine database type, defaulting to sqlite. Please provide a type in the database options to avoid this.");
|
|
@@ -421,6 +464,7 @@ async function getMigrations(config) {
|
|
|
421
464
|
toBeAddedFields[fieldName] = field;
|
|
422
465
|
continue;
|
|
423
466
|
}
|
|
467
|
+
if (field.required !== false && column.isNullable) logger.warn(`Column "${fieldName}" on table "${key}" stays nullable while the schema declares the field required, so existing rows can still hold null. Backfill every row for this column and enforce NOT NULL to remove the drift.`);
|
|
424
468
|
if (matchType(column.dataType, field.type, dbType)) continue;
|
|
425
469
|
else logger.warn(`Field ${fieldName} in table ${key} has a different type in the database. Expected ${field.type} but got ${column.dataType}.`);
|
|
426
470
|
}
|
|
@@ -501,11 +545,11 @@ async function getMigrations(config) {
|
|
|
501
545
|
return typeMap[type][provider];
|
|
502
546
|
}
|
|
503
547
|
const getModelName = initGetModelName({
|
|
504
|
-
schema:
|
|
548
|
+
schema: authTables,
|
|
505
549
|
usePlural: false
|
|
506
550
|
});
|
|
507
551
|
const getFieldName = initGetFieldName({
|
|
508
|
-
schema:
|
|
552
|
+
schema: authTables,
|
|
509
553
|
usePlural: false
|
|
510
554
|
});
|
|
511
555
|
function getReferencePath(model, field) {
|
|
@@ -530,28 +574,45 @@ async function getMigrations(config) {
|
|
|
530
574
|
indexes: table.indexes ?? []
|
|
531
575
|
});
|
|
532
576
|
};
|
|
533
|
-
if (toBeAdded.length)
|
|
534
|
-
const
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
577
|
+
if (toBeAdded.length) {
|
|
578
|
+
const populatedTables = /* @__PURE__ */ new Map();
|
|
579
|
+
for (const table of toBeAdded) for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
580
|
+
const timestampDefault = hasTimestampColumnDefault(field, dbType);
|
|
581
|
+
const staticDefault = hasStaticColumnDefault(field);
|
|
582
|
+
if (field.required !== false && !timestampDefault && !staticDefault) {
|
|
583
|
+
let populated = populatedTables.get(table.table);
|
|
584
|
+
if (populated === void 0) {
|
|
585
|
+
populated = await tableHasRows(db, dbType, table.table);
|
|
586
|
+
populatedTables.set(table.table, populated);
|
|
587
|
+
}
|
|
588
|
+
if (populated) {
|
|
589
|
+
const textDetail = field.type === "string" ? " For a text column, every existing row ends up with the same empty string." : "";
|
|
590
|
+
const guideLink = isAccountIssuerColumn(table.table, fieldName) ? ` See ${columnBackfillGuideUrl}` : "";
|
|
591
|
+
reportUnsafeChange(`Cannot add required column "${fieldName}" to populated table "${table.table}": the schema declares no default value, so existing rows have no value to backfill. MySQL accepts this statement instead of rejecting it and fills every existing row with an implicit default for the column type, reporting a successful migration over corrupted data.${textDetail} Add the column as nullable, backfill a correct value for every row, then make it NOT NULL.${guideLink}`);
|
|
592
|
+
}
|
|
543
593
|
}
|
|
544
|
-
|
|
594
|
+
const type = getType(field, fieldName, getTableIndexStringLength(table.table, fieldName));
|
|
595
|
+
const builder = db.schema.alterTable(table.table);
|
|
596
|
+
if (field.index || field.unique) {
|
|
597
|
+
const indexName = getDatabaseFieldIndexName(table.table, fieldName, field.unique ?? false);
|
|
598
|
+
let indexBuilder = db.schema.createIndex(indexName).on(table.table).columns([fieldName]);
|
|
599
|
+
if (field.unique) {
|
|
600
|
+
indexBuilder = indexBuilder.unique();
|
|
601
|
+
if (field.required === false && dbType === "mssql") indexBuilder = indexBuilder.where(fieldName, "is not", null);
|
|
602
|
+
if (field.required !== false && field.defaultValue !== void 0 && field.defaultValue !== null && typeof field.defaultValue !== "function") logger.warn(`Adding unique column "${fieldName}" to existing table "${table.table}" backfills every existing row with its default value. If the table has more than one row, creating the unique index "${indexName}" will fail; backfill distinct values manually, then re-run the migration or create the index yourself.`);
|
|
603
|
+
}
|
|
604
|
+
deferredIndexes.push(indexBuilder);
|
|
605
|
+
}
|
|
606
|
+
const built = builder.addColumn(fieldName, type, (col) => {
|
|
607
|
+
col = field.required !== false ? col.notNull() : col;
|
|
608
|
+
if (field.references) col = col.references(getReferencePath(field.references.model, field.references.field)).onDelete(field.references.onDelete || "cascade");
|
|
609
|
+
if (timestampDefault) if (dbType === "mysql") col = col.defaultTo(sql`CURRENT_TIMESTAMP(3)`);
|
|
610
|
+
else col = col.defaultTo(sql`CURRENT_TIMESTAMP`);
|
|
611
|
+
else if (staticDefault) col = col.defaultTo(typeof field.defaultValue === "boolean" && (dbType === "sqlite" || dbType === "mssql") ? field.defaultValue ? 1 : 0 : field.defaultValue);
|
|
612
|
+
return col;
|
|
613
|
+
});
|
|
614
|
+
migrations.push(built);
|
|
545
615
|
}
|
|
546
|
-
const built = builder.addColumn(fieldName, type, (col) => {
|
|
547
|
-
col = field.required !== false ? col.notNull() : col;
|
|
548
|
-
if (field.references) col = col.references(getReferencePath(field.references.model, field.references.field)).onDelete(field.references.onDelete || "cascade");
|
|
549
|
-
if (field.type === "date" && typeof field.defaultValue === "function" && (dbType === "postgres" || dbType === "mysql" || dbType === "mssql")) if (dbType === "mysql") col = col.defaultTo(sql`CURRENT_TIMESTAMP(3)`);
|
|
550
|
-
else col = col.defaultTo(sql`CURRENT_TIMESTAMP`);
|
|
551
|
-
else if (!(field.unique && field.required === false) && (field.type === "string" || field.type === "number" || field.type === "boolean") && field.defaultValue !== void 0 && field.defaultValue !== null && typeof field.defaultValue !== "function") col = col.defaultTo(typeof field.defaultValue === "boolean" && (dbType === "sqlite" || dbType === "mssql") ? field.defaultValue ? 1 : 0 : field.defaultValue);
|
|
552
|
-
return col;
|
|
553
|
-
});
|
|
554
|
-
migrations.push(built);
|
|
555
616
|
}
|
|
556
617
|
if (toBeCreated.length) for (const table of toBeCreated) {
|
|
557
618
|
const idType = getType({ type: useNumberId ? "number" : "string" }, "id");
|
|
@@ -601,9 +662,10 @@ async function getMigrations(config) {
|
|
|
601
662
|
toBeCreated,
|
|
602
663
|
toBeAdded,
|
|
603
664
|
toBeAddedIndexes,
|
|
665
|
+
unsafeChanges,
|
|
604
666
|
runMigrations,
|
|
605
667
|
compileMigrations
|
|
606
668
|
};
|
|
607
669
|
}
|
|
608
670
|
//#endregion
|
|
609
|
-
export { getMigrations, matchType };
|
|
671
|
+
export { UnsafeMigrationError, getMigrations, matchType };
|
package/dist/package.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -473,13 +473,13 @@
|
|
|
473
473
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
474
474
|
"nanostores": "^1.3.0",
|
|
475
475
|
"zod": "^4.3.6",
|
|
476
|
-
"@better-auth/core": "1.7.
|
|
477
|
-
"@better-auth/drizzle-adapter": "1.7.
|
|
478
|
-
"@better-auth/kysely-adapter": "1.7.
|
|
479
|
-
"@better-auth/memory-adapter": "1.7.
|
|
480
|
-
"@better-auth/mongo-adapter": "1.7.
|
|
481
|
-
"@better-auth/prisma-adapter": "1.7.
|
|
482
|
-
"@better-auth/telemetry": "1.7.
|
|
476
|
+
"@better-auth/core": "1.7.1",
|
|
477
|
+
"@better-auth/drizzle-adapter": "1.7.1",
|
|
478
|
+
"@better-auth/kysely-adapter": "1.7.1",
|
|
479
|
+
"@better-auth/memory-adapter": "1.7.1",
|
|
480
|
+
"@better-auth/mongo-adapter": "1.7.1",
|
|
481
|
+
"@better-auth/prisma-adapter": "1.7.1",
|
|
482
|
+
"@better-auth/telemetry": "1.7.1"
|
|
483
483
|
},
|
|
484
484
|
"devDependencies": {
|
|
485
485
|
"@lynx-js/react": "^0.121.2",
|