1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-24 08:52:28 +02:00

refactor(server): stack owner (#10900)

This commit is contained in:
Jason Rasmussen 2024-07-05 16:16:53 -04:00 committed by GitHub
parent f6cafa3290
commit e1f25b44d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 46 additions and 34 deletions

View File

@ -152,10 +152,6 @@ export const utils = {
const sql: string[] = [];
if (tables.includes('asset_stack')) {
sql.push('UPDATE "assets" SET "stackId" = NULL;');
}
for (const table of tables) {
if (table === 'system_metadata') {
// prevent reverse geocoder from being re-initialized

View File

@ -4,7 +4,7 @@ import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from '
import { UserAvatarColor, UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity';
import { UserEntity, UserStatus } from 'src/entities/user.entity';
import { getPreferences } from 'src/utils/preferences';
import { Optional, toEmail, toSanitized, ValidateBoolean } from 'src/validation';
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
export class UserUpdateMeDto {
@Optional()

View File

@ -1,11 +1,18 @@
import { AssetEntity } from 'src/entities/asset.entity';
import { Column, Entity, JoinColumn, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import { UserEntity } from 'src/entities/user.entity';
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity('asset_stack')
export class StackEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
owner!: UserEntity;
@Column()
ownerId!: string;
@OneToMany(() => AssetEntity, (asset) => asset.stack)
assets!: AssetEntity[];

View File

@ -3,9 +3,8 @@ import { StackEntity } from 'src/entities/stack.entity';
export const IStackRepository = 'IStackRepository';
export interface IStackRepository {
create(stack: Partial<StackEntity>): Promise<StackEntity>;
create(stack: Partial<StackEntity> & { ownerId: string }): Promise<StackEntity>;
update(stack: Pick<StackEntity, 'id'> & Partial<StackEntity>): Promise<StackEntity>;
delete(id: string): Promise<void>;
getById(id: string): Promise<StackEntity | null>;
deleteAll(userId: string): Promise<void>;
}

View File

@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddStackOwner1720207981949 implements MigrationInterface {
name = 'AddStackOwner1720207981949'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "asset_stack" ADD "ownerId" uuid`);
await queryRunner.query(`
UPDATE "asset_stack" stack
SET "ownerId" = asset."ownerId"
FROM "assets" asset
WHERE stack."primaryAssetId" = asset."id"
`)
await queryRunner.query('ALTER TABLE "asset_stack" ALTER COLUMN "ownerId" SET NOT NULL')
await queryRunner.query(`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_c05079e542fd74de3b5ecb5c1c8" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "asset_stack" DROP CONSTRAINT "FK_c05079e542fd74de3b5ecb5c1c8"`);
await queryRunner.query(`ALTER TABLE "asset_stack" DROP COLUMN "ownerId"`);
}
}

View File

@ -205,6 +205,7 @@ SELECT
"8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden",
"AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id",
"AssetEntity__AssetEntity_stack"."ownerId" AS "AssetEntity__AssetEntity_stack_ownerId",
"AssetEntity__AssetEntity_stack"."primaryAssetId" AS "AssetEntity__AssetEntity_stack_primaryAssetId",
"bd93d5747511a4dad4923546c51365bf1a803774"."id" AS "bd93d5747511a4dad4923546c51365bf1a803774_id",
"bd93d5747511a4dad4923546c51365bf1a803774"."deviceAssetId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceAssetId",
@ -629,6 +630,7 @@ SELECT
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
@ -769,6 +771,7 @@ SELECT
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
@ -885,6 +888,7 @@ SELECT
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
@ -1051,6 +1055,7 @@ SELECT
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
"stack"."primaryAssetId" AS "stack_primaryAssetId"
FROM
"assets" "asset"
@ -1126,6 +1131,7 @@ SELECT
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
"stack"."primaryAssetId" AS "stack_primaryAssetId"
FROM
"assets" "asset"

View File

@ -37,6 +37,7 @@ FROM
"asset"."stackId" AS "asset_stackId",
"asset"."duplicateId" AS "asset_duplicateId",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
@ -133,6 +134,7 @@ SELECT
"asset"."stackId" AS "asset_stackId",
"asset"."duplicateId" AS "asset_duplicateId",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",

View File

@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { StackEntity } from 'src/entities/stack.entity';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { Instrumentation } from 'src/utils/instrumentation';
import { In, Repository } from 'typeorm';
import { Repository } from 'typeorm';
@Instrumentation()
@Injectable()
@ -34,13 +34,6 @@ export class StackRepository implements IStackRepository {
});
}
async deleteAll(userId: string): Promise<void> {
// TODO add owner to stack entity
const stacks = await this.repository.find({ where: { primaryAsset: { ownerId: userId } } });
const stackIds = new Set(stacks.map((stack) => stack.id));
await this.repository.delete({ id: In([...stackIds]) });
}
private async save(entity: Partial<StackEntity>) {
const { id } = await this.repository.save(entity);
return this.repository.findOneOrFail({

View File

@ -350,6 +350,7 @@ describe(AssetService.name, () => {
expect(stackMock.delete).toHaveBeenCalledWith('stack-1');
expect(stackMock.create).toHaveBeenCalledWith({
assets: [{ id: 'child-1' }, { id: 'parent' }, { id: 'child-1' }, { id: 'child-2' }],
ownerId: 'user-id',
primaryAssetId: 'parent',
});
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'parent', 'child-1', 'child-2'], {

View File

@ -219,6 +219,7 @@ export class AssetService {
} else {
stack = await this.stackRepository.create({
primaryAssetId: primaryAsset.id,
ownerId: primaryAsset.ownerId,
assets: ids.map((id) => ({ id }) as AssetEntity),
});
}

View File

@ -5,7 +5,6 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
@ -18,7 +17,6 @@ import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newStackRepositoryMock } from 'test/repositories/stack.repository.mock';
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
@ -37,7 +35,6 @@ describe(UserService.name, () => {
let albumMock: Mocked<IAlbumRepository>;
let jobMock: Mocked<IJobRepository>;
let stackMock: Mocked<IStackRepository>;
let storageMock: Mocked<IStorageRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let loggerMock: Mocked<ILoggerRepository>;
@ -47,21 +44,11 @@ describe(UserService.name, () => {
systemMock = newSystemMetadataRepositoryMock();
cryptoRepositoryMock = newCryptoRepositoryMock();
jobMock = newJobRepositoryMock();
stackMock = newStackRepositoryMock();
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new UserService(
albumMock,
cryptoRepositoryMock,
jobMock,
stackMock,
storageMock,
systemMock,
userMock,
loggerMock,
);
sut = new UserService(albumMock, cryptoRepositoryMock, jobMock, storageMock, systemMock, userMock, loggerMock);
userMock.get.mockImplementation((userId) =>
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),

View File

@ -15,7 +15,6 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
@ -30,7 +29,6 @@ export class UserService {
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IStackRepository) private stackRepository: IStackRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@ -213,7 +211,6 @@ export class UserService {
}
this.logger.warn(`Removing user from database: ${user.id}`);
await this.stackRepository.deleteAll(user.id);
await this.albumRepository.deleteAll(user.id);
await this.userRepository.delete(user, true);

View File

@ -10,6 +10,8 @@ export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity =
return {
id: stackId,
assets: assets,
owner: assets[0].owner,
ownerId: assets[0].ownerId,
primaryAsset: assets[0],
primaryAssetId: assets[0].id,
};

View File

@ -7,6 +7,5 @@ export const newStackRepositoryMock = (): Mocked<IStackRepository> => {
update: vitest.fn(),
delete: vitest.fn(),
getById: vitest.fn(),
deleteAll: vitest.fn(),
};
};