redis pt 2: read cache wiring & event based invalidations

This commit is contained in:
2026-03-30 18:18:46 +08:00
parent c2bafc5bb1
commit d12d3e1ec7
14 changed files with 475 additions and 8 deletions

View File

@@ -13,6 +13,7 @@ import {
verify, verify,
} from 'jsonwebtoken'; } from 'jsonwebtoken';
import { PrismaService } from '../database/prisma.service'; import { PrismaService } from '../database/prisma.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import type { SocialAuthProfile } from './types/social-auth-profile'; import type { SocialAuthProfile } from './types/social-auth-profile';
import type { import type {
AuthTokens, AuthTokens,
@@ -35,6 +36,7 @@ import {
usernameFromEmail, usernameFromEmail,
} from './auth.utils'; } from './auth.utils';
import type { SsoProvider } from './dto/sso-provider'; import type { SsoProvider } from './dto/sso-provider';
import { UserEvents } from '../users/events/user.events';
interface SsoStateClaims { interface SsoStateClaims {
provider: SsoProvider; provider: SsoProvider;
@@ -56,6 +58,7 @@ export class AuthService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly eventEmitter: EventEmitter2,
) { ) {
this.jwtSecret = this.configService.get<string>('JWT_SECRET') || ''; this.jwtSecret = this.configService.get<string>('JWT_SECRET') || '';
this.jwtIssuer = this.jwtIssuer =
@@ -254,6 +257,10 @@ export class AuthService {
}, },
}); });
this.eventEmitter.emit(UserEvents.SEARCH_INDEX_INVALIDATED, {
userId: user.id,
});
return user; return user;
} }
@@ -273,7 +280,7 @@ export class AuthService {
); );
} }
return this.prisma.$transaction(async (tx) => { const user = await this.prisma.$transaction(async (tx) => {
let user = await tx.user.findUnique({ let user = await tx.user.findUnique({
where: { email }, where: { email },
}); });
@@ -311,6 +318,12 @@ export class AuthService {
return user; return user;
}); });
this.eventEmitter.emit(UserEvents.SEARCH_INDEX_INVALIDATED, {
userId: user.id,
});
return user;
} }
private async resolveUsername( private async resolveUsername(

61
src/common/cache/cache-keys.ts vendored Normal file
View File

@@ -0,0 +1,61 @@
const EMPTY_VALUE_TOKEN = '_';
export const CACHE_NAMESPACE = {
FRIENDS_LIST: 'friends-list',
DOLLS_LIST: 'dolls-list',
USERS_SEARCH: 'users-search',
} as const;
function normalizeKeyPart(value: string | undefined): string {
if (!value) {
return EMPTY_VALUE_TOKEN;
}
return encodeURIComponent(value);
}
export const CACHE_TTL_SECONDS = {
FRIENDS_LIST: 30,
DOLLS_LIST: 30,
USERS_SEARCH: 20,
} as const;
export function friendsListCacheKey(userId: string): string {
return normalizeKeyPart(userId);
}
export function friendsListOwnerTag(userId: string): string {
return `owner:${normalizeKeyPart(userId)}`;
}
export function friendsListDependsOnUserTag(userId: string): string {
return `depends-on:${normalizeKeyPart(userId)}`;
}
export function dollsListCacheKey(
ownerId: string,
requesterId: string,
): string {
return `${normalizeKeyPart(ownerId)}:${normalizeKeyPart(requesterId)}`;
}
export function dollsListOwnerTag(ownerId: string): string {
return `owner:${normalizeKeyPart(ownerId)}`;
}
export function dollsListViewerTag(viewerId: string): string {
return `viewer:${normalizeKeyPart(viewerId)}`;
}
export function usersSearchCacheKey(
username: string | undefined,
excludeUserId: string | undefined,
): string {
return `${normalizeKeyPart(username?.trim().toLowerCase())}:${normalizeKeyPart(excludeUserId)}`;
}
export const USERS_SEARCH_GLOBAL_TAG = 'global';
export function usersSearchUserTag(userId: string): string {
return `user:${normalizeKeyPart(userId)}`;
}

66
src/common/cache/cache-tags.service.ts vendored Normal file
View File

@@ -0,0 +1,66 @@
import { Injectable } from '@nestjs/common';
import { CacheService } from './cache.service';
const CACHE_TAG_SET_TTL_SECONDS = 86_400;
@Injectable()
export class CacheTagsService {
constructor(private readonly cacheService: CacheService) {}
async rememberKeyForTag(
namespace: string,
tag: string,
cacheKey: string,
): Promise<void> {
const redisClient = this.cacheService.getRedisClient();
if (!redisClient) {
return;
}
const tagSetKey = this.getTagSetKey(namespace, tag);
const keyWithNamespace = this.cacheService.getNamespacedKey(
namespace,
cacheKey,
);
try {
await Promise.all([
redisClient.sadd(tagSetKey, keyWithNamespace),
redisClient.expire(tagSetKey, CACHE_TAG_SET_TTL_SECONDS),
]);
} catch (error) {
this.cacheService.recordError('tag remember', tagSetKey, error);
}
}
async invalidateTag(namespace: string, tag: string): Promise<void> {
const redisClient = this.cacheService.getRedisClient();
if (!redisClient) {
return;
}
const tagSetKey = this.getTagSetKey(namespace, tag);
try {
const keys = await redisClient.smembers(tagSetKey);
if (keys.length === 0) {
await redisClient.del(tagSetKey);
return;
}
const pipeline = redisClient.pipeline();
keys.forEach((key) => pipeline.del(key));
pipeline.del(tagSetKey);
await pipeline.exec();
} catch (error) {
this.cacheService.recordError('tag invalidate', tagSetKey, error);
}
}
private getTagSetKey(namespace: string, tag: string): string {
return this.cacheService.getNamespacedKey(
'cache-tag',
`${namespace}:${tag}`,
);
}
}

View File

@@ -1,12 +1,13 @@
import { Global, Module } from '@nestjs/common'; import { Global, Module } from '@nestjs/common';
import { RedisModule } from '../../database/redis.module'; import { RedisModule } from '../../database/redis.module';
import { CacheTagsService } from './cache-tags.service';
import { CacheService } from './cache.service'; import { CacheService } from './cache.service';
import { RedisThrottlerStorage } from './redis-throttler.storage'; import { RedisThrottlerStorage } from './redis-throttler.storage';
@Global() @Global()
@Module({ @Module({
imports: [RedisModule], imports: [RedisModule],
providers: [CacheService, RedisThrottlerStorage], providers: [CacheService, CacheTagsService, RedisThrottlerStorage],
exports: [CacheService, RedisThrottlerStorage], exports: [CacheService, CacheTagsService, RedisThrottlerStorage],
}) })
export class CacheModule {} export class CacheModule {}

View File

@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { CacheTagsService } from '../common/cache/cache-tags.service';
import { CACHE_NAMESPACE, dollsListOwnerTag } from '../common/cache/cache-keys';
import { DollEvents } from './events/doll.events';
import {
type DollCreatedEvent,
type DollDeletedEvent,
type DollUpdatedEvent,
} from './events/doll.events';
@Injectable()
export class DollsCacheInvalidationService {
constructor(private readonly cacheTagsService: CacheTagsService) {}
@OnEvent(DollEvents.DOLL_CREATED)
async handleDollCreated(payload: DollCreatedEvent): Promise<void> {
await this.invalidateOwnerLists(payload.userId);
}
@OnEvent(DollEvents.DOLL_UPDATED)
async handleDollUpdated(payload: DollUpdatedEvent): Promise<void> {
await this.invalidateOwnerLists(payload.userId);
}
@OnEvent(DollEvents.DOLL_DELETED)
async handleDollDeleted(payload: DollDeletedEvent): Promise<void> {
await this.invalidateOwnerLists(payload.userId);
}
private async invalidateOwnerLists(ownerId: string): Promise<void> {
await this.cacheTagsService.invalidateTag(
CACHE_NAMESPACE.DOLLS_LIST,
dollsListOwnerTag(ownerId),
);
}
}

View File

@@ -1,6 +1,7 @@
import { Module, forwardRef } from '@nestjs/common'; import { Module, forwardRef } from '@nestjs/common';
import { DollsService } from './dolls.service'; import { DollsService } from './dolls.service';
import { DollsController } from './dolls.controller'; import { DollsController } from './dolls.controller';
import { DollsCacheInvalidationService } from './dolls-cache-invalidation.service';
import { DollsNotificationService } from './dolls-notification.service'; import { DollsNotificationService } from './dolls-notification.service';
import { DatabaseModule } from '../database/database.module'; import { DatabaseModule } from '../database/database.module';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
@@ -9,7 +10,11 @@ import { WsModule } from '../ws/ws.module';
@Module({ @Module({
imports: [DatabaseModule, AuthModule, forwardRef(() => WsModule)], imports: [DatabaseModule, AuthModule, forwardRef(() => WsModule)],
controllers: [DollsController], controllers: [DollsController],
providers: [DollsService, DollsNotificationService], providers: [
DollsService,
DollsNotificationService,
DollsCacheInvalidationService,
],
exports: [DollsService], exports: [DollsService],
}) })
export class DollsModule {} export class DollsModule {}

View File

@@ -15,6 +15,15 @@ import {
DollUpdatedEvent, DollUpdatedEvent,
DollDeletedEvent, DollDeletedEvent,
} from './events/doll.events'; } from './events/doll.events';
import { CacheService } from '../common/cache/cache.service';
import { CacheTagsService } from '../common/cache/cache-tags.service';
import {
CACHE_NAMESPACE,
CACHE_TTL_SECONDS,
dollsListCacheKey,
dollsListOwnerTag,
dollsListViewerTag,
} from '../common/cache/cache-keys';
@Injectable() @Injectable()
export class DollsService { export class DollsService {
@@ -23,6 +32,8 @@ export class DollsService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly cacheService: CacheService,
private readonly cacheTagsService: CacheTagsService,
) {} ) {}
async getFriendIds(userId: string): Promise<string[]> { async getFriendIds(userId: string): Promise<string[]> {
@@ -76,6 +87,48 @@ export class DollsService {
async listByOwner( async listByOwner(
ownerId: string, ownerId: string,
requestingUserId: string, requestingUserId: string,
): Promise<Doll[]> {
const cacheKey = dollsListCacheKey(ownerId, requestingUserId);
const namespacedKey = this.cacheService.getNamespacedKey(
CACHE_NAMESPACE.DOLLS_LIST,
cacheKey,
);
const cached = await this.cacheService.get(namespacedKey);
if (cached) {
try {
return JSON.parse(cached) as Doll[];
} catch (error) {
this.cacheService.recordError('dolls list parse', namespacedKey, error);
}
}
const dolls = await this.listByOwnerFromDatabase(ownerId, requestingUserId);
await this.cacheService.set(
namespacedKey,
JSON.stringify(dolls),
CACHE_TTL_SECONDS.DOLLS_LIST,
);
await Promise.all([
this.cacheTagsService.rememberKeyForTag(
CACHE_NAMESPACE.DOLLS_LIST,
dollsListOwnerTag(ownerId),
cacheKey,
),
this.cacheTagsService.rememberKeyForTag(
CACHE_NAMESPACE.DOLLS_LIST,
dollsListViewerTag(requestingUserId),
cacheKey,
),
]);
return dolls;
}
private async listByOwnerFromDatabase(
ownerId: string,
requestingUserId: string,
): Promise<Doll[]> { ): Promise<Doll[]> {
// If requesting own dolls, no need to check friendship // If requesting own dolls, no need to check friendship
if (ownerId === requestingUserId) { if (ownerId === requestingUserId) {

View File

@@ -0,0 +1,65 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { CacheTagsService } from '../common/cache/cache-tags.service';
import {
CACHE_NAMESPACE,
dollsListViewerTag,
friendsListDependsOnUserTag,
friendsListOwnerTag,
} from '../common/cache/cache-keys';
import { FriendEvents } from './events/friend.events';
import type {
FriendRequestAcceptedEvent,
UnfriendedEvent,
} from './events/friend.events';
@Injectable()
export class FriendsCacheInvalidationService {
constructor(private readonly cacheTagsService: CacheTagsService) {}
@OnEvent(FriendEvents.REQUEST_ACCEPTED)
async handleFriendAccepted(
payload: FriendRequestAcceptedEvent,
): Promise<void> {
const senderId = payload.friendRequest.senderId;
const receiverId = payload.friendRequest.receiverId;
await this.invalidateFriendAndDollViews(senderId, receiverId);
}
@OnEvent(FriendEvents.UNFRIENDED)
async handleUnfriended(payload: UnfriendedEvent): Promise<void> {
await this.invalidateFriendAndDollViews(payload.userId, payload.friendId);
}
private async invalidateFriendAndDollViews(
firstUserId: string,
secondUserId: string,
): Promise<void> {
await Promise.all([
this.cacheTagsService.invalidateTag(
CACHE_NAMESPACE.FRIENDS_LIST,
friendsListOwnerTag(firstUserId),
),
this.cacheTagsService.invalidateTag(
CACHE_NAMESPACE.FRIENDS_LIST,
friendsListOwnerTag(secondUserId),
),
this.cacheTagsService.invalidateTag(
CACHE_NAMESPACE.FRIENDS_LIST,
friendsListDependsOnUserTag(firstUserId),
),
this.cacheTagsService.invalidateTag(
CACHE_NAMESPACE.FRIENDS_LIST,
friendsListDependsOnUserTag(secondUserId),
),
this.cacheTagsService.invalidateTag(
CACHE_NAMESPACE.DOLLS_LIST,
dollsListViewerTag(firstUserId),
),
this.cacheTagsService.invalidateTag(
CACHE_NAMESPACE.DOLLS_LIST,
dollsListViewerTag(secondUserId),
),
]);
}
}

View File

@@ -1,5 +1,6 @@
import { Module, forwardRef } from '@nestjs/common'; import { Module, forwardRef } from '@nestjs/common';
import { FriendsController } from './friends.controller'; import { FriendsController } from './friends.controller';
import { FriendsCacheInvalidationService } from './friends-cache-invalidation.service';
import { FriendsService } from './friends.service'; import { FriendsService } from './friends.service';
import { FriendsNotificationService } from './friends-notification.service'; import { FriendsNotificationService } from './friends-notification.service';
import { DatabaseModule } from '../database/database.module'; import { DatabaseModule } from '../database/database.module';
@@ -15,7 +16,11 @@ import { WsModule } from '../ws/ws.module';
forwardRef(() => WsModule), forwardRef(() => WsModule),
], ],
controllers: [FriendsController], controllers: [FriendsController],
providers: [FriendsService, FriendsNotificationService], providers: [
FriendsService,
FriendsNotificationService,
FriendsCacheInvalidationService,
],
exports: [FriendsService], exports: [FriendsService],
}) })
export class FriendsModule {} export class FriendsModule {}

View File

@@ -15,6 +15,15 @@ import {
FriendRequestDeniedEvent, FriendRequestDeniedEvent,
UnfriendedEvent, UnfriendedEvent,
} from './events/friend.events'; } from './events/friend.events';
import { CacheService } from '../common/cache/cache.service';
import { CacheTagsService } from '../common/cache/cache-tags.service';
import {
CACHE_NAMESPACE,
CACHE_TTL_SECONDS,
friendsListCacheKey,
friendsListDependsOnUserTag,
friendsListOwnerTag,
} from '../common/cache/cache-keys';
export type FriendRequestWithRelations = FriendRequest & { export type FriendRequestWithRelations = FriendRequest & {
sender: User; sender: User;
@@ -28,6 +37,8 @@ export class FriendsService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly cacheService: CacheService,
private readonly cacheTagsService: CacheTagsService,
) {} ) {}
async sendFriendRequest( async sendFriendRequest(
@@ -272,7 +283,28 @@ export class FriendsService {
} }
async getFriends(userId: string) { async getFriends(userId: string) {
return this.prisma.friendship.findMany({ const cacheKey = friendsListCacheKey(userId);
const namespacedKey = this.cacheService.getNamespacedKey(
CACHE_NAMESPACE.FRIENDS_LIST,
cacheKey,
);
const cached = await this.cacheService.get(namespacedKey);
if (cached) {
try {
return JSON.parse(cached) as Awaited<
ReturnType<PrismaService['friendship']['findMany']>
>;
} catch (error) {
this.cacheService.recordError(
'friends list parse',
namespacedKey,
error,
);
}
}
const friendships = await this.prisma.friendship.findMany({
where: { userId }, where: { userId },
include: { include: {
friend: { friend: {
@@ -285,6 +317,29 @@ export class FriendsService {
createdAt: 'desc', createdAt: 'desc',
}, },
}); });
await this.cacheService.set(
namespacedKey,
JSON.stringify(friendships),
CACHE_TTL_SECONDS.FRIENDS_LIST,
);
const dependentFriendTags = friendships.map((friendship) =>
friendsListDependsOnUserTag(friendship.friendId),
);
const tags = [friendsListOwnerTag(userId), ...dependentFriendTags];
await Promise.all(
tags.map((tag) =>
this.cacheTagsService.rememberKeyForTag(
CACHE_NAMESPACE.FRIENDS_LIST,
tag,
cacheKey,
),
),
);
return friendships;
} }
async unfriend(userId: string, friendId: string): Promise<void> { async unfriend(userId: string, friendId: string): Promise<void> {

View File

@@ -2,6 +2,7 @@ import { Doll } from '@prisma/client';
export const UserEvents = { export const UserEvents = {
ACTIVE_DOLL_CHANGED: 'user.active-doll.changed', ACTIVE_DOLL_CHANGED: 'user.active-doll.changed',
SEARCH_INDEX_INVALIDATED: 'user.search-index.invalidated',
} as const; } as const;
export interface UserActiveDollChangedEvent { export interface UserActiveDollChangedEvent {
@@ -9,3 +10,7 @@ export interface UserActiveDollChangedEvent {
dollId: string | null; dollId: string | null;
doll: Doll | null; doll: Doll | null;
} }
export interface UserSearchIndexInvalidatedEvent {
userId?: string;
}

View File

@@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { CacheTagsService } from '../common/cache/cache-tags.service';
import {
CACHE_NAMESPACE,
USERS_SEARCH_GLOBAL_TAG,
usersSearchUserTag,
} from '../common/cache/cache-keys';
import { UserEvents } from './events/user.events';
import type { UserSearchIndexInvalidatedEvent } from './events/user.events';
@Injectable()
export class UsersCacheInvalidationService {
constructor(private readonly cacheTagsService: CacheTagsService) {}
@OnEvent(UserEvents.SEARCH_INDEX_INVALIDATED)
async handleSearchIndexInvalidation(
payload: UserSearchIndexInvalidatedEvent,
): Promise<void> {
const tasks: Promise<void>[] = [
this.cacheTagsService.invalidateTag(
CACHE_NAMESPACE.USERS_SEARCH,
USERS_SEARCH_GLOBAL_TAG,
),
];
if (payload.userId) {
tasks.push(
this.cacheTagsService.invalidateTag(
CACHE_NAMESPACE.USERS_SEARCH,
usersSearchUserTag(payload.userId),
),
);
}
await Promise.all(tasks);
}
}

View File

@@ -1,5 +1,6 @@
import { Module, forwardRef } from '@nestjs/common'; import { Module, forwardRef } from '@nestjs/common';
import { UsersService } from './users.service'; import { UsersService } from './users.service';
import { UsersCacheInvalidationService } from './users-cache-invalidation.service';
import { UsersController } from './users.controller'; import { UsersController } from './users.controller';
import { UsersNotificationService } from './users-notification.service'; import { UsersNotificationService } from './users-notification.service';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
@@ -16,7 +17,11 @@ import { WsModule } from '../ws/ws.module';
*/ */
@Module({ @Module({
imports: [forwardRef(() => AuthModule), forwardRef(() => WsModule)], imports: [forwardRef(() => AuthModule), forwardRef(() => WsModule)],
providers: [UsersService, UsersNotificationService], providers: [
UsersService,
UsersNotificationService,
UsersCacheInvalidationService,
],
controllers: [UsersController], controllers: [UsersController],
exports: [UsersService], exports: [UsersService],
}) })

View File

@@ -10,6 +10,15 @@ import { User, Prisma } from '@prisma/client';
import type { UpdateUserDto } from './dto/update-user.dto'; import type { UpdateUserDto } from './dto/update-user.dto';
import { UserEvents } from './events/user.events'; import { UserEvents } from './events/user.events';
import { normalizeEmail } from '../auth/auth.utils'; import { normalizeEmail } from '../auth/auth.utils';
import { CacheService } from '../common/cache/cache.service';
import { CacheTagsService } from '../common/cache/cache-tags.service';
import {
CACHE_NAMESPACE,
CACHE_TTL_SECONDS,
usersSearchUserTag,
usersSearchCacheKey,
USERS_SEARCH_GLOBAL_TAG,
} from '../common/cache/cache-keys';
export interface CreateLocalUserDto { export interface CreateLocalUserDto {
email: string; email: string;
@@ -30,6 +39,8 @@ export class UsersService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
private readonly cacheService: CacheService,
private readonly cacheTagsService: CacheTagsService,
) {} ) {}
// Legacy Keycloak user creation removed in favor of local auth. // Legacy Keycloak user creation removed in favor of local auth.
@@ -92,6 +103,8 @@ export class UsersService {
data: updateData, data: updateData,
}); });
this.eventEmitter.emit(UserEvents.SEARCH_INDEX_INVALIDATED, { userId: id });
this.logger.log(`User ${id} profile update requested`); this.logger.log(`User ${id} profile update requested`);
return updatedUser; return updatedUser;
@@ -121,6 +134,8 @@ export class UsersService {
where: { id }, where: { id },
}); });
this.eventEmitter.emit(UserEvents.SEARCH_INDEX_INVALIDATED, { userId: id });
this.logger.log(`User ${id} deleted their account`); this.logger.log(`User ${id} deleted their account`);
} }
@@ -128,6 +143,25 @@ export class UsersService {
username?: string, username?: string,
excludeUserId?: string, excludeUserId?: string,
): Promise<User[]> { ): Promise<User[]> {
const cacheKey = usersSearchCacheKey(username, excludeUserId);
const namespacedKey = this.cacheService.getNamespacedKey(
CACHE_NAMESPACE.USERS_SEARCH,
cacheKey,
);
const cached = await this.cacheService.get(namespacedKey);
if (cached) {
try {
return JSON.parse(cached) as User[];
} catch (error) {
this.cacheService.recordError(
'users search parse',
namespacedKey,
error,
);
}
}
const where: Prisma.UserWhereInput = {}; const where: Prisma.UserWhereInput = {};
if (username) { if (username) {
@@ -151,6 +185,24 @@ export class UsersService {
}, },
}); });
await this.cacheService.set(
namespacedKey,
JSON.stringify(users),
CACHE_TTL_SECONDS.USERS_SEARCH,
);
await this.cacheTagsService.rememberKeyForTag(
CACHE_NAMESPACE.USERS_SEARCH,
USERS_SEARCH_GLOBAL_TAG,
cacheKey,
);
if (excludeUserId) {
await this.cacheTagsService.rememberKeyForTag(
CACHE_NAMESPACE.USERS_SEARCH,
usersSearchUserTag(excludeUserId),
cacheKey,
);
}
return users; return users;
} }
@@ -251,7 +303,7 @@ export class UsersService {
const now = new Date(); const now = new Date();
const roles: string[] = []; const roles: string[] = [];
return this.prisma.user.create({ const user = await this.prisma.user.create({
data: { data: {
email: normalizeEmail(createDto.email), email: normalizeEmail(createDto.email),
name: createDto.name, name: createDto.name,
@@ -262,6 +314,12 @@ export class UsersService {
keycloakSub: null, keycloakSub: null,
} as unknown as Prisma.UserUncheckedCreateInput, } as unknown as Prisma.UserUncheckedCreateInput,
}); });
this.eventEmitter.emit(UserEvents.SEARCH_INDEX_INVALIDATED, {
userId: user.id,
});
return user;
} }
async updatePasswordHash( async updatePasswordHash(