mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-28 17:55:15 +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:
parent
4e48d90ed8
commit
37a125f0b5
@ -15,7 +15,7 @@ import ClientInstanceStore from './client-instance-store.js';
|
|||||||
import ClientApplicationsStore from './client-applications-store.js';
|
import ClientApplicationsStore from './client-applications-store.js';
|
||||||
import ContextFieldStore from '../features/context/context-field-store.js';
|
import ContextFieldStore from '../features/context/context-field-store.js';
|
||||||
import SettingStore from './setting-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 ProjectStore from '../features/project/project-store.js';
|
||||||
import TagStore from './tag-store.js';
|
import TagStore from './tag-store.js';
|
||||||
import TagTypeStore from '../features/tag-type/tag-type-store.js';
|
import TagTypeStore from '../features/tag-type/tag-type-store.js';
|
||||||
@ -105,7 +105,7 @@ export const createStores = (
|
|||||||
config.flagResolver,
|
config.flagResolver,
|
||||||
),
|
),
|
||||||
settingStore: new SettingStore(db, getLogger),
|
settingStore: new SettingStore(db, getLogger),
|
||||||
userStore: new UserStore(db, getLogger, config.flagResolver),
|
userStore: new UserStore(db, getLogger),
|
||||||
accountStore: new AccountStore(db, getLogger),
|
accountStore: new AccountStore(db, getLogger),
|
||||||
projectStore: new ProjectStore(db, eventBus, config),
|
projectStore: new ProjectStore(db, eventBus, config),
|
||||||
tagStore: new TagStore(db, eventBus, getLogger),
|
tagStore: new TagStore(db, eventBus, getLogger),
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
import type { IUnleashConfig } from '../../types/index.js';
|
import type { IUnleashConfig } from '../../types/index.js';
|
||||||
import type { Db } from '../../db/db.js';
|
import type { Db } from '../../db/db.js';
|
||||||
import FeatureToggleStore from '../feature-toggle/feature-toggle-store.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 ProjectStore from '../project/project-store.js';
|
||||||
import EnvironmentStore from '../project-environments/environment-store.js';
|
import EnvironmentStore from '../project-environments/environment-store.js';
|
||||||
import StrategyStore from '../../db/strategy-store.js';
|
import StrategyStore from '../../db/strategy-store.js';
|
||||||
@ -57,7 +57,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
|
|||||||
getLogger,
|
getLogger,
|
||||||
flagResolver,
|
flagResolver,
|
||||||
);
|
);
|
||||||
const userStore = new UserStore(db, getLogger, flagResolver);
|
const userStore = new UserStore(db, getLogger);
|
||||||
const projectStore = new ProjectStore(db, eventBus, config);
|
const projectStore = new ProjectStore(db, eventBus, config);
|
||||||
const environmentStore = new EnvironmentStore(db, eventBus, config);
|
const environmentStore = new EnvironmentStore(db, eventBus, config);
|
||||||
const strategyStore = new StrategyStore(db, getLogger);
|
const strategyStore = new StrategyStore(db, getLogger);
|
||||||
|
@ -3,7 +3,7 @@ import type { Db } from '../../db/db.js';
|
|||||||
import { OnboardingService } from './onboarding-service.js';
|
import { OnboardingService } from './onboarding-service.js';
|
||||||
import { OnboardingStore } from './onboarding-store.js';
|
import { OnboardingStore } from './onboarding-store.js';
|
||||||
import { ProjectReadModel } from '../project/project-read-model.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 FakeUserStore from '../../../test/fixtures/fake-user-store.js';
|
||||||
import { FakeProjectReadModel } from '../project/fake-project-read-model.js';
|
import { FakeProjectReadModel } from '../project/fake-project-read-model.js';
|
||||||
import { FakeOnboardingStore } from './fake-onboarding-store.js';
|
import { FakeOnboardingStore } from './fake-onboarding-store.js';
|
||||||
@ -18,7 +18,7 @@ export const createOnboardingService =
|
|||||||
eventBus,
|
eventBus,
|
||||||
flagResolver,
|
flagResolver,
|
||||||
);
|
);
|
||||||
const userStore = new UserStore(db, getLogger, flagResolver);
|
const userStore = new UserStore(db, getLogger);
|
||||||
const onboardingService = new OnboardingService(
|
const onboardingService = new OnboardingService(
|
||||||
{
|
{
|
||||||
onboardingStore,
|
onboardingStore,
|
||||||
|
@ -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 NotFoundError from '../../error/notfound-error.js';
|
||||||
import User from '../types/user.js';
|
|
||||||
|
|
||||||
import NotFoundError from '../error/notfound-error.js';
|
|
||||||
import type {
|
import type {
|
||||||
ICreateUser,
|
ICreateUser,
|
||||||
IUserLookup,
|
IUserLookup,
|
||||||
IUserStore,
|
IUserStore,
|
||||||
IUserUpdateFields,
|
IUserUpdateFields,
|
||||||
} from '../types/stores/user-store.js';
|
} from '../../types/stores/user-store.js';
|
||||||
import type { Db } from './db.js';
|
import type { Db } from '../../db/db.js';
|
||||||
import type { IFlagResolver } from '../types/index.js';
|
import type { Knex } from 'knex';
|
||||||
|
|
||||||
const TABLE = 'users';
|
const TABLE = 'users';
|
||||||
const PASSWORD_HASH_TABLE = 'used_passwords';
|
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 db: Db;
|
||||||
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
private flagResolver: IFlagResolver;
|
constructor(db: Db, getLogger: LogProvider) {
|
||||||
|
|
||||||
constructor(db: Db, getLogger: LogProvider, flagResolver: IFlagResolver) {
|
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.logger = getLogger('user-store.ts');
|
this.logger = getLogger('user-store.ts');
|
||||||
this.flagResolver = flagResolver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPasswordsPreviouslyUsed(userId: number): Promise<string[]> {
|
async getPasswordsPreviouslyUsed(userId: number): Promise<string[]> {
|
||||||
@ -130,7 +125,7 @@ class UserStore implements IUserStore {
|
|||||||
return this.insert(user);
|
return this.insert(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSelectUser(q: IUserLookup): any {
|
buildSelectUser(q: IUserLookup): Knex.QueryBuilder<User> {
|
||||||
const query = this.activeAll();
|
const query = this.activeAll();
|
||||||
if (q.id) {
|
if (q.id) {
|
||||||
return query.where('id', 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.');
|
throw new Error('Can only find users with id, username or email.');
|
||||||
}
|
}
|
||||||
|
|
||||||
activeAll(): any {
|
activeAll(): Knex.QueryBuilder<User> {
|
||||||
return this.db(TABLE).where({
|
return this.db(TABLE).where({
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
activeUsers(): any {
|
activeUsers(): Knex.QueryBuilder<User> {
|
||||||
return this.db(TABLE).where({
|
return this.db(TABLE).where({
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
is_service: false,
|
is_service: false,
|
||||||
@ -164,9 +159,25 @@ class UserStore implements IUserStore {
|
|||||||
return item ? item.id : undefined;
|
return item ? item.id : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(): Promise<User[]> {
|
async getAll(params?: {
|
||||||
const users = await this.activeUsers().select(USER_COLUMNS);
|
limit: number;
|
||||||
return users.map(rowToUser);
|
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[]> {
|
async search(query: string): Promise<User[]> {
|
||||||
@ -191,11 +202,13 @@ class UserStore implements IUserStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: number): Promise<void> {
|
async delete(id: number): Promise<void> {
|
||||||
return this.activeUsers()
|
await this.activeUsers()
|
||||||
.where({ id })
|
.where({ id })
|
||||||
.update({
|
.update({
|
||||||
deleted_at: new Date(),
|
deleted_at: new Date(),
|
||||||
|
// @ts-expect-error email is non-nullable in User type
|
||||||
email: null,
|
email: null,
|
||||||
|
// @ts-expect-error username is non-nullable in User type
|
||||||
username: null,
|
username: null,
|
||||||
scim_id: null,
|
scim_id: null,
|
||||||
scim_external_id: null,
|
scim_external_id: null,
|
||||||
@ -221,6 +234,7 @@ class UserStore implements IUserStore {
|
|||||||
disallowNPreviousPasswords: number,
|
disallowNPreviousPasswords: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.activeUsers().where('id', userId).update({
|
await this.activeUsers().where('id', userId).update({
|
||||||
|
// @ts-expect-error password_hash does not exist in User type
|
||||||
password_hash: passwordHash,
|
password_hash: passwordHash,
|
||||||
});
|
});
|
||||||
// We apparently set this to null, but you should be allowed to have null, so need to allow this
|
// 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> {
|
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> {
|
async successfullyLogin(user: User): Promise<number> {
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
const updateQuery = this.buildSelectUser(user).update({
|
const updateQuery = this.buildSelectUser(user).update({
|
||||||
|
// @ts-expect-error login_attempts does not exist in User type
|
||||||
login_attempts: 0,
|
login_attempts: 0,
|
||||||
seen_at: currentDate,
|
seen_at: currentDate,
|
||||||
});
|
});
|
||||||
@ -262,6 +277,7 @@ class UserStore implements IUserStore {
|
|||||||
firstLoginOrder = countEarlierUsers;
|
firstLoginOrder = countEarlierUsers;
|
||||||
|
|
||||||
await updateQuery.update({
|
await updateQuery.update({
|
||||||
|
// @ts-expect-error first_seen_at does not exist in User type
|
||||||
first_seen_at: currentDate,
|
first_seen_at: currentDate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -333,4 +349,3 @@ class UserStore implements IUserStore {
|
|||||||
return firstInstanceUser ? firstInstanceUser.created_at : null;
|
return firstInstanceUser ? firstInstanceUser.created_at : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default UserStore;
|
|
@ -25,6 +25,12 @@ export interface IUserStore extends Store<IUser, number> {
|
|||||||
upsert(user: ICreateUser): Promise<IUser>;
|
upsert(user: ICreateUser): Promise<IUser>;
|
||||||
hasUser(idQuery: IUserLookup): Promise<number | undefined>;
|
hasUser(idQuery: IUserLookup): Promise<number | undefined>;
|
||||||
search(query: string): Promise<IUser[]>;
|
search(query: string): Promise<IUser[]>;
|
||||||
|
getAll(params?: {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
}): Promise<IUser[]>;
|
||||||
getAllWithId(userIdList: number[]): Promise<IUser[]>;
|
getAllWithId(userIdList: number[]): Promise<IUser[]>;
|
||||||
getByQuery(idQuery: IUserLookup): Promise<IUser>;
|
getByQuery(idQuery: IUserLookup): Promise<IUser>;
|
||||||
getPasswordHash(userId: number): Promise<string>;
|
getPasswordHash(userId: number): Promise<string>;
|
||||||
|
Loading…
Reference in New Issue
Block a user