From e43968a1cab2149c3c800d286e9abf5e2acab741 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 10 Jan 2025 15:54:03 -0600 Subject: [PATCH 01/10] wip: search --- server/src/queries/session.repository.sql | 17 ++++++---------- server/src/repositories/session.repository.ts | 20 +++++++++++++++---- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index 2f0613b4d0398..906a97dedcc1e 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -1,17 +1,12 @@ -- 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 diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index 3a0af1ef69d0f..658107f049b3f 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -1,17 +1,29 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { Kysely } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { SessionEntity } from 'src/entities/session.entity'; import { ISessionRepository, SessionSearchOptions } from 'src/interfaces/session.interface'; -import { LessThanOrEqual, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; @Injectable() export class SessionRepository implements ISessionRepository { - constructor(@InjectRepository(SessionEntity) private repository: Repository) {} + constructor( + @InjectRepository(SessionEntity) private repository: Repository, + @InjectKysely() private db: Kysely, + ) {} - @GenerateSql({ params: [DummyValue.DATE] }) + @GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] }) search(options: SessionSearchOptions): Promise { - return this.repository.find({ where: { updatedAt: LessThanOrEqual(options.updatedBefore) } }); + // return this.repository.find({ where: { updatedAt: LessThanOrEqual(options.updatedBefore) } }); + + return this.db + .selectFrom('sessions') + .selectAll() + .where('sessions.updatedAt', '<=', options.updatedBefore) + .execute() as Promise; } @GenerateSql({ params: [DummyValue.STRING] }) From 409e7ba8142d9d903f61650735d190f2811d8575 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 10 Jan 2025 19:22:27 -0600 Subject: [PATCH 02/10] wip: getByToken --- server/src/queries/session.repository.sql | 68 +++++++------------ server/src/repositories/session.repository.ts | 38 ++++++++--- 2 files changed, 56 insertions(+), 50 deletions(-) diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index 906a97dedcc1e..56b8294f777f6 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -9,49 +9,33 @@ where "sessions"."updatedAt" <= $1 -- SessionRepository.getByToken -SELECT DISTINCT - "distinctAlias"."SessionEntity_id" AS "ids_SessionEntity_id" -FROM +select ( - 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 + to_json(obj) + from + ( + select + *, + ( + 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 obj + ) as "user" +from + "sessions" +where + "sessions"."token" = $1 -- SessionRepository.delete DELETE FROM "sessions" diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index 658107f049b3f..cf81f5ee0b133 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Kysely } from 'kysely'; +import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; import { DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; @@ -28,14 +29,35 @@ export class SessionRepository implements ISessionRepository { @GenerateSql({ params: [DummyValue.STRING] }) getByToken(token: string): Promise { - return this.repository.findOne({ - where: { token }, - relations: { - user: { - metadata: true, - }, - }, - }); + // return this.repository.findOne({ + // where: { token }, + // relations: { + // user: { + // metadata: true, + // }, + // }, + // }); + + return this.db + .selectFrom('sessions') + .select((eb) => + jsonObjectFrom( + eb + .selectFrom('users') + .selectAll() + .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'), + ) + .where('sessions.token', '=', token) + .executeTakeFirst() as unknown as Promise; } getByUserId(userId: string): Promise { From 94dd9a19ff80b20b1fcc10f13faa35b8257903d2 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sat, 11 Jan 2025 13:00:16 -0600 Subject: [PATCH 03/10] wip: getByToken --- server/src/queries/session.repository.sql | 49 ++++++++++++------- server/src/repositories/session.repository.ts | 30 +++++++++--- 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index 56b8294f777f6..a826f26ffaa23 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -10,30 +10,41 @@ where -- SessionRepository.getByToken select - ( + "sessions".*, + to_json("user") as "user" +from + "sessions" + inner join lateral ( select - to_json(obj) - from + "id", + "email", + "createdAt", + "profileImagePath", + "isAdmin", + "shouldChangePassword", + "deletedAt", + "oauthId", + "updatedAt", + "storageLabel", + "name", + "quotaSizeInBytes", + "quotaUsageInBytes", + "status", + "profileChangedAt", ( select - *, - ( - select - array_agg("user_metadata") as "metadata" - from - "user_metadata" - where - "users"."id" = "user_metadata"."userId" - ) as "metadata" + array_agg("user_metadata") as "metadata" from - "users" + "user_metadata" where - "users"."id" = "sessions"."userId" - and "users"."deletedAt" is null - ) as obj - ) as "user" -from - "sessions" + "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 diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index cf81f5ee0b133..2d1ed32153898 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Kysely } from 'kysely'; -import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; import { DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; @@ -40,11 +39,27 @@ export class SessionRepository implements ISessionRepository { return this.db .selectFrom('sessions') - .select((eb) => - jsonObjectFrom( + .innerJoinLateral( + (eb) => eb .selectFrom('users') - .selectAll() + .select([ + 'id', + 'email', + 'createdAt', + 'profileImagePath', + 'isAdmin', + 'shouldChangePassword', + 'deletedAt', + 'oauthId', + 'updatedAt', + 'storageLabel', + 'name', + 'quotaSizeInBytes', + 'quotaUsageInBytes', + 'status', + 'profileChangedAt', + ]) .select((eb) => eb .selectFrom('user_metadata') @@ -53,9 +68,12 @@ export class SessionRepository implements ISessionRepository { .as('metadata'), ) .whereRef('users.id', '=', 'sessions.userId') - .where('users.deletedAt', 'is', null), - ).as('user'), + .where('users.deletedAt', 'is', null) + .as('user'), + (join) => join.onTrue(), ) + .selectAll('sessions') + .select((eb) => eb.fn.toJson('user').as('user')) .where('sessions.token', '=', token) .executeTakeFirst() as unknown as Promise; } From 131549901eaed972e372c959e332aff53aa840dd Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sat, 11 Jan 2025 13:19:46 -0600 Subject: [PATCH 04/10] wip: getByUserId --- server/src/queries/session.repository.sql | 43 ++++++++ server/src/repositories/session.repository.ts | 101 ++++++++---------- server/src/services/session.service.ts | 1 + 3 files changed, 88 insertions(+), 57 deletions(-) diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index a826f26ffaa23..b65cd0a59deb1 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -48,6 +48,49 @@ from 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 diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index 2d1ed32153898..99a9b287fe332 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Kysely } from 'kysely'; +import { ExpressionBuilder, Kysely } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; @@ -8,6 +8,38 @@ import { SessionEntity } from 'src/entities/session.entity'; import { ISessionRepository, SessionSearchOptions } from 'src/interfaces/session.interface'; import { Repository } from 'typeorm'; +const withUser = (eb: ExpressionBuilder) => { + return eb + .selectFrom('users') + .select([ + 'id', + 'email', + 'createdAt', + 'profileImagePath', + 'isAdmin', + 'shouldChangePassword', + 'deletedAt', + 'oauthId', + 'updatedAt', + 'storageLabel', + 'name', + 'quotaSizeInBytes', + 'quotaUsageInBytes', + 'status', + 'profileChangedAt', + ]) + .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'); +}; + @Injectable() export class SessionRepository implements ISessionRepository { constructor( @@ -17,8 +49,6 @@ export class SessionRepository implements ISessionRepository { @GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] }) search(options: SessionSearchOptions): Promise { - // return this.repository.find({ where: { updatedAt: LessThanOrEqual(options.updatedBefore) } }); - return this.db .selectFrom('sessions') .selectAll() @@ -28,69 +58,26 @@ export class SessionRepository implements ISessionRepository { @GenerateSql({ params: [DummyValue.STRING] }) getByToken(token: string): Promise { - // return this.repository.findOne({ - // where: { token }, - // relations: { - // user: { - // metadata: true, - // }, - // }, - // }); - return this.db .selectFrom('sessions') - .innerJoinLateral( - (eb) => - eb - .selectFrom('users') - .select([ - 'id', - 'email', - 'createdAt', - 'profileImagePath', - 'isAdmin', - 'shouldChangePassword', - 'deletedAt', - 'oauthId', - 'updatedAt', - 'storageLabel', - 'name', - 'quotaSizeInBytes', - 'quotaUsageInBytes', - 'status', - 'profileChangedAt', - ]) - .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'), - (join) => join.onTrue(), - ) + .innerJoinLateral(withUser, (join) => join.onTrue()) .selectAll('sessions') .select((eb) => eb.fn.toJson('user').as('user')) .where('sessions.token', '=', token) .executeTakeFirst() as unknown as Promise; } + @GenerateSql({ params: [DummyValue.UUID] }) getByUserId(userId: string): Promise { - 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 any as Promise; } create>(dto: T): Promise { diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index 68df7828ad784..7e1c7087a4cd2 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -31,6 +31,7 @@ export class SessionService extends BaseService { async getAll(auth: AuthDto): Promise { const sessions = await this.sessionRepository.getByUserId(auth.user.id); + console.log('sessions', sessions); return sessions.map((session) => mapSession(session, auth.session?.id)); } From f46556bb6e7cbf25e9c0c8950845482196053cea Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sat, 11 Jan 2025 14:07:01 -0600 Subject: [PATCH 05/10] wip: create/update/delete --- server/src/entities/session.entity.ts | 34 +++++++++++++ server/src/interfaces/session.interface.ts | 6 ++- server/src/queries/session.repository.sql | 6 +-- server/src/repositories/session.repository.ts | 49 ++++--------------- server/src/services/auth.service.ts | 5 +- server/src/services/session.service.ts | 1 - 6 files changed, 53 insertions(+), 48 deletions(-) diff --git a/server/src/entities/session.entity.ts b/server/src/entities/session.entity.ts index 1cc9ad98572ab..e4d5431f5497d 100644 --- a/server/src/entities/session.entity.ts +++ b/server/src/entities/session.entity.ts @@ -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,35 @@ export class SessionEntity { @Column({ default: '' }) deviceOS!: string; } + +export const withUser = (eb: ExpressionBuilder) => { + return eb + .selectFrom('users') + .select([ + 'id', + 'email', + 'createdAt', + 'profileImagePath', + 'isAdmin', + 'shouldChangePassword', + 'deletedAt', + 'oauthId', + 'updatedAt', + 'storageLabel', + 'name', + 'quotaSizeInBytes', + 'quotaUsageInBytes', + 'status', + 'profileChangedAt', + ]) + .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'); +}; diff --git a/server/src/interfaces/session.interface.ts b/server/src/interfaces/session.interface.ts index 33b48045a2376..0754486d8a198 100644 --- a/server/src/interfaces/session.interface.ts +++ b/server/src/interfaces/session.interface.ts @@ -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,8 +9,8 @@ export type SessionSearchOptions = { updatedBefore: Date }; export interface ISessionRepository { search(options: SessionSearchOptions): Promise; - create>(dto: T): Promise; - update>(dto: T): Promise; + create(dto: Insertable): Promise; + update(dto: Updateable): Promise; delete(id: string): Promise; getByToken(token: string): Promise; getByUserId(userId: string): Promise; diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index b65cd0a59deb1..b928195e72009 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -92,6 +92,6 @@ order by "sessions"."createdAt" desc -- SessionRepository.delete -DELETE FROM "sessions" -WHERE - "id" = $1 +delete from "sessions" +where + "id" = $1::uuid diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index 99a9b287fe332..d27733e64f149 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -1,45 +1,14 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { ExpressionBuilder, Kysely } from 'kysely'; +import { Insertable, Kysely, Updateable } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; -import { DB } from 'src/db'; +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 { asUuid } from 'src/utils/database'; import { Repository } from 'typeorm'; -const withUser = (eb: ExpressionBuilder) => { - return eb - .selectFrom('users') - .select([ - 'id', - 'email', - 'createdAt', - 'profileImagePath', - 'isAdmin', - 'shouldChangePassword', - 'deletedAt', - 'oauthId', - 'updatedAt', - 'storageLabel', - 'name', - 'quotaSizeInBytes', - 'quotaUsageInBytes', - 'status', - 'profileChangedAt', - ]) - .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'); -}; - @Injectable() export class SessionRepository implements ISessionRepository { constructor( @@ -80,16 +49,16 @@ export class SessionRepository implements ISessionRepository { .execute() as any as Promise; } - create>(dto: T): Promise { - return this.repository.save(dto); + create(dto: Insertable): Promise { + return this.db.insertInto('sessions').values(dto).returningAll().executeTakeFirst() as Promise; } - update>(dto: T): Promise { - return this.repository.save(dto); + update(dto: Updateable): Promise { + return this.db.updateTable('sessions').set(dto).returningAll().executeTakeFirst() as Promise; } @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string): Promise { - await this.repository.delete({ id }); + await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute(); } } diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index d6154976f9ffe..9c25cb147c9bb 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -344,13 +344,14 @@ export class AuthService extends BaseService { const key = this.cryptoRepository.newPassword(32); const token = this.cryptoRepository.hashSha256(key); - await this.sessionRepository.create({ + const a = await this.sessionRepository.create({ token, - user, deviceOS: loginDetails.deviceOS, deviceType: loginDetails.deviceType, + userId: user.id, }); + console.log('session created', a); return mapLoginResponse(user, key); } diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index 7e1c7087a4cd2..68df7828ad784 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -31,7 +31,6 @@ export class SessionService extends BaseService { async getAll(auth: AuthDto): Promise { const sessions = await this.sessionRepository.getByUserId(auth.user.id); - console.log('sessions', sessions); return sessions.map((session) => mapSession(session, auth.session?.id)); } From bf7769363da1045eb0eef3930b462305af7465fb Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sat, 11 Jan 2025 14:10:40 -0600 Subject: [PATCH 06/10] remove unused code --- server/src/services/auth.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 9c25cb147c9bb..744e1158c046c 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -344,14 +344,13 @@ export class AuthService extends BaseService { const key = this.cryptoRepository.newPassword(32); const token = this.cryptoRepository.hashSha256(key); - const a = await this.sessionRepository.create({ + await this.sessionRepository.create({ token, deviceOS: loginDetails.deviceOS, deviceType: loginDetails.deviceType, userId: user.id, }); - console.log('session created', a); return mapLoginResponse(user, key); } From ab64f46e769bf330a7a9547c7f012b2797b3ded0 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sat, 11 Jan 2025 14:43:44 -0600 Subject: [PATCH 07/10] clean up and pr feedback --- server/src/interfaces/session.interface.ts | 4 +-- server/src/repositories/session.repository.ts | 32 +++++++++++-------- server/src/services/auth.service.ts | 2 +- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/server/src/interfaces/session.interface.ts b/server/src/interfaces/session.interface.ts index 0754486d8a198..8d695fbfc29c0 100644 --- a/server/src/interfaces/session.interface.ts +++ b/server/src/interfaces/session.interface.ts @@ -10,8 +10,8 @@ export type SessionSearchOptions = { updatedBefore: Date }; export interface ISessionRepository { search(options: SessionSearchOptions): Promise; create(dto: Insertable): Promise; - update(dto: Updateable): Promise; + update(id: string, dto: Updateable): Promise; delete(id: string): Promise; - getByToken(token: string): Promise; + getByToken(token: string): Promise; getByUserId(userId: string): Promise; } diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index d27733e64f149..3e6c8977212a7 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -1,5 +1,4 @@ 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'; @@ -7,14 +6,10 @@ import { DummyValue, GenerateSql } from 'src/decorators'; import { SessionEntity, withUser } from 'src/entities/session.entity'; import { ISessionRepository, SessionSearchOptions } from 'src/interfaces/session.interface'; import { asUuid } from 'src/utils/database'; -import { Repository } from 'typeorm'; @Injectable() export class SessionRepository implements ISessionRepository { - constructor( - @InjectRepository(SessionEntity) private repository: Repository, - @InjectKysely() private db: Kysely, - ) {} + constructor(@InjectKysely() private db: Kysely) {} @GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] }) search(options: SessionSearchOptions): Promise { @@ -26,14 +21,14 @@ export class SessionRepository implements ISessionRepository { } @GenerateSql({ params: [DummyValue.STRING] }) - getByToken(token: string): Promise { + getByToken(token: string): Promise { 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 unknown as Promise; + .executeTakeFirst() as Promise; } @GenerateSql({ params: [DummyValue.UUID] }) @@ -46,15 +41,26 @@ export class SessionRepository implements ISessionRepository { .where('sessions.userId', '=', userId) .orderBy('sessions.updatedAt', 'desc') .orderBy('sessions.createdAt', 'desc') - .execute() as any as Promise; + .execute() as unknown as Promise; } - create(dto: Insertable): Promise { - return this.db.insertInto('sessions').values(dto).returningAll().executeTakeFirst() as Promise; + async create(dto: Insertable): Promise { + 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(dto: Updateable): Promise { - return this.db.updateTable('sessions').set(dto).returningAll().executeTakeFirst() as Promise; + update(id: string, dto: Updateable): Promise { + return this.db + .updateTable('sessions') + .set(dto) + .where('sessions.id', '=', asUuid(id)) + .returningAll() + .executeTakeFirstOrThrow() as Promise; } @GenerateSql({ params: [DummyValue.UUID] }) diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 744e1158c046c..80321dea2abfe 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -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 }; From f1f1bedc53bb567155a4902e7ac10f128e63d340 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sat, 11 Jan 2025 22:40:38 -0600 Subject: [PATCH 08/10] fix: test --- server/src/services/auth.service.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 06035b03a2937..8e9d294e983d9 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -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) }); }); }); From 95d99b4670b42fb6529dabf4a807ba52b9f5bdc5 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sat, 11 Jan 2025 23:57:24 -0600 Subject: [PATCH 09/10] fix: e2e test --- e2e/src/api/specs/user.e2e-spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/api/specs/user.e2e-spec.ts index 1964dc6793642..9cffa5d754d1f 100644 --- a/e2e/src/api/specs/user.e2e-spec.ts +++ b/e2e/src/api/specs/user.e2e-spec.ts @@ -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(), }); }); }); From 7a6d66b559586492c8ee5936c4193c4c288ced96 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Mon, 13 Jan 2025 15:48:04 -0600 Subject: [PATCH 10/10] pr feedback --- server/src/entities/session.entity.ts | 36 ++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/server/src/entities/session.entity.ts b/server/src/entities/session.entity.ts index e4d5431f5497d..e21c6d52ba469 100644 --- a/server/src/entities/session.entity.ts +++ b/server/src/entities/session.entity.ts @@ -30,26 +30,28 @@ export class SessionEntity { 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) => { return eb .selectFrom('users') - .select([ - 'id', - 'email', - 'createdAt', - 'profileImagePath', - 'isAdmin', - 'shouldChangePassword', - 'deletedAt', - 'oauthId', - 'updatedAt', - 'storageLabel', - 'name', - 'quotaSizeInBytes', - 'quotaUsageInBytes', - 'status', - 'profileChangedAt', - ]) + .select(userColumns) .select((eb) => eb .selectFrom('user_metadata')