1
0
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:
Gastón Fournier 2025-09-24 15:23:46 +02:00
parent efdfb67c9f
commit 3ed342459a
No known key found for this signature in database
GPG Key ID: AF45428626E17A8E
6 changed files with 100 additions and 2 deletions

View File

@ -68,6 +68,7 @@ import { UniqueConnectionReadModel } from '../features/unique-connection/unique-
import { FeatureLinkStore } from '../features/feature-links/feature-link-store.js'; import { FeatureLinkStore } from '../features/feature-links/feature-link-store.js';
import { UnknownFlagsStore } from '../features/metrics/unknown-flags/unknown-flags-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 { FeatureLinksReadModel } from '../features/feature-links/feature-links-read-model.js';
import { UserUpdatesReadModel } from '../features/users/user-updates-read-model.js';
export const createStores = ( export const createStores = (
config: IUnleashConfig, config: IUnleashConfig,
@ -106,6 +107,7 @@ export const createStores = (
), ),
settingStore: new SettingStore(db, getLogger), settingStore: new SettingStore(db, getLogger),
userStore: new UserStore(db, getLogger), userStore: new UserStore(db, getLogger),
userUpdatesReadModel: new UserUpdatesReadModel(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),

View File

@ -12,9 +12,10 @@ import type { Db } from '../../db/db.js';
import type { Knex } from 'knex'; import type { Knex } from 'knex';
const TABLE = 'users'; const TABLE = 'users';
export const USERS_TABLE = TABLE;
const PASSWORD_HASH_TABLE = 'used_passwords'; const PASSWORD_HASH_TABLE = 'used_passwords';
const USER_COLUMNS_PUBLIC = [ export const USER_COLUMNS_PUBLIC = [
'id', 'id',
'name', 'name',
'username', 'username',
@ -43,7 +44,7 @@ const mapUserToColumns = (user: ICreateUser) => ({
image_url: user.imageUrl, image_url: user.imageUrl,
}); });
const rowToUser = (row) => { export const rowToUser = (row) => {
if (!row) { if (!row) {
throw new NotFoundError('No user found'); throw new NotFoundError('No user found');
} }

View 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);
}
}

View File

@ -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 { IFeatureLinkStore } from '../features/feature-links/feature-link-store-type.js';
import type { IUnknownFlagsStore } from '../features/metrics/unknown-flags/unknown-flags-store.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 { 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 { export interface IUnleashStores {
accessStore: IAccessStore; accessStore: IAccessStore;
@ -90,6 +91,7 @@ export interface IUnleashStores {
tagTypeStore: ITagTypeStore; tagTypeStore: ITagTypeStore;
userFeedbackStore: IUserFeedbackStore; userFeedbackStore: IUserFeedbackStore;
userStore: IUserStore; userStore: IUserStore;
userUpdatesReadModel: UserUpdatesReadModel;
userSplashStore: IUserSplashStore; userSplashStore: IUserSplashStore;
roleStore: IRoleStore; roleStore: IRoleStore;
segmentStore: ISegmentStore; segmentStore: ISegmentStore;

View 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,
);
};

View File

@ -65,6 +65,7 @@ import { UniqueConnectionReadModel } from '../../lib/features/unique-connection/
import FakeFeatureLinkStore from '../../lib/features/feature-links/fake-feature-link-store.js'; 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 { 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 { 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 = { const db = {
select: () => ({ select: () => ({
@ -92,6 +93,7 @@ const createStores: () => IUnleashStores = () => {
addonStore: new FakeAddonStore(), addonStore: new FakeAddonStore(),
projectStore: new FakeProjectStore(), projectStore: new FakeProjectStore(),
userStore: new FakeUserStore(), userStore: new FakeUserStore(),
userUpdatesReadModel: {} as UserUpdatesReadModel,
accessStore: new FakeAccessStore(), accessStore: new FakeAccessStore(),
accountStore: new FakeAccountStore(), accountStore: new FakeAccountStore(),
userFeedbackStore: new FakeUserFeedbackStore(), userFeedbackStore: new FakeUserFeedbackStore(),