feat(server): user metadata (#9650)

* feat(server): user metadata

* add missing method to user mock

* update migration to include cascades

* update sql files

* test: fix e2e

* chore: clean up

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
Jason Rasmussen 2024-05-22 08:13:36 -04:00 committed by GitHub
parent a4887bfa7e
commit 06ce8247cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 267 additions and 126 deletions

View File

@ -257,23 +257,6 @@ describe('/user', () => {
expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' });
});
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/user`)
.send({
id: admin.userId,
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
deletedAt: '2023-01-01T00:00:00.000Z',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toStrictEqual(before);
});
it('should update first and last name', async () => {
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });

View File

@ -69,7 +69,7 @@ export class UserCore {
dto.storageLabel = null;
}
return this.userRepository.update(id, dto);
return this.userRepository.update(id, { ...dto, updatedAt: new Date() });
}
async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> {

View File

@ -1,6 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { UploadFieldName } from 'src/dtos/asset.dto';
import { UserAvatarColor, UserEntity } from 'src/entities/user.entity';
export class CreateProfileImageDto {
@ApiProperty({ type: 'string', format: 'binary' })
@ -18,11 +17,3 @@ export function mapCreateProfileImageResponse(userId: string, profileImagePath:
profileImagePath: profileImagePath,
};
}
export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => {
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 values[randomIndex] as UserAvatarColor;
};

View File

@ -1,8 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
import { getRandomAvatarColor } from 'src/dtos/user-profile.dto';
import { UserAvatarColor, UserEntity, UserStatus } from 'src/entities/user.entity';
import { UserAvatarColor } from 'src/entities/user-metadata.entity';
import { UserEntity, UserStatus } from 'src/entities/user.entity';
import { getPreferences } from 'src/utils/preferences';
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
export class CreateUserDto {
@ -151,7 +152,7 @@ export const mapSimpleUser = (entity: UserEntity): UserDto => {
email: entity.email,
name: entity.name,
profileImagePath: entity.profileImagePath,
avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity),
avatarColor: getPreferences(entity).avatar.color,
};
};
@ -165,7 +166,7 @@ export function mapUser(entity: UserEntity): UserResponseDto {
deletedAt: entity.deletedAt,
updatedAt: entity.updatedAt,
oauthId: entity.oauthId,
memoriesEnabled: entity.memoriesEnabled,
memoriesEnabled: getPreferences(entity).memories.enabled,
quotaSizeInBytes: entity.quotaSizeInBytes,
quotaUsageInBytes: entity.quotaUsageInBytes,
status: entity.status,

View File

@ -20,6 +20,7 @@ import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
import { TagEntity } from 'src/entities/tag.entity';
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
export const entities = [
@ -44,6 +45,7 @@ export const entities = [
SystemMetadataEntity,
TagEntity,
UserEntity,
UserMetadataEntity,
SessionEntity,
LibraryEntity,
];

View File

@ -0,0 +1,63 @@
import { UserEntity } from 'src/entities/user.entity';
import { Column, DeepPartial, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
@Entity('user_metadata')
export class UserMetadataEntity<T extends keyof UserMetadata = UserMetadataKey> {
@PrimaryColumn({ type: 'uuid' })
userId!: string;
@ManyToOne(() => UserEntity, (user) => user.metadata, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
user!: UserEntity;
@PrimaryColumn({ type: 'varchar' })
key!: T;
@Column({ type: 'jsonb' })
value!: UserMetadata[T];
}
export enum UserAvatarColor {
PRIMARY = 'primary',
PINK = 'pink',
RED = 'red',
YELLOW = 'yellow',
BLUE = 'blue',
GREEN = 'green',
PURPLE = 'purple',
ORANGE = 'orange',
GRAY = 'gray',
AMBER = 'amber',
}
export interface UserPreferences {
memories: {
enabled: boolean;
};
avatar: {
color: UserAvatarColor;
};
}
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 {
memories: {
enabled: true,
},
avatar: {
color: values[randomIndex],
},
};
};
export enum UserMetadataKey {
PREFERENCES = 'preferences',
}
export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> {
[UserMetadataKey.PREFERENCES]: DeepPartial<UserPreferences>;
}

View File

@ -1,5 +1,6 @@
import { AssetEntity } from 'src/entities/asset.entity';
import { TagEntity } from 'src/entities/tag.entity';
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import {
Column,
CreateDateColumn,
@ -10,19 +11,6 @@ import {
UpdateDateColumn,
} from 'typeorm';
export enum UserAvatarColor {
PRIMARY = 'primary',
PINK = 'pink',
RED = 'red',
YELLOW = 'yellow',
BLUE = 'blue',
GREEN = 'green',
PURPLE = 'purple',
ORANGE = 'orange',
GRAY = 'gray',
AMBER = 'amber',
}
export enum UserStatus {
ACTIVE = 'active',
REMOVING = 'removing',
@ -37,9 +25,6 @@ export class UserEntity {
@Column({ default: '' })
name!: string;
@Column({ type: 'varchar', nullable: true })
avatarColor!: UserAvatarColor | null;
@Column({ default: false })
isAdmin!: boolean;
@ -73,9 +58,6 @@ export class UserEntity {
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date;
@Column({ default: true })
memoriesEnabled!: boolean;
@OneToMany(() => TagEntity, (tag) => tag.user)
tags!: TagEntity[];
@ -87,4 +69,7 @@ export class UserEntity {
@Column({ type: 'bigint', default: 0 })
quotaUsageInBytes!: number;
@OneToMany(() => UserMetadataEntity, (metadata) => metadata.user)
metadata!: UserMetadataEntity[];
}

View File

@ -1,3 +1,4 @@
import { UserMetadata } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
export interface UserListFilter {
@ -31,6 +32,7 @@ export interface IUserRepository {
getUserStats(): Promise<UserStatsQueryResponse[]>;
create(user: Partial<UserEntity>): Promise<UserEntity>;
update(id: string, user: Partial<UserEntity>): Promise<UserEntity>;
upsertMetadata<T extends keyof UserMetadata>(id: string, item: { key: T; value: UserMetadata[T] }): Promise<void>;
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
updateUsage(id: string, delta: number): Promise<void>;
syncUsage(id?: string): Promise<void>;

View File

@ -0,0 +1,60 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UserMetadata1716312279245 implements MigrationInterface {
name = 'UserMetadata1716312279245';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "user_metadata" ("userId" uuid NOT NULL, "key" character varying NOT NULL, "value" jsonb NOT NULL, CONSTRAINT "PK_5931462150b3438cbc83277fe5a" PRIMARY KEY ("userId", "key"))`,
);
const users = await queryRunner.query('SELECT "id", "memoriesEnabled", "avatarColor" FROM "users"');
for (const { id, memoriesEnabled, avatarColor } of users) {
const preferences: any = {};
if (!memoriesEnabled) {
preferences.memories = { enabled: false };
}
if (avatarColor) {
preferences.avatar = { color: avatarColor };
}
if (Object.keys(preferences).length === 0) {
continue;
}
await queryRunner.query('INSERT INTO "user_metadata" ("userId", "key", "value") VALUES ($1, $2, $3)', [
id,
'preferences',
preferences,
]);
}
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "memoriesEnabled"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "avatarColor"`);
await queryRunner.query(
`ALTER TABLE "user_metadata" ADD CONSTRAINT "FK_6afb43681a21cf7815932bc38ac" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_metadata" DROP CONSTRAINT "FK_6afb43681a21cf7815932bc38ac"`);
await queryRunner.query(`ALTER TABLE "users" ADD "avatarColor" character varying`);
await queryRunner.query(`ALTER TABLE "users" ADD "memoriesEnabled" boolean NOT NULL DEFAULT true`);
const items = await queryRunner.query(
`SELECT "userId" as "id", "value" FROM "user_metadata" WHERE "key"='preferences'`,
);
for (const { id, value } of items) {
if (!value) {
continue;
}
if (value.avatar?.color) {
await queryRunner.query(`UPDATE "users" SET "avatarColor" = $1 WHERE "id" = $2`, [value.avatar.color, id]);
}
if (value.memories?.enabled === false) {
await queryRunner.query(`UPDATE "users" SET "memoriesEnabled" = false WHERE "id" = $1`, [id]);
}
}
await queryRunner.query(`DROP TABLE "user_metadata"`);
}
}

