defaultCoreDataMergeHandler static method

Future<void> defaultCoreDataMergeHandler(
  1. Session session, {
  2. required UuidValue userToKeepId,
  3. required UuidValue userToRemoveId,
  4. required Transaction transaction,
})

Merges core Serverpod data from userToRemove to userToKeep.

This includes:

  • Merging scopes
  • Moving access tokens and refresh tokens
  • Moving user profile (if userToKeep does not have one)

Implementation

static Future<void> defaultCoreDataMergeHandler(
  final Session session, {
  required final UuidValue userToKeepId,
  required final UuidValue userToRemoveId,
  required final Transaction transaction,
}) async {
  final users = await Future.wait([
    AuthUser.db.findById(
      session,
      userToKeepId,
      transaction: transaction,
    ),
    AuthUser.db.findById(
      session,
      userToRemoveId,
      transaction: transaction,
    ),
  ]);
  AuthUser? userToKeep = users[0];
  final userToRemove = users[1];

  if (userToKeep == null || userToRemove == null) {
    throw AuthUserNotFoundException();
  }

  // Merge Scopes
  final combinedScopes = {
    ...userToKeep.scopeNames,
    ...userToRemove.scopeNames,
  };
  final mergedBlocked = userToKeep.blocked || userToRemove.blocked;
  if (combinedScopes.length > userToKeep.scopeNames.length ||
      mergedBlocked != userToKeep.blocked) {
    userToKeep = userToKeep.copyWith(
      scopeNames: combinedScopes,
      blocked: mergedBlocked,
    );
    userToKeep = await AuthUser.db.updateRow(
      session,
      userToKeep,
      transaction: transaction,
    );
  }

  // Move Refresh Tokens
  await RefreshToken.db.updateWhere(
    session,
    where: (final t) => t.authUserId.equals(userToRemove.id),
    columnValues: (final t) => [t.authUserId(userToKeep!.id!)],
    transaction: transaction,
  );

  // Move Server-Side Sessions
  await ServerSideSession.db.updateWhere(
    session,
    where: (final t) => t.authUserId.equals(userToRemove.id),
    columnValues: (final t) => [t.authUserId(userToKeep!.id!)],
    transaction: transaction,
  );

  // Load existing profiles
  final profiles = await Future.wait([
    UserProfile.db.findFirstRow(
      session,
      where: (final t) => t.authUserId.equals(userToKeep!.id!),
      transaction: transaction,
    ),

    UserProfile.db.findFirstRow(
      session,
      where: (final t) => t.authUserId.equals(userToRemove.id),
      transaction: transaction,
    ),
  ]);
  final keepProfile = profiles[0];
  final removeProfile = profiles[1];

  // Merge User Profile
  if (removeProfile != null) {
    if (keepProfile != null) {
      // Both profiles exist, merge fields
      final mergedProfile = keepProfile.merge(removeProfile);
      if (keepProfile.imageId == null && removeProfile.imageId != null) {
        await UserProfileImage.db.updateWhere(
          session,
          where: (final t) => t.id.equals(removeProfile.imageId),
          columnValues: (final t) => [
            t.userProfileId(keepProfile.id!),
          ],
          transaction: transaction,
        );
      }
      await UserProfile.db.updateRow(
        session,
        mergedProfile,
        transaction: transaction,
      );
      // Don't need to remove the userToRemove profile since that database
      // relationship is set to onDelete=cascade.
    } else {
      session.log(
        'No profile found for user to keep ${userToKeep.id}. As a '
        'fallback, the profile from to-be-deleted user ${userToRemove.id} '
        'is being migrated to User ${userToKeep.id}. This likely '
        'constitutes an error -- please file an issue with the Serverpod '
        'team.',
        level: LogLevel.warning,
      );
      // Target has no profile, move the origin profile over
      await UserProfile.db.updateRow(
        session,
        removeProfile.copyWith(authUserId: userToKeep.id),
        columns: (final t) => [t.authUserId],
        transaction: transaction,
      );
    }
  }
}