From a927f1f42bc14a8f14fa95d0a6f208760eb4ebf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Fri, 3 Oct 2025 09:24:53 +0200 Subject: [PATCH] chore: new method that considers users updated at the same time (#10723) We could have users updated at the exact same time, so we need to include that timestamp in the next fetch to avoid missing users, but also include the latest user id so we can exclude the ones already fetched. The following test shows how to process pages with this new method: https://github.com/Unleash/unleash/blob/c03df86ee006f531024137a1cfc846eb282ab672/src/lib/features/users/user-updates-read-model.test.ts#L39-L65 --- .../users/user-updates-read-model.test.ts | 65 +++++++++++++++++++ .../features/users/user-updates-read-model.ts | 18 ++++- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/lib/features/users/user-updates-read-model.test.ts diff --git a/src/lib/features/users/user-updates-read-model.test.ts b/src/lib/features/users/user-updates-read-model.test.ts new file mode 100644 index 0000000000..f2e1b00a76 --- /dev/null +++ b/src/lib/features/users/user-updates-read-model.test.ts @@ -0,0 +1,65 @@ +import dbInit, { + type ITestDb, +} from '../../../test/e2e/helpers/database-init.js'; + +let db: ITestDb; +const TABLE = 'users'; +const INSERT_INSTANT = new Date(); +beforeAll(async () => { + db = await dbInit(); + + for (let i = 0; i < 10; i += 1) { + await db.rawDatabase(TABLE).insert({ + name: `test-user-${i}`, + username: `test-user-${i}`, + email: `test-user-${i}@example.com`, + created_at: INSERT_INSTANT, + updated_at: INSERT_INSTANT, + }); + } +}); + +test('returns all users if page is large enough', async () => { + const userUpdatesReadModel = db.stores.userUpdatesReadModel; + + const users = + await userUpdatesReadModel.getUsersUpdatedAfterOrEqual(INSERT_INSTANT); + expect(users).toHaveLength(10); +}); + +test('returns 0 if no users updated after timestamp', async () => { + const userUpdatesReadModel = db.stores.userUpdatesReadModel; + + const users = await userUpdatesReadModel.getUsersUpdatedAfterOrEqual( + new Date(INSERT_INSTANT.getTime() + 1), + ); + expect(users).toHaveLength(0); +}); + +test('returns the users in pages', async () => { + const userUpdatesReadModel = db.stores.userUpdatesReadModel; + const pageSize = 4; + + const usersPage1 = await userUpdatesReadModel.getUsersUpdatedAfterOrEqual( + INSERT_INSTANT, + pageSize, + ); + expect(usersPage1).toHaveLength(4); + expect(usersPage1[0].username).toBe('test-user-0'); + + const usersPage2 = await userUpdatesReadModel.getUsersUpdatedAfterOrEqual( + INSERT_INSTANT, + pageSize, + usersPage1[usersPage1.length - 1].id, + ); + expect(usersPage2).toHaveLength(4); + expect(usersPage2[0].username).toBe('test-user-4'); + + const usersPage3 = await userUpdatesReadModel.getUsersUpdatedAfterOrEqual( + INSERT_INSTANT, + pageSize, + usersPage2[usersPage2.length - 1].id, + ); + expect(usersPage3).toHaveLength(2); + expect(usersPage3[1].username).toBe('test-user-9'); +}); diff --git a/src/lib/features/users/user-updates-read-model.ts b/src/lib/features/users/user-updates-read-model.ts index 7191b60e35..4444b45e8c 100644 --- a/src/lib/features/users/user-updates-read-model.ts +++ b/src/lib/features/users/user-updates-read-model.ts @@ -50,9 +50,18 @@ export class UserUpdatesReadModel { return result ? result.last_updated_at : null; } + /** @deprecated */ async getUsersUpdatedAfter( date: Date, limit: number = 100, + ): Promise { + return this.getUsersUpdatedAfterOrEqual(date, limit, 0); + } + + async getUsersUpdatedAfterOrEqual( + date: Date, + limit: number = 100, + afterId: number = 0, ): Promise { const result = await this.db(USERS_TABLE) .where({ @@ -60,8 +69,15 @@ export class UserUpdatesReadModel { is_system: false, is_service: false, }) - .where('updated_at', '>', date) + .where((builder) => { + builder.where('updated_at', '>', date).orWhere((subBuilder) => { + subBuilder + .where('updated_at', '=', date) + .where('id', '>', afterId); + }); + }) .orderBy('updated_at', 'asc') + .orderBy('id', 'asc') .select([ ...USER_COLUMNS_PUBLIC, 'created_at',