mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 14:41:59 +00:00
refactor(server): stack owner (#10900)
This commit is contained in:
parent
f6cafa3290
commit
e1f25b44d2
14 changed files with 46 additions and 34 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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[];
|
||||
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
22
server/src/migrations/1720207981949-AddStackOwner.ts
Normal file
22
server/src/migrations/1720207981949-AddStackOwner.ts
Normal 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"`);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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'], {
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
2
server/test/fixtures/asset.stub.ts
vendored
2
server/test/fixtures/asset.stub.ts
vendored
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -7,6 +7,5 @@ export const newStackRepositoryMock = (): Mocked<IStackRepository> => {
|
|||
update: vitest.fn(),
|
||||
delete: vitest.fn(),
|
||||
getById: vitest.fn(),
|
||||
deleteAll: vitest.fn(),
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue