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 { 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),
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
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 { 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;
|
||||||
|
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 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(),
|
||||||
|
Loading…
Reference in New Issue
Block a user