1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: ability to query users with pagination (#10130)

Made a few QoL improvements:
- Don't use default export for class
- Move users store to a feature package (didn't move the interface as it
might be referenced elsewhere)
- Add types for query builders (and ts-expect-error when needed)
This commit is contained in:
Gastón Fournier 2025-06-12 17:43:22 +02:00 committed by GitHub
parent 4e48d90ed8
commit 37a125f0b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 49 additions and 28 deletions

View File

@ -15,7 +15,7 @@ import ClientInstanceStore from './client-instance-store.js';
import ClientApplicationsStore from './client-applications-store.js';
import ContextFieldStore from '../features/context/context-field-store.js';
import SettingStore from './setting-store.js';
import UserStore from './user-store.js';
import { UserStore } from '../features/users/user-store.js';
import ProjectStore from '../features/project/project-store.js';
import TagStore from './tag-store.js';
import TagTypeStore from '../features/tag-type/tag-type-store.js';
@ -105,7 +105,7 @@ export const createStores = (
config.flagResolver,
),
settingStore: new SettingStore(db, getLogger),
userStore: new UserStore(db, getLogger, config.flagResolver),
userStore: new UserStore(db, getLogger),
accountStore: new AccountStore(db, getLogger),
projectStore: new ProjectStore(db, eventBus, config),
tagStore: new TagStore(db, eventBus, getLogger),

View File

@ -10,7 +10,7 @@ import {
import type { IUnleashConfig } from '../../types/index.js';
import type { Db } from '../../db/db.js';
import FeatureToggleStore from '../feature-toggle/feature-toggle-store.js';
import UserStore from '../../db/user-store.js';
import { UserStore } from '../users/user-store.js';
import ProjectStore from '../project/project-store.js';
import EnvironmentStore from '../project-environments/environment-store.js';
import StrategyStore from '../../db/strategy-store.js';
@ -57,7 +57,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
getLogger,
flagResolver,
);
const userStore = new UserStore(db, getLogger, flagResolver);
const userStore = new UserStore(db, getLogger);
const projectStore = new ProjectStore(db, eventBus, config);
const environmentStore = new EnvironmentStore(db, eventBus, config);
const strategyStore = new StrategyStore(db, getLogger);

View File

@ -3,7 +3,7 @@ import type { Db } from '../../db/db.js';
import { OnboardingService } from './onboarding-service.js';
import { OnboardingStore } from './onboarding-store.js';
import { ProjectReadModel } from '../project/project-read-model.js';
import UserStore from '../../db/user-store.js';
import { UserStore } from '../users/user-store.js';
import FakeUserStore from '../../../test/fixtures/fake-user-store.js';
import { FakeProjectReadModel } from '../project/fake-project-read-model.js';
import { FakeOnboardingStore } from './fake-onboarding-store.js';
@ -18,7 +18,7 @@ export const createOnboardingService =
eventBus,
flagResolver,
);
const userStore = new UserStore(db, getLogger, flagResolver);
const userStore = new UserStore(db, getLogger);
const onboardingService = new OnboardingService(
{
onboardingStore,

View File

@ -1,17 +1,15 @@
/* eslint camelcase: "off" */
import type { Logger, LogProvider } from '../../logger.js';
import User from '../../types/user.js';
import type { Logger, LogProvider } from '../logger.js';
import User from '../types/user.js';
import NotFoundError from '../error/notfound-error.js';
import NotFoundError from '../../error/notfound-error.js';
import type {
ICreateUser,
IUserLookup,
IUserStore,
IUserUpdateFields,
} from '../types/stores/user-store.js';
import type { Db } from './db.js';
import type { IFlagResolver } from '../types/index.js';
} from '../../types/stores/user-store.js';
import type { Db } from '../../db/db.js';
import type { Knex } from 'knex';
const TABLE = 'users';
const PASSWORD_HASH_TABLE = 'used_passwords';
@ -63,17 +61,14 @@ const rowToUser = (row) => {
});
};
class UserStore implements IUserStore {
export class UserStore implements IUserStore {
private db: Db;
private logger: Logger;
private flagResolver: IFlagResolver;
constructor(db: Db, getLogger: LogProvider, flagResolver: IFlagResolver) {
constructor(db: Db, getLogger: LogProvider) {
this.db = db;
this.logger = getLogger('user-store.ts');
this.flagResolver = flagResolver;
}
async getPasswordsPreviouslyUsed(userId: number): Promise<string[]> {
@ -130,7 +125,7 @@ class UserStore implements IUserStore {
return this.insert(user);
}
buildSelectUser(q: IUserLookup): any {
buildSelectUser(q: IUserLookup): Knex.QueryBuilder<User> {
const query = this.activeAll();
if (q.id) {
return query.where('id', q.id);
@ -144,13 +139,13 @@ class UserStore implements IUserStore {
throw new Error('Can only find users with id, username or email.');
}
activeAll(): any {
activeAll(): Knex.QueryBuilder<User> {
return this.db(TABLE).where({
deleted_at: null,
});
}
activeUsers(): any {
activeUsers(): Knex.QueryBuilder<User> {
return this.db(TABLE).where({
deleted_at: null,
is_service: false,
@ -164,9 +159,25 @@ class UserStore implements IUserStore {
return item ? item.id : undefined;
}
async getAll(): Promise<User[]> {
const users = await this.activeUsers().select(USER_COLUMNS);
return users.map(rowToUser);
async getAll(params?: {
limit: number;
offset: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}): Promise<User[]> {
const usersQuery = this.activeUsers().select(USER_COLUMNS);
if (params) {
if (params.sortBy) {
usersQuery.orderBy(params.sortBy, params.sortOrder);
}
if (params.limit) {
usersQuery.limit(params.limit);
}
if (params.offset) {
usersQuery.offset(params.offset);
}
}
return (await usersQuery).map(rowToUser);
}
async search(query: string): Promise<User[]> {
@ -191,11 +202,13 @@ class UserStore implements IUserStore {
}
async delete(id: number): Promise<void> {
return this.activeUsers()
await this.activeUsers()
.where({ id })
.update({
deleted_at: new Date(),
// @ts-expect-error email is non-nullable in User type
email: null,
// @ts-expect-error username is non-nullable in User type
username: null,
scim_id: null,
scim_external_id: null,
@ -221,6 +234,7 @@ class UserStore implements IUserStore {
disallowNPreviousPasswords: number,
): Promise<void> {
await this.activeUsers().where('id', userId).update({
// @ts-expect-error password_hash does not exist in User type
password_hash: passwordHash,
});
// We apparently set this to null, but you should be allowed to have null, so need to allow this
@ -237,12 +251,13 @@ class UserStore implements IUserStore {
}
async incLoginAttempts(user: User): Promise<void> {
return this.buildSelectUser(user).increment('login_attempts', 1);
await this.buildSelectUser(user).increment('login_attempts', 1);
}
async successfullyLogin(user: User): Promise<number> {
const currentDate = new Date();
const updateQuery = this.buildSelectUser(user).update({
// @ts-expect-error login_attempts does not exist in User type
login_attempts: 0,
seen_at: currentDate,
});
@ -262,6 +277,7 @@ class UserStore implements IUserStore {
firstLoginOrder = countEarlierUsers;
await updateQuery.update({
// @ts-expect-error first_seen_at does not exist in User type
first_seen_at: currentDate,
});
}
@ -333,4 +349,3 @@ class UserStore implements IUserStore {
return firstInstanceUser ? firstInstanceUser.created_at : null;
}
}
export default UserStore;

View File

@ -25,6 +25,12 @@ export interface IUserStore extends Store<IUser, number> {
upsert(user: ICreateUser): Promise<IUser>;
hasUser(idQuery: IUserLookup): Promise<number | undefined>;
search(query: string): Promise<IUser[]>;
getAll(params?: {
limit: number;
offset: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}): Promise<IUser[]>;
getAllWithId(userIdList: number[]): Promise<IUser[]>;
getByQuery(idQuery: IUserLookup): Promise<IUser>;
getPasswordHash(userId: number): Promise<string>;