refactor: metadata entity (#17492)

This commit is contained in:
Jason Rasmussen 2025-04-09 11:45:30 -04:00 committed by GitHub
parent 3e372500b0
commit 206545356d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 120 additions and 148 deletions

View File

@ -1,6 +1,5 @@
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { AssetStatus, AssetType, MemoryType, Permission, UserStatus } from 'src/enum';
import { OnThisDayData } from 'src/types';
import { OnThisDayData, UserMetadataItem } from 'src/types';
export type AuthUser = {
id: string;
@ -96,7 +95,7 @@ export type UserAdmin = User & {
quotaSizeInBytes: number | null;
quotaUsageInBytes: number;
status: UserStatus;
metadata: UserMetadataEntity[];
metadata: UserMetadataItem[];
};
export type Asset = {

View File

@ -2,7 +2,6 @@ import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
import { Activity } from 'src/database';
import { mapUser, UserResponseDto } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity';
import { Optional, ValidateUUID } from 'src/validation';
export enum ReactionType {
@ -75,6 +74,6 @@ export const mapActivity = (activity: Activity): ActivityResponseDto => {
createdAt: activity.createdAt,
comment: activity.comment,
type: activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT,
user: mapUser(activity.user as unknown as UserEntity),
user: mapUser(activity.user),
};
};

View File

@ -1,8 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator';
import { UserPreferences } from 'src/entities/user-metadata.entity';
import { UserAvatarColor } from 'src/enum';
import { UserPreferences } from 'src/types';
import { Optional, ValidateBoolean } from 'src/validation';
class AvatarUpdate {

View File

@ -2,9 +2,9 @@ import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
import { User, UserAdmin } from 'src/database';
import { UserMetadataEntity, UserMetadataItem } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
import { UserMetadataItem } from 'src/types';
import { getPreferences } from 'src/utils/preferences';
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
@ -143,8 +143,9 @@ export class UserAdminResponseDto extends UserResponseDto {
}
export function mapUserAdmin(entity: UserEntity | UserAdmin): UserAdminResponseDto {
const license = (entity.metadata as UserMetadataItem[])?.find(
(item): item is UserMetadataEntity<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
const metadata = entity.metadata || [];
const license = metadata.find(
(item): item is UserMetadataItem<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
)?.value;
return {
...mapUser(entity),

View File

@ -1,110 +0,0 @@
import { UserEntity } from 'src/entities/user.entity';
import { UserAvatarColor, UserMetadataKey } from 'src/enum';
import { DeepPartial } from 'src/types';
import { HumanReadableSize } from 'src/utils/bytes';
export type UserMetadataItem<T extends keyof UserMetadata = UserMetadataKey> = {
key: T;
value: UserMetadata[T];
};
export class UserMetadataEntity<T extends keyof UserMetadata = UserMetadataKey> implements UserMetadataItem<T> {
userId!: string;
user?: UserEntity;
key!: T;
value!: UserMetadata[T];
}
export interface UserPreferences {
folders: {
enabled: boolean;
sidebarWeb: boolean;
};
memories: {
enabled: boolean;
};
people: {
enabled: boolean;
sidebarWeb: boolean;
};
ratings: {
enabled: boolean;
};
sharedLinks: {
enabled: boolean;
sidebarWeb: boolean;
};
tags: {
enabled: boolean;
sidebarWeb: boolean;
};
avatar: {
color: UserAvatarColor;
};
emailNotifications: {
enabled: boolean;
albumInvite: boolean;
albumUpdate: boolean;
};
download: {
archiveSize: number;
includeEmbeddedVideos: boolean;
};
purchase: {
showSupportBadge: boolean;
hideBuyButtonUntil: string;
};
}
export const getDefaultPreferences = (user: { email: string }): UserPreferences => {
const values = Object.values(UserAvatarColor);
const randomIndex = Math.floor(
[...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
);
return {
folders: {
enabled: false,
sidebarWeb: false,
},
memories: {
enabled: true,
},
people: {
enabled: true,
sidebarWeb: false,
},
sharedLinks: {
enabled: true,
sidebarWeb: false,
},
ratings: {
enabled: false,
},
tags: {
enabled: false,
sidebarWeb: false,
},
avatar: {
color: values[randomIndex],
},
emailNotifications: {
enabled: true,
albumInvite: true,
albumUpdate: true,
},
download: {
archiveSize: HumanReadableSize.GiB * 4,
includeEmbeddedVideos: false,
},
purchase: {
showSupportBadge: true,
hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(),
},
};
};
export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> {
[UserMetadataKey.PREFERENCES]: DeepPartial<UserPreferences>;
[UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: string };
}

View File

@ -2,8 +2,8 @@ import { ExpressionBuilder } from 'kysely';
import { jsonArrayFrom } from 'kysely/helpers/postgres';
import { DB } from 'src/db';
import { AssetEntity } from 'src/entities/asset.entity';
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserStatus } from 'src/enum';
import { UserMetadataItem } from 'src/types';
export class UserEntity {
id!: string;
@ -23,7 +23,7 @@ export class UserEntity {
assets!: AssetEntity[];
quotaSizeInBytes!: number | null;
quotaUsageInBytes!: number;
metadata!: UserMetadataEntity[];
metadata!: UserMetadataItem[];
profileChangedAt!: Date;
}

View File

@ -5,10 +5,10 @@ import { InjectKysely } from 'nestjs-kysely';
import { columns, UserAdmin } from 'src/database';
import { DB, UserMetadata as DbUserMetadata } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity';
import { UserEntity, withMetadata } from 'src/entities/user.entity';
import { AssetType, UserStatus } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
import { UserMetadata, UserMetadataItem } from 'src/types';
import { asUuid } from 'src/utils/database';
type Upsert = Insertable<DbUserMetadata>;

View File

@ -1,7 +1,7 @@
import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity';
import { UserMetadataKey } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
import { Column, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools';
import { UserMetadata, UserMetadataItem } from 'src/types';
@Table('user_metadata')
export class UserMetadataTable<T extends keyof UserMetadata = UserMetadataKey> implements UserMetadataItem<T> {

View File

@ -1,10 +1,10 @@
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import { DateTime } from 'luxon';
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { AuthType, Permission } from 'src/enum';
import { AuthService } from 'src/services/auth.service';
import { UserMetadataItem } from 'src/types';
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userStub } from 'test/fixtures/user.stub';
@ -230,7 +230,7 @@ describe('AuthService', () => {
...dto,
id: 'admin',
createdAt: new Date('2021-01-01'),
metadata: [] as UserMetadataEntity[],
metadata: [] as UserMetadataItem[],
} as UserEntity);
await expect(sut.adminSignUp(dto)).resolves.toMatchObject({

View File

@ -357,8 +357,6 @@ describe(NotificationService.name, () => {
{
key: UserMetadataKey.PREFERENCES,
value: { emailNotifications: { enabled: false, albumInvite: true } },
userId: userStub.user1.id,
user: userStub.user1,
},
],
});
@ -374,8 +372,6 @@ describe(NotificationService.name, () => {
{
key: UserMetadataKey.PREFERENCES,
value: { emailNotifications: { enabled: true, albumInvite: false } },
userId: userStub.user1.id,
user: userStub.user1,
},
],
});
@ -391,8 +387,6 @@ describe(NotificationService.name, () => {
{
key: UserMetadataKey.PREFERENCES,
value: { emailNotifications: { enabled: true, albumInvite: true } },
userId: userStub.user1.id,
user: userStub.user1,
},
],
});
@ -414,8 +408,6 @@ describe(NotificationService.name, () => {
{
key: UserMetadataKey.PREFERENCES,
value: { emailNotifications: { enabled: true, albumInvite: true } },
userId: userStub.user1.id,
user: userStub.user1,
},
],
});
@ -443,8 +435,6 @@ describe(NotificationService.name, () => {
{
key: UserMetadataKey.PREFERENCES,
value: { emailNotifications: { enabled: true, albumInvite: true } },
userId: userStub.user1.id,
user: userStub.user1,
},
],
});
@ -476,8 +466,6 @@ describe(NotificationService.name, () => {
{
key: UserMetadataKey.PREFERENCES,
value: { emailNotifications: { enabled: true, albumInvite: true } },
userId: userStub.user1.id,
user: userStub.user1,
},
],
});
@ -536,8 +524,6 @@ describe(NotificationService.name, () => {
{
key: UserMetadataKey.PREFERENCES,
value: { emailNotifications: { enabled: false, albumUpdate: true } },
user: userStub.user1,
userId: userStub.user1.id,
},
],
});
@ -559,8 +545,6 @@ describe(NotificationService.name, () => {
{
key: UserMetadataKey.PREFERENCES,
value: { emailNotifications: { enabled: true, albumUpdate: false } },
user: userStub.user1,
userId: userStub.user1.id,
},
],
});

View File

@ -8,12 +8,11 @@ import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
import { UserFindOptions } from 'src/repositories/user.repository';
import { BaseService } from 'src/services/base.service';
import { JobOf } from 'src/types';
import { JobOf, UserMetadataItem } from 'src/types';
import { ImmichFileResponse } from 'src/utils/file';
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
@ -135,7 +134,7 @@ export class UserService extends BaseService {
const metadata = await this.userRepository.getMetadata(auth.user.id);
const license = metadata.find(
(item): item is UserMetadataEntity<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
(item): item is UserMetadataItem<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
);
if (!license) {
throw new NotFoundException();

View File

@ -11,6 +11,8 @@ import {
SyncEntityType,
SystemMetadataKey,
TranscodeTarget,
UserAvatarColor,
UserMetadataKey,
VideoCodec,
} from 'src/enum';
@ -455,3 +457,54 @@ export interface SystemMetadata extends Record<SystemMetadataKey, Record<string,
[SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata;
[SystemMetadataKey.MEMORIES_STATE]: MemoriesState;
}
export type UserMetadataItem<T extends keyof UserMetadata = UserMetadataKey> = {
key: T;
value: UserMetadata[T];
};
export interface UserPreferences {
folders: {
enabled: boolean;
sidebarWeb: boolean;
};
memories: {
enabled: boolean;
};
people: {
enabled: boolean;
sidebarWeb: boolean;
};
ratings: {
enabled: boolean;
};
sharedLinks: {
enabled: boolean;
sidebarWeb: boolean;
};
tags: {
enabled: boolean;
sidebarWeb: boolean;
};
avatar: {
color: UserAvatarColor;
};
emailNotifications: {
enabled: boolean;
albumInvite: boolean;
albumUpdate: boolean;
};
download: {
archiveSize: number;
includeEmbeddedVideos: boolean;
};
purchase: {
showSupportBadge: boolean;
hideBuyButtonUntil: string;
};
}
export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> {
[UserMetadataKey.PREFERENCES]: DeepPartial<UserPreferences>;
[UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: string };
}

View File

@ -1,10 +1,58 @@
import _ from 'lodash';
import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
import { UserMetadataItem, UserPreferences, getDefaultPreferences } from 'src/entities/user-metadata.entity';
import { UserMetadataKey } from 'src/enum';
import { DeepPartial } from 'src/types';
import { UserAvatarColor, UserMetadataKey } from 'src/enum';
import { DeepPartial, UserMetadataItem, UserPreferences } from 'src/types';
import { HumanReadableSize } from 'src/utils/bytes';
import { getKeysDeep } from 'src/utils/misc';
const getDefaultPreferences = (user: { email: string }): UserPreferences => {
const values = Object.values(UserAvatarColor);
const randomIndex = Math.floor(
[...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
);
return {
folders: {
enabled: false,
sidebarWeb: false,
},
memories: {
enabled: true,
},
people: {
enabled: true,
sidebarWeb: false,
},
sharedLinks: {
enabled: true,
sidebarWeb: false,
},
ratings: {
enabled: false,
},
tags: {
enabled: false,
sidebarWeb: false,
},
avatar: {
color: values[randomIndex],
},
emailNotifications: {
enabled: true,
albumInvite: true,
albumUpdate: true,
},
download: {
archiveSize: HumanReadableSize.GiB * 4,
includeEmbeddedVideos: false,
},
purchase: {
showSupportBadge: true,
hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(),
},
};
};
export const getPreferences = (email: string, metadata: UserMetadataItem[]): UserPreferences => {
const preferences = getDefaultPreferences({ email });
const item = metadata.find(({ key }) => key === UserMetadataKey.PREFERENCES);

View File

@ -38,7 +38,6 @@ export const userStub = {
assets: [],
metadata: [
{
userId: authStub.user1.user.id,
key: UserMetadataKey.PREFERENCES,
value: { avatar: { color: UserAvatarColor.PRIMARY } },
},