mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 23:53:17 -05:00
fix(cli): roll back MySQL SCIM retirement
This commit is contained in:
@@ -1373,7 +1373,10 @@ it("retires published 1.6.30 SCIM credentials with a custom account ID column an
|
||||
}
|
||||
});
|
||||
|
||||
it("checkpoints MySQL legacy tables before retiring SCIM accounts", {
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/pull/10575#discussion_r3831145554
|
||||
*/
|
||||
it("checkpoints MySQL legacy tables and rolls back interrupted SCIM retirement", {
|
||||
timeout: 60_000,
|
||||
}, async () => {
|
||||
const databaseName = createDatabaseName().replace("postgres", "scim_mysql");
|
||||
@@ -1417,12 +1420,6 @@ it("checkpoints MySQL legacy tables before retiring SCIM accounts", {
|
||||
emailDomain: "mysql.example.com",
|
||||
});
|
||||
|
||||
const auth17 = betterAuth({
|
||||
baseURL: "http://localhost:3000",
|
||||
database: { db: currentDatabase, transaction: true, type: "mysql" },
|
||||
emailAndPassword: { enabled: true },
|
||||
plugins: [createCurrentScimPlugin(legacyScimAccount.userId)],
|
||||
});
|
||||
const options = {
|
||||
accountIssuers: {
|
||||
workforce: "local:retired-scim:workforce",
|
||||
@@ -1432,6 +1429,12 @@ it("checkpoints MySQL legacy tables before retiring SCIM accounts", {
|
||||
providers: "reprovision" as const,
|
||||
},
|
||||
};
|
||||
const auth17 = betterAuth({
|
||||
baseURL: "http://localhost:3000",
|
||||
database: { db: currentDatabase, transaction: true, type: "mysql" },
|
||||
emailAndPassword: { enabled: true },
|
||||
plugins: [createCurrentScimPlugin(legacyScimAccount.userId)],
|
||||
});
|
||||
await expect(migrateFrom16(auth17.options, options)).rejects.toThrow(
|
||||
"forced SCIM account retirement interruption",
|
||||
);
|
||||
@@ -1454,13 +1457,18 @@ it("checkpoints MySQL legacy tables before retiring SCIM accounts", {
|
||||
"SELECT COUNT(*) AS count FROM `account` WHERE `id` = ?",
|
||||
[legacyScimAccount.id],
|
||||
);
|
||||
expect(Number(remainingAccounts[0]?.count)).toBe(0);
|
||||
expect(Number(remainingAccounts[0]?.count)).toBe(1);
|
||||
|
||||
const migration = await migrateFrom16(auth17.options, options);
|
||||
expect(migration).toMatchObject({
|
||||
accounts: { migrated: 0, providers: {} },
|
||||
scim: {
|
||||
identities: [],
|
||||
identities: [
|
||||
{
|
||||
providerId: "workforce",
|
||||
userId: legacyScimAccount.userId,
|
||||
},
|
||||
],
|
||||
reprovisionRequired: true,
|
||||
retiredProviders: 1,
|
||||
},
|
||||
|
||||
@@ -1631,8 +1631,21 @@ export async function migrateFrom16(
|
||||
options,
|
||||
releaseData,
|
||||
);
|
||||
await inspectScimAccountsFrom16(config, options, releaseData);
|
||||
const inspectedScimAccounts = await inspectScimAccountsFrom16(
|
||||
config,
|
||||
options,
|
||||
releaseData,
|
||||
);
|
||||
const migrationDatabase = await getMigrationDatabase(config);
|
||||
if (
|
||||
migrationDatabase.databaseType === "mysql" &&
|
||||
inspectedScimAccounts.length > 0 &&
|
||||
!migrationDatabase.transaction
|
||||
) {
|
||||
throw new BetterAuthError(
|
||||
`The ${migrationDatabase.adapterId} adapter must expose a transaction-scoped migration connection before Better Auth can safely retire populated 1.6 SCIM accounts on MySQL.`,
|
||||
);
|
||||
}
|
||||
const initialMigration = await getMigrationsWithDatabase(
|
||||
config,
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import type { BetterAuthOptions } from "@better-auth/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getMigrationDatabase } from "./migration-database";
|
||||
import type {
|
||||
LegacyReleaseDataState,
|
||||
MigrateFrom16Options,
|
||||
@@ -47,6 +48,32 @@ function createScimRetirementFixture() {
|
||||
}
|
||||
|
||||
describe("1.6 SCIM account retirement", () => {
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/pull/10575#discussion_r3831145554
|
||||
*/
|
||||
it("requires a transaction before deleting MySQL SCIM accounts", async () => {
|
||||
const { config, database, options, state } = createScimRetirementFixture();
|
||||
const migrationDatabase = await getMigrationDatabase(config);
|
||||
const inspectedAccounts = await inspectScimAccountsFrom16(
|
||||
config,
|
||||
options,
|
||||
state,
|
||||
);
|
||||
|
||||
await expect(
|
||||
retireScimAccountsFrom16(config, options, state, inspectedAccounts, {
|
||||
...migrationDatabase,
|
||||
databaseType: "mysql",
|
||||
transaction: undefined,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"must expose a transaction-scoped migration connection before Better Auth can safely retire populated 1.6 SCIM accounts on MySQL",
|
||||
);
|
||||
expect(
|
||||
database.prepare(`SELECT "id" FROM "account" ORDER BY "id"`).all(),
|
||||
).toEqual([{ id: "a1" }]);
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/pull/10575
|
||||
*/
|
||||
|
||||
@@ -1382,20 +1382,22 @@ async function readScimAccountsFrom16(
|
||||
(table) => table.name === accountTable,
|
||||
);
|
||||
if (!accountTableMetadata) return { accounts: [], accountTable };
|
||||
const accounts =
|
||||
providerIds.size === 0
|
||||
? []
|
||||
: (
|
||||
await sql<LegacyScimAccountRecord>`
|
||||
SELECT
|
||||
${sql.ref(idColumn)} AS "id",
|
||||
${sql.ref(accountIdColumn)} AS "providerAccountId",
|
||||
${sql.ref(providerIdColumn)} AS "providerId",
|
||||
${sql.ref(userIdColumn)} AS "userId"
|
||||
FROM ${sql.table(accountTable)}
|
||||
WHERE ${sql.ref(providerIdColumn)} IN (${sql.join([...providerIds])})
|
||||
`.execute(kysely)
|
||||
).rows;
|
||||
if (providerIds.size === 0) return { accounts: [], accountTable };
|
||||
const accountQuery = sql<LegacyScimAccountRecord>`
|
||||
SELECT
|
||||
${sql.ref(idColumn)} AS "id",
|
||||
${sql.ref(accountIdColumn)} AS "providerAccountId",
|
||||
${sql.ref(providerIdColumn)} AS "providerId",
|
||||
${sql.ref(userIdColumn)} AS "userId"
|
||||
FROM ${sql.table(accountTable)}
|
||||
WHERE ${sql.ref(providerIdColumn)} IN (${sql.join([...providerIds])})
|
||||
`;
|
||||
const lockedAccountQuery =
|
||||
migrationDatabase?.databaseType === "mysql" &&
|
||||
migrationDatabase.inTransaction
|
||||
? sql<LegacyScimAccountRecord>`${accountQuery} FOR UPDATE`
|
||||
: accountQuery;
|
||||
const accounts = (await lockedAccountQuery.execute(kysely)).rows;
|
||||
return { accounts: [...accounts], accountTable };
|
||||
}
|
||||
|
||||
@@ -1467,12 +1469,13 @@ export async function retireScimAccountsFrom16(
|
||||
migrationDatabase?: MigrationDatabase,
|
||||
): Promise<LegacyScimAccountRow[]> {
|
||||
if (!state.scimProvider || !options.scim) return [];
|
||||
const database = migrationDatabase ?? (await getMigrationDatabase(config));
|
||||
const accounts = await inspectScimAccountsFrom16(
|
||||
config,
|
||||
options,
|
||||
state,
|
||||
undefined,
|
||||
migrationDatabase,
|
||||
database,
|
||||
);
|
||||
const freshAccountIds = new Set(accounts.map((account) => account.id));
|
||||
if (
|
||||
@@ -1485,7 +1488,17 @@ export async function retireScimAccountsFrom16(
|
||||
);
|
||||
}
|
||||
if (accounts.length === 0) return [];
|
||||
const { kysely } = migrationDatabase ?? (await getMigrationDatabase(config));
|
||||
if (database.databaseType === "mysql" && !database.inTransaction) {
|
||||
if (!database.transaction) {
|
||||
throw new BetterAuthError(
|
||||
`The ${database.adapterId} adapter must expose a transaction-scoped migration connection before Better Auth can safely retire populated 1.6 SCIM accounts on MySQL.`,
|
||||
);
|
||||
}
|
||||
return database.transaction((transaction) =>
|
||||
retireScimAccountsFrom16(config, options, state, accounts, transaction),
|
||||
);
|
||||
}
|
||||
const { kysely } = database;
|
||||
const accountSchema = getAuthTables(config).account;
|
||||
if (!accountSchema) return [];
|
||||
const accountTable = accountSchema.modelName || "account";
|
||||
@@ -1500,7 +1513,7 @@ export async function retireScimAccountsFrom16(
|
||||
const { accounts: remainingAccounts } = await readScimAccountsFrom16(
|
||||
config,
|
||||
state,
|
||||
migrationDatabase,
|
||||
database,
|
||||
);
|
||||
if (remainingAccounts.length > 0) {
|
||||
reportMigrationDecisionBlocker(undefined, {
|
||||
|
||||
Reference in New Issue
Block a user