2023-10-31 11:01:32 -04:00
|
|
|
import { Injectable } from '@nestjs/common';
|
2025-01-13 19:30:34 -06:00
|
|
|
import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
2025-03-17 15:32:12 -04:00
|
|
|
import { DateTime } from 'luxon';
|
2025-01-13 19:30:34 -06:00
|
|
|
import { InjectKysely } from 'nestjs-kysely';
|
2025-03-17 15:32:12 -04:00
|
|
|
import { columns, UserAdmin } from 'src/database';
|
2025-03-28 10:40:09 -04:00
|
|
|
import { DB, UserMetadata as DbUserMetadata } from 'src/db';
|
2024-03-20 15:04:03 -05:00
|
|
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
2025-02-12 15:23:08 -05:00
|
|
|
import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity';
|
2025-01-13 19:30:34 -06:00
|
|
|
import { UserEntity, withMetadata } from 'src/entities/user.entity';
|
2025-03-06 13:33:24 -05:00
|
|
|
import { AssetType, UserStatus } from 'src/enum';
|
2025-03-29 09:26:24 -04:00
|
|
|
import { UserTable } from 'src/schema/tables/user.table';
|
2025-01-13 19:30:34 -06:00
|
|
|
import { asUuid } from 'src/utils/database';
|
|
|
|
|
|
|
|
|
|
type Upsert = Insertable<DbUserMetadata>;
|
2022-07-15 21:30:56 +02:00
|
|
|
|
2025-02-11 14:08:13 -05:00
|
|
|
export interface UserListFilter {
|
|
|
|
|
withDeleted?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UserStatsQueryResponse {
|
|
|
|
|
userId: string;
|
|
|
|
|
userName: string;
|
|
|
|
|
photos: number;
|
|
|
|
|
videos: number;
|
|
|
|
|
usage: number;
|
|
|
|
|
usagePhotos: number;
|
|
|
|
|
usageVideos: number;
|
|
|
|
|
quotaSizeInBytes: number | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UserFindOptions {
|
|
|
|
|
withDeleted?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-11 21:34:36 -05:00
|
|
|
@Injectable()
|
2025-02-11 14:08:13 -05:00
|
|
|
export class UserRepository {
|
2025-01-13 19:30:34 -06:00
|
|
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
2022-07-15 21:30:56 +02:00
|
|
|
|
2025-01-13 19:30:34 -06:00
|
|
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BOOLEAN] })
|
|
|
|
|
get(userId: string, options: UserFindOptions): Promise<UserEntity | undefined> {
|
2023-10-31 11:01:32 -04:00
|
|
|
options = options || {};
|
2025-01-13 19:30:34 -06:00
|
|
|
|
|
|
|
|
return this.db
|
|
|
|
|
.selectFrom('users')
|
2025-03-17 15:32:12 -04:00
|
|
|
.select(columns.userAdmin)
|
2025-01-13 19:30:34 -06:00
|
|
|
.select(withMetadata)
|
|
|
|
|
.where('users.id', '=', userId)
|
|
|
|
|
.$if(!options.withDeleted, (eb) => eb.where('users.deletedAt', 'is', null))
|
|
|
|
|
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
2022-07-15 21:30:56 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-12 15:23:08 -05:00
|
|
|
getMetadata(userId: string) {
|
|
|
|
|
return this.db
|
|
|
|
|
.selectFrom('user_metadata')
|
|
|
|
|
.select(['key', 'value'])
|
|
|
|
|
.where('user_metadata.userId', '=', userId)
|
|
|
|
|
.execute() as Promise<UserMetadataItem[]>;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-30 10:10:30 -05:00
|
|
|
@GenerateSql()
|
2025-01-13 19:30:34 -06:00
|
|
|
getAdmin(): Promise<UserEntity | undefined> {
|
|
|
|
|
return this.db
|
|
|
|
|
.selectFrom('users')
|
2025-03-17 15:32:12 -04:00
|
|
|
.select(columns.userAdmin)
|
2025-01-13 19:30:34 -06:00
|
|
|
.where('users.isAdmin', '=', true)
|
|
|
|
|
.where('users.deletedAt', 'is', null)
|
|
|
|
|
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
2022-07-15 21:30:56 +02:00
|
|
|
}
|
|
|
|
|
|
2023-11-30 10:10:30 -05:00
|
|
|
@GenerateSql()
|
2023-10-11 04:37:13 +02:00
|
|
|
async hasAdmin(): Promise<boolean> {
|
2025-01-13 19:30:34 -06:00
|
|
|
const admin = await this.db
|
|
|
|
|
.selectFrom('users')
|
|
|
|
|
.select('users.id')
|
|
|
|
|
.where('users.isAdmin', '=', true)
|
|
|
|
|
.where('users.deletedAt', 'is', null)
|
|
|
|
|
.executeTakeFirst();
|
|
|
|
|
|
|
|
|
|
return !!admin;
|
2023-10-11 04:37:13 +02:00
|
|
|
}
|
|
|
|
|
|
2023-11-30 10:10:30 -05:00
|
|
|
@GenerateSql({ params: [DummyValue.EMAIL] })
|
2025-01-13 19:30:34 -06:00
|
|
|
getByEmail(email: string, withPassword?: boolean): Promise<UserEntity | undefined> {
|
|
|
|
|
return this.db
|
|
|
|
|
.selectFrom('users')
|
2025-03-17 15:32:12 -04:00
|
|
|
.select(columns.userAdmin)
|
2025-01-13 19:30:34 -06:00
|
|
|
.$if(!!withPassword, (eb) => eb.select('password'))
|
|
|
|
|
.where('email', '=', email)
|
|
|
|
|
.where('users.deletedAt', 'is', null)
|
|
|
|
|
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
2022-07-15 21:30:56 +02:00
|
|
|
}
|
|
|
|
|
|
2023-11-30 10:10:30 -05:00
|
|
|
@GenerateSql({ params: [DummyValue.STRING] })
|
2025-01-13 19:30:34 -06:00
|
|
|
getByStorageLabel(storageLabel: string): Promise<UserEntity | undefined> {
|
|
|
|
|
return this.db
|
|
|
|
|
.selectFrom('users')
|
2025-03-17 15:32:12 -04:00
|
|
|
.select(columns.userAdmin)
|
2025-01-13 19:30:34 -06:00
|
|
|
.where('users.storageLabel', '=', storageLabel)
|
|
|
|
|
.where('users.deletedAt', 'is', null)
|
|
|
|
|
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
2023-05-21 23:18:10 -04:00
|
|
|
}
|
|
|
|
|
|
2023-11-30 10:10:30 -05:00
|
|
|
@GenerateSql({ params: [DummyValue.STRING] })
|
2025-01-13 19:30:34 -06:00
|
|
|
getByOAuthId(oauthId: string): Promise<UserEntity | undefined> {
|
|
|
|
|
return this.db
|
|
|
|
|
.selectFrom('users')
|
2025-03-17 15:32:12 -04:00
|
|
|
.select(columns.userAdmin)
|
2025-01-13 19:30:34 -06:00
|
|
|
.where('users.oauthId', '=', oauthId)
|
|
|
|
|
.where('users.deletedAt', 'is', null)
|
|
|
|
|
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-17 15:32:12 -04:00
|
|
|
@GenerateSql({ params: [DateTime.now().minus({ years: 1 })] })
|
|
|
|
|
getDeletedAfter(target: DateTime) {
|
|
|
|
|
return this.db.selectFrom('users').select(['id']).where('users.deletedAt', '<', target.toJSDate()).execute();
|
2025-01-13 19:30:34 -06:00
|
|
|
}
|
|
|
|
|
|
2025-03-17 15:32:12 -04:00
|
|
|
@GenerateSql(
|
|
|
|
|
{ name: 'with deleted', params: [{ withDeleted: true }] },
|
|
|
|
|
{ name: 'without deleted', params: [{ withDeleted: false }] },
|
|
|
|
|
)
|
|
|
|
|
getList({ withDeleted }: UserListFilter = {}) {
|
2025-01-13 19:30:34 -06:00
|
|
|
return this.db
|
|
|
|
|
.selectFrom('users')
|
2025-03-17 15:32:12 -04:00
|
|
|
.select(columns.userAdmin)
|
2025-01-13 19:30:34 -06:00
|
|
|
.select(withMetadata)
|
|
|
|
|
.$if(!withDeleted, (eb) => eb.where('users.deletedAt', 'is', null))
|
|
|
|
|
.orderBy('createdAt', 'desc')
|
2025-03-17 15:32:12 -04:00
|
|
|
.execute() as Promise<UserAdmin[]>;
|
2025-01-13 19:30:34 -06:00
|
|
|
}
|
|
|
|
|
|
2025-03-28 10:40:09 -04:00
|
|
|
async create(dto: Insertable<UserTable>): Promise<UserEntity> {
|
2025-01-13 19:30:34 -06:00
|
|
|
return this.db
|
|
|
|
|
.insertInto('users')
|
|
|
|
|
.values(dto)
|
2025-03-17 15:32:12 -04:00
|
|
|
.returning(columns.userAdmin)
|
2025-01-13 19:30:34 -06:00
|
|
|
.executeTakeFirst() as unknown as Promise<UserEntity>;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 10:40:09 -04:00
|
|
|
update(id: string, dto: Updateable<UserTable>): Promise<UserEntity> {
|
2025-01-13 19:30:34 -06:00
|
|
|
return this.db
|
|
|
|
|
.updateTable('users')
|
|
|
|
|
.set(dto)
|
|
|
|
|
.where('users.id', '=', asUuid(id))
|
|
|
|
|
.where('users.deletedAt', 'is', null)
|
2025-03-17 15:32:12 -04:00
|
|
|
.returning(columns.userAdmin)
|
2025-01-13 19:30:34 -06:00
|
|
|
.returning(withMetadata)
|
|
|
|
|
.executeTakeFirst() as unknown as Promise<UserEntity>;
|
2022-07-15 21:30:56 +02:00
|
|
|
}
|
|
|
|
|
|
2025-01-29 11:49:08 -05:00
|
|
|
restore(id: string): Promise<UserEntity> {
|
|
|
|
|
return this.db
|
|
|
|
|
.updateTable('users')
|
|
|
|
|
.set({ status: UserStatus.ACTIVE, deletedAt: null })
|
|
|
|
|
.where('users.id', '=', asUuid(id))
|
2025-03-17 15:32:12 -04:00
|
|
|
.returning(columns.userAdmin)
|
2025-01-29 11:49:08 -05:00
|
|
|
.returning(withMetadata)
|
|
|
|
|
.executeTakeFirst() as unknown as Promise<UserEntity>;
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-01 18:43:16 +01:00
|
|
|
async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) {
|
2025-01-13 19:30:34 -06:00
|
|
|
await this.db
|
|
|
|
|
.insertInto('user_metadata')
|
|
|
|
|
.values({ userId: id, key, value } as Upsert)
|
|
|
|
|
.onConflict((oc) =>
|
|
|
|
|
oc.columns(['userId', 'key']).doUpdateSet({
|
|
|
|
|
key,
|
|
|
|
|
value,
|
|
|
|
|
} as Upsert),
|
|
|
|
|
)
|
|
|
|
|
.execute();
|
2024-05-22 08:13:36 -04:00
|
|
|
}
|
|
|
|
|
|
2024-07-01 18:43:16 +01:00
|
|
|
async deleteMetadata<T extends keyof UserMetadata>(id: string, key: T) {
|
2025-01-13 19:30:34 -06:00
|
|
|
await this.db.deleteFrom('user_metadata').where('userId', '=', id).where('key', '=', key).execute();
|
2024-07-01 18:43:16 +01:00
|
|
|
}
|
|
|
|
|
|
2025-02-25 11:31:07 -05:00
|
|
|
delete(user: { id: string }, hard?: boolean): Promise<UserEntity> {
|
2025-01-13 19:30:34 -06:00
|
|
|
return hard
|
|
|
|
|
? (this.db.deleteFrom('users').where('id', '=', user.id).execute() as unknown as Promise<UserEntity>)
|
|
|
|
|
: (this.db
|
|
|
|
|
.updateTable('users')
|
|
|
|
|
.set({ deletedAt: new Date() })
|
|
|
|
|
.where('id', '=', user.id)
|
|
|
|
|
.execute() as unknown as Promise<UserEntity>);
|
2022-11-07 16:53:47 -05:00
|
|
|
}
|
|
|
|
|
|
2023-11-30 10:10:30 -05:00
|
|
|
@GenerateSql()
|
2023-03-21 22:49:19 -04:00
|
|
|
async getUserStats(): Promise<UserStatsQueryResponse[]> {
|
2025-01-13 19:30:34 -06:00
|
|
|
const stats = (await this.db
|
|
|
|
|
.selectFrom('users')
|
|
|
|
|
.leftJoin('assets', 'assets.ownerId', 'users.id')
|
|
|
|
|
.leftJoin('exif', 'exif.assetId', 'assets.id')
|
|
|
|
|
.select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes as quotaSizeInBytes'])
|
|
|
|
|
.select((eb) => [
|
|
|
|
|
eb.fn
|
|
|
|
|
.countAll()
|
2025-03-06 13:33:24 -05:00
|
|
|
.filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.isVisible', '=', true)]))
|
2025-01-13 19:30:34 -06:00
|
|
|
.as('photos'),
|
|
|
|
|
eb.fn
|
|
|
|
|
.countAll()
|
2025-03-06 13:33:24 -05:00
|
|
|
.filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.VIDEO), eb('assets.isVisible', '=', true)]))
|
2025-01-13 19:30:34 -06:00
|
|
|
.as('videos'),
|
|
|
|
|
eb.fn
|
|
|
|
|
.coalesce(eb.fn.sum('exif.fileSizeInByte').filterWhere('assets.libraryId', 'is', null), eb.lit(0))
|
|
|
|
|
.as('usage'),
|
|
|
|
|
eb.fn
|
|
|
|
|
.coalesce(
|
|
|
|
|
eb.fn
|
|
|
|
|
.sum('exif.fileSizeInByte')
|
2025-03-06 13:33:24 -05:00
|
|
|
.filterWhere((eb) =>
|
|
|
|
|
eb.and([eb('assets.libraryId', 'is', null), eb('assets.type', '=', AssetType.IMAGE)]),
|
|
|
|
|
),
|
2025-01-13 19:30:34 -06:00
|
|
|
eb.lit(0),
|
|
|
|
|
)
|
|
|
|
|
.as('usagePhotos'),
|
|
|
|
|
eb.fn
|
|
|
|
|
.coalesce(
|
|
|
|
|
eb.fn
|
|
|
|
|
.sum('exif.fileSizeInByte')
|
2025-03-06 13:33:24 -05:00
|
|
|
.filterWhere((eb) =>
|
|
|
|
|
eb.and([eb('assets.libraryId', 'is', null), eb('assets.type', '=', AssetType.VIDEO)]),
|
|
|
|
|
),
|
2025-01-13 19:30:34 -06:00
|
|
|
eb.lit(0),
|
|
|
|
|
)
|
|
|
|
|
.as('usageVideos'),
|
|
|
|
|
])
|
|
|
|
|
.where('assets.deletedAt', 'is', null)
|
2023-03-21 22:49:19 -04:00
|
|
|
.groupBy('users.id')
|
2025-01-13 19:30:34 -06:00
|
|
|
.orderBy('users.createdAt', 'asc')
|
|
|
|
|
.execute()) as UserStatsQueryResponse[];
|
2023-03-21 22:49:19 -04:00
|
|
|
|
|
|
|
|
for (const stat of stats) {
|
|
|
|
|
stat.photos = Number(stat.photos);
|
|
|
|
|
stat.videos = Number(stat.videos);
|
|
|
|
|
stat.usage = Number(stat.usage);
|
2024-11-15 23:38:57 +01:00
|
|
|
stat.usagePhotos = Number(stat.usagePhotos);
|
|
|
|
|
stat.usageVideos = Number(stat.usageVideos);
|
2023-03-21 22:49:19 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return stats;
|
|
|
|
|
}
|
2023-10-31 11:01:32 -04:00
|
|
|
|
2024-05-03 15:34:57 -04:00
|
|
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] })
|
2024-01-12 18:43:36 -06:00
|
|
|
async updateUsage(id: string, delta: number): Promise<void> {
|
2025-01-13 19:30:34 -06:00
|
|
|
await this.db
|
|
|
|
|
.updateTable('users')
|
|
|
|
|
.set({ quotaUsageInBytes: sql`"quotaUsageInBytes" + ${delta}`, updatedAt: new Date() })
|
|
|
|
|
.where('id', '=', asUuid(id))
|
|
|
|
|
.where('users.deletedAt', 'is', null)
|
|
|
|
|
.execute();
|
2024-01-12 18:43:36 -06:00
|
|
|
}
|
|
|
|
|
|
2024-01-15 09:04:29 -06:00
|
|
|
@GenerateSql({ params: [DummyValue.UUID] })
|
|
|
|
|
async syncUsage(id?: string) {
|
2025-01-13 19:30:34 -06:00
|
|
|
const query = this.db
|
|
|
|
|
.updateTable('users')
|
|
|
|
|
.set({
|
|
|
|
|
quotaUsageInBytes: (eb) =>
|
|
|
|
|
eb
|
|
|
|
|
.selectFrom('assets')
|
|
|
|
|
.leftJoin('exif', 'exif.assetId', 'assets.id')
|
2025-02-12 15:23:08 -05:00
|
|
|
.select((eb) => eb.fn.coalesce(eb.fn.sum<number>('exif.fileSizeInByte'), eb.lit(0)).as('usage'))
|
2025-01-13 19:30:34 -06:00
|
|
|
.where('assets.libraryId', 'is', null)
|
|
|
|
|
.where('assets.ownerId', '=', eb.ref('users.id')),
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
})
|
|
|
|
|
.where('users.deletedAt', 'is', null)
|
|
|
|
|
.$if(id != undefined, (eb) => eb.where('users.id', '=', asUuid(id!)));
|
2024-01-15 09:04:29 -06:00
|
|
|
|
|
|
|
|
await query.execute();
|
2024-01-12 18:43:36 -06:00
|
|
|
}
|
2022-10-23 16:54:54 -05:00
|
|
|
}
|