mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
refactor: metadata entity (#17492)
This commit is contained in:
parent
3e372500b0
commit
206545356d
@ -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 = {
|
||||
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
@ -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 };
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>;
|
||||
|
@ -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> {
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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);
|
||||
|
1
server/test/fixtures/user.stub.ts
vendored
1
server/test/fixtures/user.stub.ts
vendored
@ -38,7 +38,6 @@ export const userStub = {
|
||||
assets: [],
|
||||
metadata: [
|
||||
{
|
||||
userId: authStub.user1.user.id,
|
||||
key: UserMetadataKey.PREFERENCES,
|
||||
value: { avatar: { color: UserAvatarColor.PRIMARY } },
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user