View File

@ -12,7 +12,6 @@ SELECT
"ActivityEntity"."isLiked" AS "ActivityEntity_isLiked",
"ActivityEntity__ActivityEntity_user"."id" AS "ActivityEntity__ActivityEntity_user_id",
"ActivityEntity__ActivityEntity_user"."name" AS "ActivityEntity__ActivityEntity_user_name",
"ActivityEntity__ActivityEntity_user"."avatarColor" AS "ActivityEntity__ActivityEntity_user_avatarColor",
"ActivityEntity__ActivityEntity_user"."isAdmin" AS "ActivityEntity__ActivityEntity_user_isAdmin",
"ActivityEntity__ActivityEntity_user"."email" AS "ActivityEntity__ActivityEntity_user_email",
"ActivityEntity__ActivityEntity_user"."storageLabel" AS "ActivityEntity__ActivityEntity_user_storageLabel",
@ -23,7 +22,6 @@ SELECT
"ActivityEntity__ActivityEntity_user"."deletedAt" AS "ActivityEntity__ActivityEntity_user_deletedAt",
"ActivityEntity__ActivityEntity_user"."status" AS "ActivityEntity__ActivityEntity_user_status",
"ActivityEntity__ActivityEntity_user"."updatedAt" AS "ActivityEntity__ActivityEntity_user_updatedAt",
"ActivityEntity__ActivityEntity_user"."memoriesEnabled" AS "ActivityEntity__ActivityEntity_user_memoriesEnabled",
"ActivityEntity__ActivityEntity_user"."quotaSizeInBytes" AS "ActivityEntity__ActivityEntity_user_quotaSizeInBytes",
"ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes"
FROM

