refactor: session repository (#15957)

This commit is contained in:
Jason Rasmussen 2025-02-07 18:16:40 -05:00 committed by GitHub
parent d7d4d22fe0
commit 758449e9f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 38 additions and 57 deletions

View File

@ -1,4 +1,4 @@
import { SessionEntity } from 'src/entities/session.entity'; import { SessionItem } from 'src/types';
export class SessionResponseDto { export class SessionResponseDto {
id!: string; id!: string;
@ -9,7 +9,7 @@ export class SessionResponseDto {
deviceOS!: string; deviceOS!: string;
} }
export const mapSession = (entity: SessionEntity, currentId?: string): SessionResponseDto => ({ export const mapSession = (entity: SessionItem, currentId?: string): SessionResponseDto => ({
id: entity.id, id: entity.id,
createdAt: entity.createdAt.toISOString(), createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(), updatedAt: entity.updatedAt.toISOString(),

View File

@ -1,17 +0,0 @@
import { Insertable, Updateable } from 'kysely';
import { Sessions } from 'src/db';
import { SessionEntity } from 'src/entities/session.entity';
export const ISessionRepository = 'ISessionRepository';
type E = SessionEntity;
export type SessionSearchOptions = { updatedBefore: Date };
export interface ISessionRepository {
search(options: SessionSearchOptions): Promise<SessionEntity[]>;
create(dto: Insertable<Sessions>): Promise<SessionEntity>;
update(id: string, dto: Updateable<Sessions>): Promise<SessionEntity>;
delete(id: string): Promise<void>;
getByToken(token: string): Promise<E | undefined>;
getByUserId(userId: string): Promise<E[]>;
}

View File

@ -10,7 +10,6 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface'; import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
@ -72,6 +71,7 @@ export const repositories = [
NotificationRepository, NotificationRepository,
OAuthRepository, OAuthRepository,
ProcessRepository, ProcessRepository,
SessionRepository,
ServerInfoRepository, ServerInfoRepository,
SystemMetadataRepository, SystemMetadataRepository,
TelemetryRepository, TelemetryRepository,
@ -93,7 +93,6 @@ export const providers = [
{ provide: IPartnerRepository, useClass: PartnerRepository }, { provide: IPartnerRepository, useClass: PartnerRepository },
{ provide: IPersonRepository, useClass: PersonRepository }, { provide: IPersonRepository, useClass: PersonRepository },
{ provide: ISearchRepository, useClass: SearchRepository }, { provide: ISearchRepository, useClass: SearchRepository },
{ provide: ISessionRepository, useClass: SessionRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: IStackRepository, useClass: StackRepository }, { provide: IStackRepository, useClass: StackRepository },
{ provide: IStorageRepository, useClass: StorageRepository }, { provide: IStorageRepository, useClass: StorageRepository },

View File

@ -3,36 +3,37 @@ import { Insertable, Kysely, Updateable } from 'kysely';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { DB, Sessions } from 'src/db'; import { DB, Sessions } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators'; import { DummyValue, GenerateSql } from 'src/decorators';
import { SessionEntity, withUser } from 'src/entities/session.entity'; import { withUser } from 'src/entities/session.entity';
import { ISessionRepository, SessionSearchOptions } from 'src/interfaces/session.interface';
import { asUuid } from 'src/utils/database'; import { asUuid } from 'src/utils/database';
export type SessionSearchOptions = { updatedBefore: Date };
@Injectable() @Injectable()
export class SessionRepository implements ISessionRepository { export class SessionRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {} constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] }) @GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] })
search(options: SessionSearchOptions): Promise<SessionEntity[]> { search(options: SessionSearchOptions) {
return this.db return this.db
.selectFrom('sessions') .selectFrom('sessions')
.selectAll() .selectAll()
.where('sessions.updatedAt', '<=', options.updatedBefore) .where('sessions.updatedAt', '<=', options.updatedBefore)
.execute() as Promise<SessionEntity[]>; .execute();
} }
@GenerateSql({ params: [DummyValue.STRING] }) @GenerateSql({ params: [DummyValue.STRING] })
getByToken(token: string): Promise<SessionEntity | undefined> { getByToken(token: string) {
return this.db return this.db
.selectFrom('sessions') .selectFrom('sessions')
.innerJoinLateral(withUser, (join) => join.onTrue()) .innerJoinLateral(withUser, (join) => join.onTrue())
.selectAll('sessions') .selectAll('sessions')
.select((eb) => eb.fn.toJson('user').as('user')) .select((eb) => eb.fn.toJson('user').as('user'))
.where('sessions.token', '=', token) .where('sessions.token', '=', token)
.executeTakeFirst() as Promise<SessionEntity | undefined>; .executeTakeFirst();
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
getByUserId(userId: string): Promise<SessionEntity[]> { getByUserId(userId: string) {
return this.db return this.db
.selectFrom('sessions') .selectFrom('sessions')
.innerJoinLateral(withUser, (join) => join.onTrue()) .innerJoinLateral(withUser, (join) => join.onTrue())
@ -41,30 +42,24 @@ export class SessionRepository implements ISessionRepository {
.where('sessions.userId', '=', userId) .where('sessions.userId', '=', userId)
.orderBy('sessions.updatedAt', 'desc') .orderBy('sessions.updatedAt', 'desc')
.orderBy('sessions.createdAt', 'desc') .orderBy('sessions.createdAt', 'desc')
.execute() as unknown as Promise<SessionEntity[]>; .execute();
} }
async create(dto: Insertable<Sessions>): Promise<SessionEntity> { create(dto: Insertable<Sessions>) {
const { id, token, userId, createdAt, updatedAt, deviceType, deviceOS } = await this.db return this.db.insertInto('sessions').values(dto).returningAll().executeTakeFirstOrThrow();
.insertInto('sessions')
.values(dto)
.returningAll()
.executeTakeFirstOrThrow();
return { id, token, userId, createdAt, updatedAt, deviceType, deviceOS } as SessionEntity;
} }
update(id: string, dto: Updateable<Sessions>): Promise<SessionEntity> { update(id: string, dto: Updateable<Sessions>) {
return this.db return this.db
.updateTable('sessions') .updateTable('sessions')
.set(dto) .set(dto)
.where('sessions.id', '=', asUuid(id)) .where('sessions.id', '=', asUuid(id))
.returningAll() .returningAll()
.executeTakeFirstOrThrow() as Promise<SessionEntity>; .executeTakeFirstOrThrow();
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
async delete(id: string): Promise<void> { async delete(id: string) {
await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute(); await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute();
} }
} }

View File

@ -5,11 +5,10 @@ import { UserEntity } from 'src/entities/user.entity';
import { AuthType, Permission } from 'src/enum'; import { AuthType, Permission } from 'src/enum';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { AuthService } from 'src/services/auth.service'; import { AuthService } from 'src/services/auth.service';
import { IApiKeyRepository, IOAuthRepository, ISystemMetadataRepository } from 'src/types'; import { IApiKeyRepository, IOAuthRepository, ISessionRepository, ISystemMetadataRepository } from 'src/types';
import { keyStub } from 'test/fixtures/api-key.stub'; import { keyStub } from 'test/fixtures/api-key.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub'; import { sessionStub } from 'test/fixtures/session.stub';
@ -257,7 +256,7 @@ describe('AuthService', () => {
it('should validate using authorization header', async () => { it('should validate using authorization header', async () => {
userMock.get.mockResolvedValue(userStub.user1); userMock.get.mockResolvedValue(userStub.user1);
sessionMock.getByToken.mockResolvedValue(sessionStub.valid); sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
await expect( await expect(
sut.authenticate({ sut.authenticate({
headers: { authorization: 'Bearer auth_token' }, headers: { authorization: 'Bearer auth_token' },
@ -362,7 +361,7 @@ describe('AuthService', () => {
}); });
it('should return an auth dto', async () => { it('should return an auth dto', async () => {
sessionMock.getByToken.mockResolvedValue(sessionStub.valid); sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
await expect( await expect(
sut.authenticate({ sut.authenticate({
headers: { cookie: 'immich_access_token=auth_token' }, headers: { cookie: 'immich_access_token=auth_token' },
@ -376,7 +375,7 @@ describe('AuthService', () => {
}); });
it('should throw if admin route and not an admin', async () => { it('should throw if admin route and not an admin', async () => {
sessionMock.getByToken.mockResolvedValue(sessionStub.valid); sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
await expect( await expect(
sut.authenticate({ sut.authenticate({
headers: { cookie: 'immich_access_token=auth_token' }, headers: { cookie: 'immich_access_token=auth_token' },
@ -387,7 +386,7 @@ describe('AuthService', () => {
}); });
it('should update when access time exceeds an hour', async () => { it('should update when access time exceeds an hour', async () => {
sessionMock.getByToken.mockResolvedValue(sessionStub.inactive); sessionMock.getByToken.mockResolvedValue(sessionStub.inactive as any);
sessionMock.update.mockResolvedValue(sessionStub.valid); sessionMock.update.mockResolvedValue(sessionStub.valid);
await expect( await expect(
sut.authenticate({ sut.authenticate({

View File

@ -17,6 +17,7 @@ import {
mapLoginResponse, mapLoginResponse,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { SessionEntity } from 'src/entities/session.entity';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum'; import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum';
import { OAuthProfile } from 'src/repositories/oauth.repository'; import { OAuthProfile } from 'src/repositories/oauth.repository';
@ -338,7 +339,7 @@ export class AuthService extends BaseService {
await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date() }); await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date() });
} }
return { user: session.user, session }; return { user: session.user as unknown as UserEntity, session: session as unknown as SessionEntity };
} }
throw new UnauthorizedException('Invalid user token'); throw new UnauthorizedException('Invalid user token');

View File

@ -18,7 +18,6 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface'; import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
@ -40,6 +39,7 @@ import { NotificationRepository } from 'src/repositories/notification.repository
import { OAuthRepository } from 'src/repositories/oauth.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ProcessRepository } from 'src/repositories/process.repository'; import { ProcessRepository } from 'src/repositories/process.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository'; import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository'; import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository'; import { TrashRepository } from 'src/repositories/trash.repository';
@ -80,7 +80,7 @@ export class BaseService {
protected processRepository: ProcessRepository, protected processRepository: ProcessRepository,
@Inject(ISearchRepository) protected searchRepository: ISearchRepository, @Inject(ISearchRepository) protected searchRepository: ISearchRepository,
protected serverInfoRepository: ServerInfoRepository, protected serverInfoRepository: ServerInfoRepository,
@Inject(ISessionRepository) protected sessionRepository: ISessionRepository, protected sessionRepository: SessionRepository,
@Inject(ISharedLinkRepository) protected sharedLinkRepository: ISharedLinkRepository, @Inject(ISharedLinkRepository) protected sharedLinkRepository: ISharedLinkRepository,
@Inject(IStackRepository) protected stackRepository: IStackRepository, @Inject(IStackRepository) protected stackRepository: IStackRepository,
@Inject(IStorageRepository) protected storageRepository: IStorageRepository, @Inject(IStorageRepository) protected storageRepository: IStorageRepository,

View File

@ -1,7 +1,6 @@
import { UserEntity } from 'src/entities/user.entity';
import { JobStatus } from 'src/interfaces/job.interface'; import { JobStatus } from 'src/interfaces/job.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { SessionService } from 'src/services/session.service'; import { SessionService } from 'src/services/session.service';
import { ISessionRepository } from 'src/types';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub'; import { sessionStub } from 'test/fixtures/session.stub';
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
@ -38,7 +37,6 @@ describe('SessionService', () => {
deviceType: '', deviceType: '',
id: '123', id: '123',
token: '420', token: '420',
user: {} as UserEntity,
userId: '42', userId: '42',
}, },
]); ]);
@ -50,7 +48,7 @@ describe('SessionService', () => {
describe('getAll', () => { describe('getAll', () => {
it('should get the devices', async () => { it('should get the devices', async () => {
sessionMock.getByUserId.mockResolvedValue([sessionStub.valid, sessionStub.inactive]); sessionMock.getByUserId.mockResolvedValue([sessionStub.valid as any, sessionStub.inactive]);
await expect(sut.getAll(authStub.user1)).resolves.toEqual([ await expect(sut.getAll(authStub.user1)).resolves.toEqual([
{ {
createdAt: '2021-01-01T00:00:00.000Z', createdAt: '2021-01-01T00:00:00.000Z',
@ -76,7 +74,7 @@ describe('SessionService', () => {
describe('logoutDevices', () => { describe('logoutDevices', () => {
it('should logout all devices', async () => { it('should logout all devices', async () => {
sessionMock.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid]); sessionMock.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid] as any[]);
await sut.deleteAll(authStub.user1); await sut.deleteAll(authStub.user1);

View File

@ -16,6 +16,7 @@ import { NotificationRepository } from 'src/repositories/notification.repository
import { OAuthRepository } from 'src/repositories/oauth.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ProcessRepository } from 'src/repositories/process.repository'; import { ProcessRepository } from 'src/repositories/process.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository'; import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { MetricGroupRepository, TelemetryRepository } from 'src/repositories/telemetry.repository'; import { MetricGroupRepository, TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository'; import { TrashRepository } from 'src/repositories/trash.repository';
@ -61,6 +62,7 @@ export type IMetricGroupRepository = RepositoryInterface<MetricGroupRepository>;
export type INotificationRepository = RepositoryInterface<NotificationRepository>; export type INotificationRepository = RepositoryInterface<NotificationRepository>;
export type IOAuthRepository = RepositoryInterface<OAuthRepository>; export type IOAuthRepository = RepositoryInterface<OAuthRepository>;
export type IProcessRepository = RepositoryInterface<ProcessRepository>; export type IProcessRepository = RepositoryInterface<ProcessRepository>;
export type ISessionRepository = RepositoryInterface<SessionRepository>;
export type IServerInfoRepository = RepositoryInterface<ServerInfoRepository>; export type IServerInfoRepository = RepositoryInterface<ServerInfoRepository>;
export type ISystemMetadataRepository = RepositoryInterface<SystemMetadataRepository>; export type ISystemMetadataRepository = RepositoryInterface<SystemMetadataRepository>;
export type ITelemetryRepository = RepositoryInterface<TelemetryRepository>; export type ITelemetryRepository = RepositoryInterface<TelemetryRepository>;
@ -81,6 +83,8 @@ export type MemoryItem =
| Awaited<ReturnType<IMemoryRepository['create']>> | Awaited<ReturnType<IMemoryRepository['create']>>
| Awaited<ReturnType<IMemoryRepository['search']>>[0]; | Awaited<ReturnType<IMemoryRepository['search']>>[0];
export type SessionItem = Awaited<ReturnType<ISessionRepository['getByUserId']>>[0];
export interface CropOptions { export interface CropOptions {
top: number; top: number;
left: number; left: number;

View File

@ -1,4 +1,4 @@
import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISessionRepository } from 'src/types';
import { Mocked, vitest } from 'vitest'; import { Mocked, vitest } from 'vitest';
export const newSessionRepositoryMock = (): Mocked<ISessionRepository> => { export const newSessionRepositoryMock = (): Mocked<ISessionRepository> => {

View File

@ -17,6 +17,7 @@ import { NotificationRepository } from 'src/repositories/notification.repository
import { OAuthRepository } from 'src/repositories/oauth.repository'; import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ProcessRepository } from 'src/repositories/process.repository'; import { ProcessRepository } from 'src/repositories/process.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository'; import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository'; import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository'; import { TrashRepository } from 'src/repositories/trash.repository';
@ -39,6 +40,7 @@ import {
IOAuthRepository, IOAuthRepository,
IProcessRepository, IProcessRepository,
IServerInfoRepository, IServerInfoRepository,
ISessionRepository,
ISystemMetadataRepository, ISystemMetadataRepository,
ITrashRepository, ITrashRepository,
IVersionHistoryRepository, IVersionHistoryRepository,
@ -170,7 +172,7 @@ export const newTestService = <T extends BaseService>(
processMock as IProcessRepository as ProcessRepository, processMock as IProcessRepository as ProcessRepository,
searchMock, searchMock,
serverInfoMock as IServerInfoRepository as ServerInfoRepository, serverInfoMock as IServerInfoRepository as ServerInfoRepository,
sessionMock, sessionMock as ISessionRepository as SessionRepository,
sharedLinkMock, sharedLinkMock,
stackMock, stackMock,
storageMock, storageMock,