feat(server,web): add force delete to immediately remove user (#7681)

* feat(server,web): add force delete to immediately remove user

* update wording on force delete confirmation

* fix force delete css

* PR feedback

* cleanup user service delete for force

* adding user status column

* some cleanup and tests

* more test fixes

* run npm run sql:generate

* chore: cleanup and websocket

* chore: linting

* userRepository.restore

* removed bad color class from delete-confirm-dialoge

* additional confirmation for user force delete

* shorten confirmation message

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Sam Holton 2024-03-08 17:49:39 -05:00 committed by GitHub
parent 9cb0a1ffbf
commit 7a4ae7d142
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 628 additions and 97 deletions

View file

@ -280,6 +280,11 @@ export class JobService {
}
break;
}
case JobName.USER_DELETION: {
this.communicationRepository.broadcast(ClientEvent.USER_DELETE, item.data.id);
break;
}
}
}
}

View file

@ -4,6 +4,7 @@ export const ICommunicationRepository = 'ICommunicationRepository';
export enum ClientEvent {
UPLOAD_SUCCESS = 'on_upload_success',
USER_DELETE = 'on_user_delete',
ASSET_DELETE = 'on_asset_delete',
ASSET_TRASH = 'on_asset_trash',
ASSET_UPDATE = 'on_asset_update',
@ -22,6 +23,7 @@ export enum ServerEvent {
export interface ClientEventMap {
[ClientEvent.UPLOAD_SUCCESS]: AssetResponseDto;
[ClientEvent.USER_DELETE]: string;
[ClientEvent.ASSET_DELETE]: string;
[ClientEvent.ASSET_TRASH]: string[];
[ClientEvent.ASSET_UPDATE]: AssetResponseDto;

View file

@ -32,7 +32,6 @@ export interface IUserRepository {
create(user: Partial<UserEntity>): Promise<UserEntity>;
update(id: string, user: Partial<UserEntity>): Promise<UserEntity>;
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
restore(user: UserEntity): Promise<UserEntity>;
updateUsage(id: string, delta: number): Promise<void>;
syncUsage(id?: string): Promise<void>;
}

View file

@ -0,0 +1,6 @@
import { ValidateBoolean } from '../../domain.util';
export class DeleteUserDto {
@ValidateBoolean({ optional: true })
force?: boolean;
}

View file

@ -1,3 +1,4 @@
export * from './create-profile-image.dto';
export * from './create-user.dto';
export * from './delete-user.dto';
export * from './update-user.dto';

View file

@ -1,4 +1,4 @@
import { UserAvatarColor, UserEntity } from '@app/infra/entities';
import { UserAvatarColor, UserEntity, UserStatus } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum } from 'class-validator';
@ -33,6 +33,8 @@ export class UserResponseDto extends UserDto {
quotaSizeInBytes!: number | null;
@ApiProperty({ type: 'integer', format: 'int64' })
quotaUsageInBytes!: number | null;
@ApiProperty({ enumName: 'UserStatus', enum: UserStatus })
status!: string;
}
export const mapSimpleUser = (entity: UserEntity): UserDto => {
@ -58,5 +60,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
memoriesEnabled: entity.memoriesEnabled,
quotaSizeInBytes: entity.quotaSizeInBytes,
quotaUsageInBytes: entity.quotaUsageInBytes,
status: entity.status,
};
}

View file

@ -1,4 +1,4 @@
import { UserEntity } from '@app/infra/entities';
import { UserEntity, UserStatus } from '@app/infra/entities';
import {
BadRequestException,
ForbiddenException,
@ -243,16 +243,14 @@ describe(UserService.name, () => {
it('should throw error if user could not be found', async () => {
when(userMock.get).calledWith(userStub.admin.id, { withDeleted: true }).mockResolvedValue(null);
await expect(sut.restore(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException);
expect(userMock.restore).not.toHaveBeenCalled();
expect(userMock.update).not.toHaveBeenCalled();
});
it('should restore an user', async () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.restore.mockResolvedValue(userStub.user1);
userMock.update.mockResolvedValue(userStub.user1);
await expect(sut.restore(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1));
expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: true });
expect(userMock.restore).toHaveBeenCalledWith(userStub.user1);
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { status: UserStatus.ACTIVE, deletedAt: null });
});
});
@ -260,27 +258,47 @@ describe(UserService.name, () => {
it('should throw error if user could not be found', async () => {
userMock.get.mockResolvedValue(null);
await expect(sut.delete(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException);
await expect(sut.delete(authStub.admin, userStub.admin.id, {})).rejects.toThrowError(BadRequestException);
expect(userMock.delete).not.toHaveBeenCalled();
});
it('cannot delete admin user', async () => {
await expect(sut.delete(authStub.admin, userStub.admin.id)).rejects.toBeInstanceOf(ForbiddenException);
await expect(sut.delete(authStub.admin, userStub.admin.id, {})).rejects.toBeInstanceOf(ForbiddenException);
});
it('should require the auth user be an admin', async () => {
await expect(sut.delete(authStub.user1, authStub.admin.user.id)).rejects.toBeInstanceOf(ForbiddenException);
await expect(sut.delete(authStub.user1, authStub.admin.user.id, {})).rejects.toBeInstanceOf(ForbiddenException);
expect(userMock.delete).not.toHaveBeenCalled();
});
it('should delete user', async () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.delete.mockResolvedValue(userStub.user1);
userMock.update.mockResolvedValue(userStub.user1);
await expect(sut.delete(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUser(userStub.user1));
expect(userMock.get).toHaveBeenCalledWith(userStub.user1.id, {});
expect(userMock.delete).toHaveBeenCalledWith(userStub.user1);
await expect(sut.delete(authStub.admin, userStub.user1.id, {})).resolves.toEqual(mapUser(userStub.user1));
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
status: UserStatus.DELETED,
deletedAt: expect.any(Date),
});
});
it('should force delete user', async () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.update.mockResolvedValue(userStub.user1);
await expect(sut.delete(authStub.admin, userStub.user1.id, { force: true })).resolves.toEqual(
mapUser(userStub.user1),
);
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
status: UserStatus.REMOVING,
deletedAt: expect.any(Date),
});
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.USER_DELETION,
data: { id: userStub.user1.id, force: true },
});
});
});