View File

@ -18,7 +18,6 @@ FROM
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
@ -29,7 +28,6 @@ FROM
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
@ -37,7 +35,6 @@ FROM
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
@ -48,7 +45,6 @@ FROM
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
@ -98,7 +94,6 @@ SELECT
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
@ -109,7 +104,6 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
@ -117,7 +111,6 @@ SELECT
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
@ -128,7 +121,6 @@ SELECT
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes"
FROM
@ -160,7 +152,6 @@ SELECT
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
@ -171,7 +162,6 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
@ -179,7 +169,6 @@ SELECT
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
@ -190,7 +179,6 @@ SELECT
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes"
FROM
@ -299,7 +287,6 @@ SELECT
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
@ -310,7 +297,6 @@ SELECT
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
@ -327,7 +313,6 @@ SELECT
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
@ -338,7 +323,6 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
FROM
@ -376,7 +360,6 @@ SELECT
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
@ -387,7 +370,6 @@ SELECT
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
@ -404,7 +386,6 @@ SELECT
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
@ -415,7 +396,6 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
FROM
@ -504,7 +484,6 @@ SELECT
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
@ -515,7 +494,6 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
FROM
@ -564,7 +542,6 @@ SELECT
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
@ -575,7 +552,6 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
FROM

View File

@ -11,7 +11,6 @@ FROM
"APIKeyEntity"."userId" AS "APIKeyEntity_userId",
"APIKeyEntity__APIKeyEntity_user"."id" AS "APIKeyEntity__APIKeyEntity_user_id",
"APIKeyEntity__APIKeyEntity_user"."name" AS "APIKeyEntity__APIKeyEntity_user_name",
"APIKeyEntity__APIKeyEntity_user"."avatarColor" AS "APIKeyEntity__APIKeyEntity_user_avatarColor",
"APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin",
"APIKeyEntity__APIKeyEntity_user"."email" AS "APIKeyEntity__APIKeyEntity_user_email",
"APIKeyEntity__APIKeyEntity_user"."storageLabel" AS "APIKeyEntity__APIKeyEntity_user_storageLabel",
@ -22,7 +21,6 @@ FROM
"APIKeyEntity__APIKeyEntity_user"."deletedAt" AS "APIKeyEntity__APIKeyEntity_user_deletedAt",
"APIKeyEntity__APIKeyEntity_user"."status" AS "APIKeyEntity__APIKeyEntity_user_status",
"APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt",
"APIKeyEntity__APIKeyEntity_user"."memoriesEnabled" AS "APIKeyEntity__APIKeyEntity_user_memoriesEnabled",
"APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes",
"APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes"
FROM

View File

@ -17,7 +17,6 @@ FROM
"LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt",
"LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id",
"LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name",
"LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor",
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
@ -28,7 +27,6 @@ FROM
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
"LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
FROM
@ -89,7 +87,6 @@ SELECT
"LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt",
"LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id",
"LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name",
"LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor",
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
@ -100,7 +97,6 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
"LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
FROM
@ -128,7 +124,6 @@ SELECT
"LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt",
"LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id",
"LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name",
"LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor",
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
@ -139,7 +134,6 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
"LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
FROM
@ -166,7 +160,6 @@ SELECT
"LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt",
"LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id",
"LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name",
"LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor",
"LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin",
"LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email",
"LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel",
@ -177,7 +170,6 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt",
"LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status",
"LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt",
"LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled",
"LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes",
"LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes"
FROM

View File

@ -27,7 +27,6 @@ FROM
"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"."avatarColor" AS "SessionEntity__SessionEntity_user_avatarColor",
"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",
@ -38,7 +37,6 @@ FROM
"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"."memoriesEnabled" AS "SessionEntity__SessionEntity_user_memoriesEnabled",
"SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes",
"SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes"
FROM

View File

@ -147,7 +147,6 @@ FROM
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fps" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fps",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."avatarColor" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_avatarColor",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel",
@ -158,7 +157,6 @@ FROM
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes"
FROM
@ -252,7 +250,6 @@ SELECT
"SharedLinkEntity__SharedLinkEntity_album"."order" AS "SharedLinkEntity__SharedLinkEntity_album_order",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."avatarColor" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_avatarColor",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel",
@ -263,7 +260,6 @@ SELECT
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes"
FROM
@ -306,7 +302,6 @@ FROM
"SharedLinkEntity"."albumId" AS "SharedLinkEntity_albumId",
"SharedLinkEntity__SharedLinkEntity_user"."id" AS "SharedLinkEntity__SharedLinkEntity_user_id",
"SharedLinkEntity__SharedLinkEntity_user"."name" AS "SharedLinkEntity__SharedLinkEntity_user_name",
"SharedLinkEntity__SharedLinkEntity_user"."avatarColor" AS "SharedLinkEntity__SharedLinkEntity_user_avatarColor",
"SharedLinkEntity__SharedLinkEntity_user"."isAdmin" AS "SharedLinkEntity__SharedLinkEntity_user_isAdmin",
"SharedLinkEntity__SharedLinkEntity_user"."email" AS "SharedLinkEntity__SharedLinkEntity_user_email",
"SharedLinkEntity__SharedLinkEntity_user"."storageLabel" AS "SharedLinkEntity__SharedLinkEntity_user_storageLabel",
@ -317,7 +312,6 @@ FROM
"SharedLinkEntity__SharedLinkEntity_user"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_user_deletedAt",
"SharedLinkEntity__SharedLinkEntity_user"."status" AS "SharedLinkEntity__SharedLinkEntity_user_status",
"SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt",
"SharedLinkEntity__SharedLinkEntity_user"."memoriesEnabled" AS "SharedLinkEntity__SharedLinkEntity_user_memoriesEnabled",
"SharedLinkEntity__SharedLinkEntity_user"."quotaSizeInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaSizeInBytes",
"SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes"
FROM

View File

