mirror of
https://github.com/immich-app/immich.git
synced 2025-01-23 12:12:45 +01:00
refactor: migrate sessions repository to kysely (#15268)
* wip: search * wip: getByToken * wip: getByToken * wip: getByUserId * wip: create/update/delete * remove unused code * clean up and pr feedback * fix: test * fix: e2e test * pr feedback
This commit is contained in:
parent
36eef9807b
commit
79726acc72
7 changed files with 185 additions and 96 deletions
e2e/src/api/specs
server/src
entities
interfaces
queries
repositories
services
|
@ -129,6 +129,8 @@ describe('/users', () => {
|
|||
expect(body).toEqual({
|
||||
...before,
|
||||
updatedAt: expect.any(String),
|
||||
profileChangedAt: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
name: 'Name',
|
||||
});
|
||||
});
|
||||
|
@ -177,6 +179,8 @@ describe('/users', () => {
|
|||
...before,
|
||||
email: 'non-admin@immich.cloud',
|
||||
updatedAt: expect.anything(),
|
||||
createdAt: expect.anything(),
|
||||
profileChangedAt: expect.anything(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { ExpressionBuilder } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
|
@ -27,3 +29,37 @@ export class SessionEntity {
|
|||
@Column({ default: '' })
|
||||
deviceOS!: string;
|
||||
}
|
||||
|
||||
const userColumns = [
|
||||
'id',
|
||||
'email',
|
||||
'createdAt',
|
||||
'profileImagePath',
|
||||
'isAdmin',
|
||||
'shouldChangePassword',
|
||||
'deletedAt',
|
||||
'oauthId',
|
||||
'updatedAt',
|
||||
'storageLabel',
|
||||
'name',
|
||||
'quotaSizeInBytes',
|
||||
'quotaUsageInBytes',
|
||||
'status',
|
||||
'profileChangedAt',
|
||||
] as const;
|
||||
|
||||
export const withUser = (eb: ExpressionBuilder<DB, 'sessions'>) => {
|
||||
return eb
|
||||
.selectFrom('users')
|
||||
.select(userColumns)
|
||||
.select((eb) =>
|
||||
eb
|
||||
.selectFrom('user_metadata')
|
||||
.whereRef('users.id', '=', 'user_metadata.userId')
|
||||
.select((eb) => eb.fn('array_agg', [eb.table('user_metadata')]).as('metadata'))
|
||||
.as('metadata'),
|
||||
)
|
||||
.whereRef('users.id', '=', 'sessions.userId')
|
||||
.where('users.deletedAt', 'is', null)
|
||||
.as('user');
|
||||
};
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { Insertable, Updateable } from 'kysely';
|
||||
import { Sessions } from 'src/db';
|
||||
import { SessionEntity } from 'src/entities/session.entity';
|
||||
|
||||
export const ISessionRepository = 'ISessionRepository';
|
||||
|
@ -7,9 +9,9 @@ export type SessionSearchOptions = { updatedBefore: Date };
|
|||
|
||||
export interface ISessionRepository {
|
||||
search(options: SessionSearchOptions): Promise<SessionEntity[]>;
|
||||
create<T extends Partial<E>>(dto: T): Promise<T>;
|
||||
update<T extends Partial<E>>(dto: T): Promise<T>;
|
||||
create(dto: Insertable<Sessions>): Promise<SessionEntity>;
|
||||
update(id: string, dto: Updateable<Sessions>): Promise<SessionEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
getByToken(token: string): Promise<E | null>;
|
||||
getByToken(token: string): Promise<E | undefined>;
|
||||
getByUserId(userId: string): Promise<E[]>;
|
||||
}
|
||||
|
|
|
@ -1,64 +1,97 @@
|
|||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- SessionRepository.search
|
||||
SELECT
|
||||
"SessionEntity"."id" AS "SessionEntity_id",
|
||||
"SessionEntity"."userId" AS "SessionEntity_userId",
|
||||
"SessionEntity"."createdAt" AS "SessionEntity_createdAt",
|
||||
"SessionEntity"."updatedAt" AS "SessionEntity_updatedAt",
|
||||
"SessionEntity"."deviceType" AS "SessionEntity_deviceType",
|
||||
"SessionEntity"."deviceOS" AS "SessionEntity_deviceOS"
|
||||
FROM
|
||||
"sessions" "SessionEntity"
|
||||
WHERE
|
||||
(("SessionEntity"."updatedAt" <= $1))
|
||||
select
|
||||
*
|
||||
from
|
||||
"sessions"
|
||||
where
|
||||
"sessions"."updatedAt" <= $1
|
||||
|
||||
-- SessionRepository.getByToken
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."SessionEntity_id" AS "ids_SessionEntity_id"
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
"SessionEntity"."id" AS "SessionEntity_id",
|
||||
"SessionEntity"."userId" AS "SessionEntity_userId",
|
||||
"SessionEntity"."createdAt" AS "SessionEntity_createdAt",
|
||||
"SessionEntity"."updatedAt" AS "SessionEntity_updatedAt",
|
||||
"SessionEntity"."deviceType" AS "SessionEntity_deviceType",
|
||||
"SessionEntity"."deviceOS" AS "SessionEntity_deviceOS",
|
||||
"SessionEntity__SessionEntity_user"."id" AS "SessionEntity__SessionEntity_user_id",
|
||||
"SessionEntity__SessionEntity_user"."name" AS "SessionEntity__SessionEntity_user_name",
|
||||
"SessionEntity__SessionEntity_user"."isAdmin" AS "SessionEntity__SessionEntity_user_isAdmin",
|
||||
"SessionEntity__SessionEntity_user"."email" AS "SessionEntity__SessionEntity_user_email",
|
||||
"SessionEntity__SessionEntity_user"."storageLabel" AS "SessionEntity__SessionEntity_user_storageLabel",
|
||||
"SessionEntity__SessionEntity_user"."oauthId" AS "SessionEntity__SessionEntity_user_oauthId",
|
||||
"SessionEntity__SessionEntity_user"."profileImagePath" AS "SessionEntity__SessionEntity_user_profileImagePath",
|
||||
"SessionEntity__SessionEntity_user"."shouldChangePassword" AS "SessionEntity__SessionEntity_user_shouldChangePassword",
|
||||
"SessionEntity__SessionEntity_user"."createdAt" AS "SessionEntity__SessionEntity_user_createdAt",
|
||||
"SessionEntity__SessionEntity_user"."deletedAt" AS "SessionEntity__SessionEntity_user_deletedAt",
|
||||
"SessionEntity__SessionEntity_user"."status" AS "SessionEntity__SessionEntity_user_status",
|
||||
"SessionEntity__SessionEntity_user"."updatedAt" AS "SessionEntity__SessionEntity_user_updatedAt",
|
||||
"SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes",
|
||||
"SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes",
|
||||
"SessionEntity__SessionEntity_user"."profileChangedAt" AS "SessionEntity__SessionEntity_user_profileChangedAt",
|
||||
"469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."userId" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_userId",
|
||||
"469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."key" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_key",
|
||||
"469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."value" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_value"
|
||||
FROM
|
||||
"sessions" "SessionEntity"
|
||||
LEFT JOIN "users" "SessionEntity__SessionEntity_user" ON "SessionEntity__SessionEntity_user"."id" = "SessionEntity"."userId"
|
||||
AND (
|
||||
"SessionEntity__SessionEntity_user"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "user_metadata" "469e6aa7ff79eff78f8441f91ba15bb07d3634dd" ON "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."userId" = "SessionEntity__SessionEntity_user"."id"
|
||||
WHERE
|
||||
(("SessionEntity"."token" = $1))
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"SessionEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
"sessions".*,
|
||||
to_json("user") as "user"
|
||||
from
|
||||
"sessions"
|
||||
inner join lateral (
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt",
|
||||
(
|
||||
select
|
||||
array_agg("user_metadata") as "metadata"
|
||||
from
|
||||
"user_metadata"
|
||||
where
|
||||
"users"."id" = "user_metadata"."userId"
|
||||
) as "metadata"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "sessions"."userId"
|
||||
and "users"."deletedAt" is null
|
||||
) as "user" on true
|
||||
where
|
||||
"sessions"."token" = $1
|
||||
|
||||
-- SessionRepository.getByUserId
|
||||
select
|
||||
"sessions".*,
|
||||
to_json("user") as "user"
|
||||
from
|
||||
"sessions"
|
||||
inner join lateral (
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt",
|
||||
(
|
||||
select
|
||||
array_agg("user_metadata") as "metadata"
|
||||
from
|
||||
"user_metadata"
|
||||
where
|
||||
"users"."id" = "user_metadata"."userId"
|
||||
) as "metadata"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "sessions"."userId"
|
||||
and "users"."deletedAt" is null
|
||||
) as "user" on true
|
||||
where
|
||||
"sessions"."userId" = $1
|
||||
order by
|
||||
"sessions"."updatedAt" desc,
|
||||
"sessions"."createdAt" desc
|
||||
|
||||
-- SessionRepository.delete
|
||||
DELETE FROM "sessions"
|
||||
WHERE
|
||||
"id" = $1
|
||||
delete from "sessions"
|
||||
where
|
||||
"id" = $1::uuid
|
||||
|
|
|
@ -1,56 +1,70 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB, Sessions } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { SessionEntity } from 'src/entities/session.entity';
|
||||
import { SessionEntity, withUser } from 'src/entities/session.entity';
|
||||
import { ISessionRepository, SessionSearchOptions } from 'src/interfaces/session.interface';
|
||||
import { LessThanOrEqual, Repository } from 'typeorm';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
@Injectable()
|
||||
export class SessionRepository implements ISessionRepository {
|
||||
constructor(@InjectRepository(SessionEntity) private repository: Repository<SessionEntity>) {}
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.DATE] })
|
||||
@GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] })
|
||||
search(options: SessionSearchOptions): Promise<SessionEntity[]> {
|
||||
return this.repository.find({ where: { updatedAt: LessThanOrEqual(options.updatedBefore) } });
|
||||
return this.db
|
||||
.selectFrom('sessions')
|
||||
.selectAll()
|
||||
.where('sessions.updatedAt', '<=', options.updatedBefore)
|
||||
.execute() as Promise<SessionEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getByToken(token: string): Promise<SessionEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: { token },
|
||||
relations: {
|
||||
user: {
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
getByToken(token: string): Promise<SessionEntity | undefined> {
|
||||
return this.db
|
||||
.selectFrom('sessions')
|
||||
.innerJoinLateral(withUser, (join) => join.onTrue())
|
||||
.selectAll('sessions')
|
||||
.select((eb) => eb.fn.toJson('user').as('user'))
|
||||
.where('sessions.token', '=', token)
|
||||
.executeTakeFirst() as Promise<SessionEntity | undefined>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getByUserId(userId: string): Promise<SessionEntity[]> {
|
||||
return this.repository.find({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
relations: {
|
||||
user: true,
|
||||
},
|
||||
order: {
|
||||
updatedAt: 'desc',
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
return this.db
|
||||
.selectFrom('sessions')
|
||||
.innerJoinLateral(withUser, (join) => join.onTrue())
|
||||
.selectAll('sessions')
|
||||
.select((eb) => eb.fn.toJson('user').as('user'))
|
||||
.where('sessions.userId', '=', userId)
|
||||
.orderBy('sessions.updatedAt', 'desc')
|
||||
.orderBy('sessions.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<SessionEntity[]>;
|
||||
}
|
||||
|
||||
create<T extends Partial<SessionEntity>>(dto: T): Promise<T & { id: string }> {
|
||||
return this.repository.save(dto);
|
||||
async create(dto: Insertable<Sessions>): Promise<SessionEntity> {
|
||||
const { id, token, userId, createdAt, updatedAt, deviceType, deviceOS } = await this.db
|
||||
.insertInto('sessions')
|
||||
.values(dto)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return { id, token, userId, createdAt, updatedAt, deviceType, deviceOS } as SessionEntity;
|
||||
}
|
||||
|
||||
update<T extends Partial<SessionEntity>>(dto: T): Promise<T> {
|
||||
return this.repository.save(dto);
|
||||
update(id: string, dto: Updateable<Sessions>): Promise<SessionEntity> {
|
||||
return this.db
|
||||
.updateTable('sessions')
|
||||
.set(dto)
|
||||
.where('sessions.id', '=', asUuid(id))
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as Promise<SessionEntity>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repository.delete({ id });
|
||||
await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -354,7 +354,7 @@ describe('AuthService', () => {
|
|||
|
||||
describe('validate - user token', () => {
|
||||
it('should throw if no token is found', async () => {
|
||||
sessionMock.getByToken.mockResolvedValue(null);
|
||||
sessionMock.getByToken.mockResolvedValue(void 0);
|
||||
await expect(
|
||||
sut.authenticate({
|
||||
headers: { 'x-immich-user-token': 'auth_token' },
|
||||
|
@ -399,7 +399,7 @@ describe('AuthService', () => {
|
|||
metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' },
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) });
|
||||
expect(sessionMock.update.mock.calls[0][1]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -331,7 +331,7 @@ export class AuthService extends BaseService {
|
|||
const updatedAt = DateTime.fromJSDate(session.updatedAt);
|
||||
const diff = now.diff(updatedAt, ['hours']);
|
||||
if (diff.hours > 1) {
|
||||
await this.sessionRepository.update({ id: session.id, updatedAt: new Date() });
|
||||
await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date() });
|
||||
}
|
||||
|
||||
return { user: session.user, session };
|
||||
|
@ -346,9 +346,9 @@ export class AuthService extends BaseService {
|
|||
|
||||
await this.sessionRepository.create({
|
||||
token,
|
||||
user,
|
||||
deviceOS: loginDetails.deviceOS,
|
||||
deviceType: loginDetails.deviceType,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return mapLoginResponse(user, key);
|
||||
|
|
Loading…
Reference in a new issue