feat(user): persist personalization.location with updateUserLocation

This commit is contained in:
Marco Beretta 2026-06-15 01:52:10 +02:00
parent 955674db44
commit 72c8eb512f
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
4 changed files with 88 additions and 1 deletions

View file

@ -1,9 +1,9 @@
import mongoose from 'mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import type * as t from '~/types';
import balanceSchema from '~/schema/balance';
import { createUserMethods } from './user';
import userSchema from '~/schema/user';
import balanceSchema from '~/schema/balance';
/** Mocking crypto for generateToken */
jest.mock('~/crypto', () => ({
@ -694,6 +694,41 @@ describe('User Methods - Database Tests', () => {
});
});
describe('updateUserLocation', () => {
it('persists a manual location on a user with no personalization object', async () => {
const user = await mongoose.models.User.create({ email: 'loc1@test.com' });
const updated = await methods.updateUserLocation(user._id.toString(), {
enabled: true,
source: 'manual',
manual: 'Berlin, Germany',
timezone: 'Europe/Berlin',
});
expect(updated?.personalization?.location?.enabled).toBe(true);
expect(updated?.personalization?.location?.manual).toBe('Berlin, Germany');
expect(updated?.personalization?.location?.timezone).toBe('Europe/Berlin');
});
it('persists device coordinates and place', async () => {
const user = await mongoose.models.User.create({ email: 'loc2@test.com' });
const updated = await methods.updateUserLocation(user._id.toString(), {
enabled: true,
source: 'auto',
place: 'Paris, Île-de-France, France',
coordinates: { latitude: 48.85, longitude: 2.35 },
timezone: 'Europe/Paris',
});
expect(updated?.personalization?.location?.place).toBe('Paris, Île-de-France, France');
expect(updated?.personalization?.location?.coordinates?.latitude).toBe(48.85);
});
it('returns null for a missing user', async () => {
const result = await methods.updateUserLocation(new mongoose.Types.ObjectId().toString(), {
enabled: false,
});
expect(result).toBeNull();
});
});
describe('findUsers with options', () => {
beforeEach(async () => {
await User.create([

View file

@ -106,6 +106,10 @@ export function createUserMethods(mongoose: typeof import('mongoose')): {
action: 'install' | 'uninstall',
) => Promise<IUser | null>;
toggleUserMemories: (userId: string, memoriesEnabled: boolean) => Promise<IUser | null>;
updateUserLocation: (
userId: string,
location: import('librechat-data-provider').TUserLocation,
) => Promise<IUser | null>;
} {
/**
* Normalizes email fields in search criteria to lowercase and trimmed.
@ -338,6 +342,33 @@ export function createUserMethods(mongoose: typeof import('mongoose')): {
}).lean<IUser>();
}
/**
* Update a user's location personalization setting.
* Creates the personalization object if it doesn't exist.
*/
async function updateUserLocation(
userId: string,
location: import('librechat-data-provider').TUserLocation,
): Promise<IUser | null> {
const User = mongoose.models.User;
const user = await User.findById(userId);
if (!user) {
return null;
}
const updateOperation = {
$set: {
'personalization.location': { ...location, updatedAt: new Date() },
},
};
return await User.findByIdAndUpdate(userId, updateOperation, {
new: true,
runValidators: true,
}).lean<IUser>();
}
/**
* Search for users by pattern matching on name, email, or username (case-insensitive)
* @param searchPattern - The pattern to search for
@ -517,6 +548,7 @@ export function createUserMethods(mongoose: typeof import('mongoose')): {
deleteUserById,
updateUserPlugins,
toggleUserMemories,
updateUserLocation,
};
}

View file

@ -133,6 +133,24 @@ const userSchema: Schema<IUser> = new Schema<IUser>(
type: Boolean,
default: true,
},
location: {
type: {
enabled: { type: Boolean, default: false },
source: { type: String, enum: ['auto', 'manual'] },
manual: { type: String, maxlength: 256 },
place: { type: String, maxlength: 256 },
coordinates: {
type: {
latitude: { type: Number },
longitude: { type: Number },
},
default: undefined,
},
timezone: { type: String, maxlength: 64 },
updatedAt: { type: Date },
},
default: undefined,
},
},
default: {},
},

View file

@ -50,6 +50,7 @@ export interface IUser extends Document {
termsAccepted?: boolean;
personalization?: {
memories?: boolean;
location?: import('librechat-data-provider').TUserLocation;
};
favorites?: TUserFavorite[];
/** Per-skill active/inactive overrides. Key = skillId, value = active state. */
@ -95,6 +96,7 @@ export interface UpdateUserRequest {
termsAccepted?: boolean;
personalization?: {
memories?: boolean;
location?: import('librechat-data-provider').TUserLocation;
};
skillStates?: Record<string, boolean>;
}