@ -4,7 +4,6 @@
SELECT
"UserEntity"."id" AS "UserEntity_id",
"UserEntity"."name" AS "UserEntity_name",
"UserEntity"."avatarColor" AS "UserEntity_avatarColor",
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
"UserEntity"."email" AS "UserEntity_email",
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
@ -15,7 +14,6 @@ SELECT
"UserEntity"."deletedAt" AS "UserEntity_deletedAt",
"UserEntity"."status" AS "UserEntity_status",
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
"UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
FROM
@ -51,7 +49,6 @@ LIMIT
SELECT
"user"."id" AS "user_id",
"user"."name" AS "user_name",
"user"."avatarColor" AS "user_avatarColor",
"user"."isAdmin" AS "user_isAdmin",
"user"."email" AS "user_email",
"user"."storageLabel" AS "user_storageLabel",
@ -62,7 +59,6 @@ SELECT
"user"."deletedAt" AS "user_deletedAt",
"user"."status" AS "user_status",
"user"."updatedAt" AS "user_updatedAt",
"user"."memoriesEnabled" AS "user_memoriesEnabled",
"user"."quotaSizeInBytes" AS "user_quotaSizeInBytes",
"user"."quotaUsageInBytes" AS "user_quotaUsageInBytes"
FROM
@ -75,7 +71,6 @@ WHERE
SELECT
"UserEntity"."id" AS "UserEntity_id",
"UserEntity"."name" AS "UserEntity_name",
"UserEntity"."avatarColor" AS "UserEntity_avatarColor",
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
"UserEntity"."email" AS "UserEntity_email",
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
@ -86,7 +81,6 @@ SELECT
"UserEntity"."deletedAt" AS "UserEntity_deletedAt",
"UserEntity"."status" AS "UserEntity_status",
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
"UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
FROM
@ -101,7 +95,6 @@ LIMIT
SELECT
"UserEntity"."id" AS "UserEntity_id",
"UserEntity"."name" AS "UserEntity_name",
"UserEntity"."avatarColor" AS "UserEntity_avatarColor",
"UserEntity"."isAdmin" AS "UserEntity_isAdmin",
"UserEntity"."email" AS "UserEntity_email",
"UserEntity"."storageLabel" AS "UserEntity_storageLabel",
@ -112,7 +105,6 @@ SELECT
"UserEntity"."deletedAt" AS "UserEntity_deletedAt",
"UserEntity"."status" AS "UserEntity_status",
"UserEntity"."updatedAt" AS "UserEntity_updatedAt",
"UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled",
"UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes",
"UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes"
FROM