View file

@ -1,4 +1,4 @@
import { UserEntity } from '@app/infra/entities';
import { UserEntity, UserStatus } from '@app/infra/entities';
import { ImmichLogger } from '@app/infra/logger';
import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { DateTime } from 'luxon';
@ -18,7 +18,7 @@ import {
} from '../repositories';
import { StorageCore, StorageFolder } from '../storage';
import { SystemConfigCore } from '../system-config/system-config.core';
import { CreateUserDto, UpdateUserDto } from './dto';
import { CreateUserDto, DeleteUserDto, UpdateUserDto } from './dto';
import { CreateProfileImageResponseDto, UserResponseDto, mapCreateProfileImageResponse, mapUser } from './response-dto';
import { UserCore } from './user.core';
@ -73,22 +73,29 @@ export class UserService {
return this.userCore.updateUser(auth.user, dto.id, dto).then(mapUser);
}
async delete(auth: AuthDto, id: string): Promise<UserResponseDto> {
const user = await this.findOrFail(id, {});
if (user.isAdmin) {
async delete(auth: AuthDto, id: string, dto: DeleteUserDto): Promise<UserResponseDto> {
const { force } = dto;
const { isAdmin } = await this.findOrFail(id, {});
if (isAdmin) {
throw new ForbiddenException('Cannot delete admin user');
}
await this.albumRepository.softDeleteAll(id);
return this.userRepository.delete(user).then(mapUser);
const status = force ? UserStatus.REMOVING : UserStatus.DELETED;
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
if (force) {
await this.jobRepository.queue({ name: JobName.USER_DELETION, data: { id: user.id, force } });
}
return mapUser(user);
}
async restore(auth: AuthDto, id: string): Promise<UserResponseDto> {
let user = await this.findOrFail(id, { withDeleted: true });
user = await this.userRepository.restore(user);
await this.findOrFail(id, { withDeleted: true });
await this.albumRepository.restoreAll(id);
return mapUser(user);
return this.userRepository.update(id, { deletedAt: null, status: UserStatus.ACTIVE }).then(mapUser);
}
async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise<CreateProfileImageResponseDto> {
@ -154,7 +161,7 @@ export class UserService {
return true;
}
async handleUserDelete({ id }: IEntityJob) {
async handleUserDelete({ id, force }: IEntityJob) {
const config = await this.configCore.getConfig();
const user = await this.userRepository.get(id, { withDeleted: true });
if (!user) {
@ -162,7 +169,7 @@ export class UserService {
}
// just for extra protection here
if (!this.isReadyForDeletion(user, config.user.deleteDelay)) {
if (!force && !this.isReadyForDeletion(user, config.user.deleteDelay)) {
this.logger.warn(`Skipped user that was not ready for deletion: id=${id}`);
return false;
}

View file

@ -3,6 +3,7 @@ import {
CreateUserDto as CreateDto,
CreateProfileImageDto,
CreateProfileImageResponseDto,
DeleteUserDto,
UpdateUserDto as UpdateDto,
UserResponseDto,
UserService,
@ -66,8 +67,12 @@ export class UserController {
@AdminRoute()
@Delete(':id')
deleteUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.delete(auth, id);
deleteUser(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: DeleteUserDto,
): Promise<UserResponseDto> {
return this.service.delete(auth, id, dto);
}
@AdminRoute()

View file

@ -23,6 +23,12 @@ export enum UserAvatarColor {
AMBER = 'amber',
}
export enum UserStatus {
ACTIVE = 'active',
REMOVING = 'removing',
DELETED = 'deleted',
}
@Entity('users')
export class UserEntity {
@PrimaryGeneratedColumn('uuid')
@ -61,6 +67,9 @@ export class UserEntity {
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt!: Date | null;
@Column({ type: 'varchar', default: UserStatus.ACTIVE })
status!: UserStatus;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date;

View file

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddUserStatus1709870213078 implements MigrationInterface {
name = 'AddUserStatus1709870213078'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD "status" character varying NOT NULL DEFAULT 'active'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "status"`);
}
}

View file

@ -77,10 +77,6 @@ export class UserRepository implements IUserRepository {
return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user);
}
async restore(user: UserEntity): Promise<UserEntity> {
return this.userRepository.recover(user);
}
@GenerateSql()
async getUserStats(): Promise<UserStatsQueryResponse[]> {
const stats = await this.userRepository
@ -135,6 +131,6 @@ export class UserRepository implements IUserRepository {
private async save(user: Partial<UserEntity>) {
const { id } = await this.userRepository.save(user);
return this.userRepository.findOneByOrFail({ id });
return this.userRepository.findOneOrFail({ where: { id }, withDeleted: true });
}
}

View file

@ -26,6 +26,7 @@ FROM
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"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",
@ -41,6 +42,7 @@ FROM
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -100,6 +102,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"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",
@ -115,6 +118,7 @@ SELECT
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -156,6 +160,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"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",
@ -171,6 +176,7 @@ SELECT
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -284,6 +290,7 @@ SELECT
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -311,6 +318,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"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",
@ -355,6 +363,7 @@ SELECT
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -382,6 +391,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"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",
@ -463,6 +473,7 @@ SELECT
"AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword",
"AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt",
"AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."status" AS "AlbumEntity__AlbumEntity_sharedUsers_status",
"AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt",
"AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled",
"AlbumEntity__AlbumEntity_sharedUsers"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_sharedUsers_quotaSizeInBytes",
@ -490,6 +501,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"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",
@ -552,6 +564,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
"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",

View file

@ -20,6 +20,7 @@ FROM
"APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword",
"APIKeyEntity__APIKeyEntity_user"."createdAt" AS "APIKeyEntity__APIKeyEntity_user_createdAt",
"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",

View file

@ -28,6 +28,7 @@ FROM
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"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",
@ -143,6 +144,7 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"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",
@ -188,6 +190,7 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"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",
@ -227,6 +230,7 @@ SELECT
"LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword",
"LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt",
"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",

View file

@ -155,6 +155,7 @@ FROM
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
"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",
@ -258,6 +259,7 @@ SELECT
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt",
"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",
@ -311,6 +313,7 @@ FROM
"SharedLinkEntity__SharedLinkEntity_user"."shouldChangePassword" AS "SharedLinkEntity__SharedLinkEntity_user_shouldChangePassword",
"SharedLinkEntity__SharedLinkEntity_user"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_user_createdAt",
"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",

View file

@ -13,6 +13,7 @@ SELECT
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
"UserEntity"."createdAt" AS "UserEntity_createdAt",
"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",
@ -59,6 +60,7 @@ SELECT
"user"."shouldChangePassword" AS "user_shouldChangePassword",
"user"."createdAt" AS "user_createdAt",
"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",
@ -82,6 +84,7 @@ SELECT
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
"UserEntity"."createdAt" AS "UserEntity_createdAt",
"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",
@ -107,6 +110,7 @@ SELECT
"UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword",
"UserEntity"."createdAt" AS "UserEntity_createdAt",
"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",

View file

@ -23,6 +23,7 @@ FROM
"UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword",
"UserTokenEntity__UserTokenEntity_user"."createdAt" AS "UserTokenEntity__UserTokenEntity_user_createdAt",
"UserTokenEntity__UserTokenEntity_user"."deletedAt" AS "UserTokenEntity__UserTokenEntity_user_deletedAt",
"UserTokenEntity__UserTokenEntity_user"."status" AS "UserTokenEntity__UserTokenEntity_user_status",
"UserTokenEntity__UserTokenEntity_user"."updatedAt" AS "UserTokenEntity__UserTokenEntity_user_updatedAt",
"UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled",
"UserTokenEntity__UserTokenEntity_user"."quotaSizeInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaSizeInBytes",