mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-24 17:51:14 +02:00
feat: add users updated read model
This commit is contained in:
parent
efdfb67c9f
commit
3ed342459a
@ -68,6 +68,7 @@ import { UniqueConnectionReadModel } from '../features/unique-connection/unique-
|
||||
import { FeatureLinkStore } from '../features/feature-links/feature-link-store.js';
|
||||
import { UnknownFlagsStore } from '../features/metrics/unknown-flags/unknown-flags-store.js';
|
||||
import { FeatureLinksReadModel } from '../features/feature-links/feature-links-read-model.js';
|
||||
import { UserUpdatesReadModel } from '../features/users/user-updates-read-model.js';
|
||||
|
||||
export const createStores = (
|
||||
config: IUnleashConfig,
|
||||
@ -106,6 +107,7 @@ export const createStores = (
|
||||
),
|
||||
settingStore: new SettingStore(db, getLogger),
|
||||
userStore: new UserStore(db, getLogger),
|
||||
userUpdatesReadModel: new UserUpdatesReadModel(db, getLogger),
|
||||
accountStore: new AccountStore(db, getLogger),
|
||||
projectStore: new ProjectStore(db, eventBus, config),
|
||||
tagStore: new TagStore(db, eventBus, getLogger),
|
||||
|
@ -12,9 +12,10 @@ import type { Db } from '../../db/db.js';
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
const TABLE = 'users';
|
||||
export const USERS_TABLE = TABLE;
|
||||
const PASSWORD_HASH_TABLE = 'used_passwords';
|
||||
|
||||
const USER_COLUMNS_PUBLIC = [
|
||||
export const USER_COLUMNS_PUBLIC = [
|
||||
'id',
|
||||
'name',
|
||||
'username',
|
||||
@ -43,7 +44,7 @@ const mapUserToColumns = (user: ICreateUser) => ({
|
||||
image_url: user.imageUrl,
|
||||
});
|
||||
|
||||
const rowToUser = (row) => {
|
||||
export const rowToUser = (row) => {
|
||||
if (!row) {
|
||||
throw new NotFoundError('No user found');
|
||||
}
|
||||
|
45
src/lib/features/users/user-updates-read-model.ts
Normal file
45
src/lib/features/users/user-updates-read-model.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type { Logger, LogProvider } from '../../logger.js';
|
||||
|
||||
import type { Db } from '../../db/db.js';
|
||||
import { rowToUser, USER_COLUMNS_PUBLIC, USERS_TABLE } from './user-store.js';
|
||||
import type { User } from '../../internals.js';
|
||||
|
||||
export class UserUpdatesReadModel {
|
||||
private db: Db;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(db: Db, getLogger: LogProvider) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('user-updates-read-model.ts');
|
||||
}
|
||||
|
||||
async getLastUpdatedAt(): Promise<Date | null> {
|
||||
const result = await this.db(USERS_TABLE)
|
||||
.where({
|
||||
// also consider deleted users (different than activeUsers query)
|
||||
is_system: false,
|
||||
is_service: false,
|
||||
})
|
||||
.max('updated_at as last_updated_at')
|
||||
.first();
|
||||
return result ? result.last_updated_at : null;
|
||||
}
|
||||
|
||||
async getUsersUpdatedAfter(
|
||||
date: Date,
|
||||
limit: number = 100,
|
||||
): Promise<User[]> {
|
||||
const result = await this.db(USERS_TABLE)
|
||||
.where({
|
||||
// also consider deleted users (different than activeUsers query)
|
||||
is_system: false,
|
||||
is_service: false,
|
||||
updated_at: { $gt: date },
|
||||
})
|
||||
.orderBy('updated_at', 'asc')
|
||||
.select(USER_COLUMNS_PUBLIC)
|
||||
.limit(limit);
|
||||
return result.map(rowToUser);
|
||||
}
|
||||
}
|
@ -62,6 +62,7 @@ import { ReleasePlanMilestoneStrategyStore } from '../features/release-plans/rel
|
||||
import type { IFeatureLinkStore } from '../features/feature-links/feature-link-store-type.js';
|
||||
import type { IUnknownFlagsStore } from '../features/metrics/unknown-flags/unknown-flags-store.js';
|
||||
import type { IFeatureLinksReadModel } from '../features/feature-links/feature-links-read-model-type.js';
|
||||
import type { UserUpdatesReadModel } from '../features/users/user-updates-read-model.js';
|
||||
|
||||
export interface IUnleashStores {
|
||||
accessStore: IAccessStore;
|
||||
@ -90,6 +91,7 @@ export interface IUnleashStores {
|
||||
tagTypeStore: ITagTypeStore;
|
||||
userFeedbackStore: IUserFeedbackStore;
|
||||
userStore: IUserStore;
|
||||
userUpdatesReadModel: UserUpdatesReadModel;
|
||||
userSplashStore: IUserSplashStore;
|
||||
roleStore: IRoleStore;
|
||||
segmentStore: ISegmentStore;
|
||||
|
46
src/migrations/20250923130348-add-users-updated-at.js
Normal file
46
src/migrations/20250923130348-add-users-updated-at.js
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
exports.up = (db, callback) => {
|
||||
db.runSql(
|
||||
`
|
||||
ALTER TABLE users ADD COLUMN updated_at timestamptz NOT NULL DEFAULT now();
|
||||
|
||||
-- Backfill existing rows using max(created_at, deleted_at)
|
||||
UPDATE users
|
||||
SET updated_at = COALESCE(
|
||||
GREATEST(created_at, deleted_at),
|
||||
created_at,
|
||||
now()
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_users_updated_at()
|
||||
RETURNS trigger AS $unleash_user_updated_at_fn$
|
||||
BEGIN
|
||||
NEW.updated_at := now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$unleash_user_updated_at_fn$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_set_users_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_users_updated_at();
|
||||
|
||||
-- create an index only for non-system and non-service users
|
||||
CREATE INDEX idx_users_only_updated_at_desc ON users (updated_at DESC)
|
||||
WHERE is_system = false AND is_service = false;
|
||||
`,
|
||||
callback,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = (db, callback) => {
|
||||
db.runSql(
|
||||
`
|
||||
DROP INDEX IF EXISTS idx_users_only_updated_at_desc;
|
||||
DROP TRIGGER IF EXISTS trg_set_users_updated_at ON users;
|
||||
DROP FUNCTION IF EXISTS set_users_updated_at();
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS updated_at;
|
||||
`,
|
||||
callback,
|
||||
);
|
||||
};
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -65,6 +65,7 @@ import { UniqueConnectionReadModel } from '../../lib/features/unique-connection/
|
||||
import FakeFeatureLinkStore from '../../lib/features/feature-links/fake-feature-link-store.js';
|
||||
import { FakeFeatureLinksReadModel } from '../../lib/features/feature-links/fake-feature-links-read-model.js';
|
||||
import { FakeUnknownFlagsStore } from '../../lib/features/metrics/unknown-flags/fake-unknown-flags-store.js';
|
||||
import type { UserUpdatesReadModel } from '../../lib/features/users/user-updates-read-model.js';
|
||||
|
||||
const db = {
|
||||
select: () => ({
|
||||
@ -92,6 +93,7 @@ const createStores: () => IUnleashStores = () => {
|
||||
addonStore: new FakeAddonStore(),
|
||||
projectStore: new FakeProjectStore(),
|
||||
userStore: new FakeUserStore(),
|
||||
userUpdatesReadModel: {} as UserUpdatesReadModel,
|
||||
accessStore: new FakeAccessStore(),
|
||||
accountStore: new FakeAccountStore(),
|
||||
userFeedbackStore: new FakeUserFeedbackStore(),
|
||||
|
Loading…
Reference in New Issue
Block a user