View File

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetEntity } from 'src/entities/asset.entity';
import { UserMetadata, UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import {
IUserRepository,
@ -18,6 +19,7 @@ export class UserRepository implements IUserRepository {
constructor(
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(UserEntity) private userRepository: Repository<UserEntity>,
@InjectRepository(UserMetadataEntity) private metadataRepository: Repository<UserMetadataEntity>,
) {}
async get(userId: string, options: UserFindOptions): Promise<UserEntity | null> {
@ -25,6 +27,9 @@ export class UserRepository implements IUserRepository {
return this.userRepository.findOne({
where: { id: userId },
withDeleted: options.withDeleted,
relations: {
metadata: true,
},
});
}
@ -69,6 +74,9 @@ export class UserRepository implements IUserRepository {
order: {
createdAt: 'DESC',
},
relations: {
metadata: true,
},
});
}
@ -81,6 +89,13 @@ export class UserRepository implements IUserRepository {
return this.save({ ...user, id });
}
async upsertMetadata<T extends UserMetadataKey.PREFERENCES>(
id: string,
{ key, value }: { key: T; value: UserMetadata[T] },
) {
await this.metadataRepository.upsert({ userId: id, key, value }, { conflictPaths: { userId: true, key: true } });
}
async delete(user: UserEntity, hard?: boolean): Promise<UserEntity> {
return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user);
}
@ -142,6 +157,12 @@ export class UserRepository implements IUserRepository {
private async save(user: Partial<UserEntity>) {
const { id } = await this.userRepository.save(user);
return this.userRepository.findOneOrFail({ where: { id }, withDeleted: true });
return this.userRepository.findOneOrFail({
where: { id },
withDeleted: true,
relations: {
metadata: true,
},
});
}
}

View File

@ -4,6 +4,7 @@ import { Issuer, generators } from 'openid-client';
import { Socket } from 'socket.io';
import { AuthType } from 'src/constants';
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
@ -248,8 +249,13 @@ describe('AuthService', () => {
it('should sign up the admin', async () => {
userMock.getAdmin.mockResolvedValue(null);
userMock.create.mockResolvedValue({ ...dto, id: 'admin', createdAt: new Date('2021-01-01') } as UserEntity);
await expect(sut.adminSignUp(dto)).resolves.toEqual({
userMock.create.mockResolvedValue({
...dto,
id: 'admin',
createdAt: new Date('2021-01-01'),
metadata: [] as UserMetadataEntity[],
} as UserEntity);
await expect(sut.adminSignUp(dto)).resolves.toMatchObject({
avatarColor: expect.any(String),
id: 'admin',
createdAt: new Date('2021-01-01'),

View File

@ -1,6 +1,6 @@
import { BadRequestException } from '@nestjs/common';
import { PartnerResponseDto } from 'src/dtos/partner.dto';
import { UserAvatarColor } from 'src/entities/user.entity';
import { UserAvatarColor } from 'src/entities/user-metadata.entity';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface';
import { PartnerService } from 'src/services/partner.service';
@ -23,7 +23,7 @@ const responseDto = {
deletedAt: null,
updatedAt: new Date('2021-01-01'),
memoriesEnabled: true,
avatarColor: UserAvatarColor.PRIMARY,
avatarColor: UserAvatarColor.GRAY,
quotaSizeInBytes: null,
inTimeline: true,
quotaUsageInBytes: 0,

View File

@ -138,13 +138,17 @@ describe(UserService.name, () => {
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
id: userStub.user1.id,
storageLabel: null,
updatedAt: expect.any(Date),
});
});
it('should omit a storage label set by non-admin users', async () => {
userMock.update.mockResolvedValue(userStub.user1);
await sut.update({ user: userStub.user1 }, { id: userStub.user1.id, storageLabel: 'admin' });
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { id: userStub.user1.id });
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
id: userStub.user1.id,
updatedAt: expect.any(Date),
});
});
it('user can only update its information', async () => {
@ -174,6 +178,7 @@ describe(UserService.name, () => {
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
id: 'user-id',
email: 'updated@test.com',
updatedAt: expect.any(Date),
});
});
@ -210,6 +215,7 @@ describe(UserService.name, () => {
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
id: 'user-id',
shouldChangePassword: true,
updatedAt: expect.any(Date),
});
});
@ -231,7 +237,7 @@ describe(UserService.name, () => {
await sut.update(authStub.admin, dto);
expect(userMock.update).toHaveBeenCalledWith(userStub.admin.id, dto);
expect(userMock.update).toHaveBeenCalledWith(userStub.admin.id, { ...dto, updatedAt: expect.any(Date) });
});
it('should not let the another user become an admin', async () => {

View File

@ -6,6 +6,7 @@ import { UserCore } from 'src/cores/user.core';
import { AuthDto } from 'src/dtos/auth.dto';
import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto';
import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { UserMetadataKey } from 'src/entities/user-metadata.entity';
import { UserEntity, UserStatus } from 'src/entities/user.entity';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
@ -16,6 +17,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { getPreferences, getPreferencesPartial } from 'src/utils/preferences';
@Injectable()
export class UserService {
@ -61,9 +63,21 @@ export class UserService {
}
async create(dto: CreateUserDto): Promise<UserResponseDto> {
const user = await this.userCore.createUser(dto);
const tempPassword = user.shouldChangePassword ? dto.password : undefined;
if (dto.notify) {
const { memoriesEnabled, notify, ...rest } = dto;
let user = await this.userCore.createUser(rest);
// TODO remove and replace with entire dto.preferences config
if (memoriesEnabled === false) {
await this.userRepository.upsertMetadata(user.id, {
key: UserMetadataKey.PREFERENCES,
value: { memories: { enabled: false } },
});
user = await this.findOrFail(user.id, {});
}
const tempPassword = user.shouldChangePassword ? rest.password : undefined;
if (notify) {
await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id: user.id, tempPassword } });
}
return mapUser(user);
@ -76,7 +90,28 @@ export class UserService {
await this.userRepository.syncUsage(dto.id);
}
return this.userCore.updateUser(auth.user, dto.id, dto).then(mapUser);
// TODO replace with entire preferences object
if (dto.memoriesEnabled !== undefined || dto.avatarColor) {
const newPreferences = getPreferences(user);
if (dto.memoriesEnabled !== undefined) {
newPreferences.memories.enabled = dto.memoriesEnabled;
delete dto.memoriesEnabled;
}
if (dto.avatarColor) {
newPreferences.avatar.color = dto.avatarColor;
delete dto.avatarColor;
}
await this.userRepository.upsertMetadata(dto.id, {
key: UserMetadataKey.PREFERENCES,
value: getPreferencesPartial(user, newPreferences),
});
}
const updatedUser = await this.userCore.updateUser(auth.user, dto.id, dto);
return mapUser(updatedUser);
}
async delete(auth: AuthDto, id: string, dto: DeleteUserDto): Promise<UserResponseDto> {

View File

@ -0,0 +1,39 @@
import _ from 'lodash';
import { UserMetadataKey, UserPreferences, getDefaultPreferences } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { getKeysDeep } from 'src/utils/misc';
import { DeepPartial } from 'typeorm';
export const getPreferences = (user: UserEntity) => {
const preferences = getDefaultPreferences(user);
if (!user.metadata) {
return preferences;
}
const item = user.metadata.find(({ key }) => key === UserMetadataKey.PREFERENCES);
const partial = item?.value || {};
for (const property of getKeysDeep(partial)) {
_.set(preferences, property, _.get(partial, property));
}
return preferences;
};
export const getPreferencesPartial = (user: { email: string }, newPreferences: UserPreferences) => {
const defaultPreferences = getDefaultPreferences(user);
const partial: DeepPartial<UserPreferences> = {};
for (const property of getKeysDeep(defaultPreferences)) {
const newValue = _.get(newPreferences, property);
const isEmpty = newValue === undefined || newValue === null || newValue === '';
const defaultValue = _.get(defaultPreferences, property);
const isEqual = newValue === defaultValue || _.isEqual(newValue, defaultValue);
if (isEmpty || isEqual) {
continue;
}
_.set(partial, property, newValue);
}
return partial;
};

View File

@ -1,4 +1,5 @@
import { UserAvatarColor, UserEntity } from 'src/entities/user.entity';
import { UserAvatarColor, UserMetadataKey } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { authStub } from 'test/fixtures/auth.stub';
export const userDto = {
@ -39,8 +40,7 @@ export const userStub = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
avatarColor: UserAvatarColor.PRIMARY,
metadata: [],
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
}),
@ -57,8 +57,14 @@ export const userStub = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
avatarColor: UserAvatarColor.PRIMARY,
metadata: [
{
user: authStub.user1.user,
userId: authStub.user1.user.id,
key: UserMetadataKey.PREFERENCES,
value: { avatar: { color: UserAvatarColor.PRIMARY } },
},
],
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
}),
@ -75,8 +81,6 @@ export const userStub = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
avatarColor: UserAvatarColor.PRIMARY,
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
}),
@ -93,8 +97,6 @@ export const userStub = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
avatarColor: UserAvatarColor.PRIMARY,
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
}),
@ -111,8 +113,6 @@ export const userStub = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
avatarColor: UserAvatarColor.PRIMARY,
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
}),
@ -129,8 +129,6 @@ export const userStub = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
avatarColor: UserAvatarColor.PRIMARY,
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
}),

View File

@ -22,5 +22,6 @@ export const newUserRepositoryMock = (reset = true): Mocked<IUserRepository> =>
hasAdmin: vitest.fn(),
updateUsage: vitest.fn(),
syncUsage: vitest.fn(),
upsertMetadata: vitest.fn(),
};
};