1
0
forked from Cutlery/immich

crud implementation

This commit is contained in:
Alex Tran 2024-04-03 11:57:04 -05:00
parent ca06105d00
commit 683a56fc07
8 changed files with 182 additions and 84 deletions

View File

@ -332,15 +332,15 @@
] ]
} }
}, },
"/album/sub-album": { "/album/nested-album": {
"post": { "post": {
"operationId": "createSubAlbum", "operationId": "createNestedAlbum",
"parameters": [], "parameters": [],
"requestBody": { "requestBody": {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/CreateSubAlbumDto" "$ref": "#/components/schemas/CreateNestedAlbumDto"
} }
} }
}, },
@ -348,6 +348,13 @@
}, },
"responses": { "responses": {
"201": { "201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AlbumResponseDto"
}
}
},
"description": "" "description": ""
} }
}, },
@ -624,9 +631,9 @@
] ]
} }
}, },
"/album/{id}/sub-album": { "/album/{id}/nested-album": {
"get": { "get": {
"operationId": "getAlbumTree", "operationId": "getNestedAlbums",
"parameters": [ "parameters": [
{ {
"name": "id", "name": "id",
@ -640,6 +647,13 @@
], ],
"responses": { "responses": {
"200": { "200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NestedAlbumResponseDto"
}
}
},
"description": "" "description": ""
} }
}, },
@ -659,9 +673,9 @@
] ]
} }
}, },
"/album/{id}/sub-album/{childAlbumId}": { "/album/{id}/nested-album/{childAlbumId}": {
"delete": { "delete": {
"operationId": "removeSubAlbum", "operationId": "removeNestedAlbum",
"parameters": [ "parameters": [
{ {
"name": "childAlbumId", "name": "childAlbumId",
@ -683,6 +697,13 @@
], ],
"responses": { "responses": {
"200": { "200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AlbumResponseDto"
}
}
},
"description": "" "description": ""
} }
}, },
@ -7196,12 +7217,6 @@
}, },
"type": "array" "type": "array"
}, },
"childAlbums": {
"items": {
"$ref": "#/components/schemas/SubAlbumResponseDto"
},
"type": "array"
},
"createdAt": { "createdAt": {
"format": "date-time", "format": "date-time",
"type": "string" "type": "string"
@ -7235,12 +7250,6 @@
"ownerId": { "ownerId": {
"type": "string" "type": "string"
}, },
"parentAlbums": {
"items": {
"$ref": "#/components/schemas/SubAlbumResponseDto"
},
"type": "array"
},
"shared": { "shared": {
"type": "boolean" "type": "boolean"
}, },
@ -8145,6 +8154,22 @@
], ],
"type": "object" "type": "object"
}, },
"CreateNestedAlbumDto": {
"properties": {
"childId": {
"format": "uuid",
"type": "string"
},
"parentId": {
"type": "string"
}
},
"required": [
"childId",
"parentId"
],
"type": "object"
},
"CreateProfileImageDto": { "CreateProfileImageDto": {
"properties": { "properties": {
"file": { "file": {
@ -8172,22 +8197,6 @@
], ],
"type": "object" "type": "object"
}, },
"CreateSubAlbumDto": {
"properties": {
"childrenId": {
"format": "uuid",
"type": "string"
},
"parentId": {
"type": "string"
}
},
"required": [
"childrenId",
"parentId"
],
"type": "object"
},
"CreateTagDto": { "CreateTagDto": {
"properties": { "properties": {
"name": { "name": {
@ -9218,6 +9227,27 @@
], ],
"type": "string" "type": "string"
}, },
"NestedAlbumResponseDto": {
"properties": {
"children": {
"items": {
"$ref": "#/components/schemas/AlbumResponseDto"
},
"type": "array"
},
"parents": {
"items": {
"$ref": "#/components/schemas/AlbumResponseDto"
},
"type": "array"
}
},
"required": [
"children",
"parents"
],
"type": "object"
},
"OAuthAuthorizeResponseDto": { "OAuthAuthorizeResponseDto": {
"properties": { "properties": {
"url": { "url": {
@ -10347,26 +10377,6 @@
], ],
"type": "object" "type": "object"
}, },
"SubAlbumResponseDto": {
"properties": {
"albumName": {
"type": "string"
},
"albumThumbnailAssetId": {
"nullable": true,
"type": "string"
},
"id": {
"type": "string"
}
},
"required": [
"albumName",
"albumThumbnailAssetId",
"id"
],
"type": "object"
},
"SystemConfigDto": { "SystemConfigDto": {
"properties": { "properties": {
"ffmpeg": { "ffmpeg": {

View File

@ -6,8 +6,9 @@ import {
AlbumInfoDto, AlbumInfoDto,
AlbumResponseDto, AlbumResponseDto,
CreateAlbumDto, CreateAlbumDto,
CreateSubAlbumDto, CreateNestedAlbumDto,
GetAlbumsDto, GetAlbumsDto,
NestedAlbumResponseDto,
UpdateAlbumDto, UpdateAlbumDto,
} from 'src/dtos/album.dto'; } from 'src/dtos/album.dto';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
@ -98,22 +99,22 @@ export class AlbumController {
return this.service.removeUser(auth, id, userId); return this.service.removeUser(auth, id, userId);
} }
@Post('sub-album') @Post('nested-album')
createSubAlbum(@Auth() auth: AuthDto, @Body() dto: CreateSubAlbumDto) { createNestedAlbum(@Auth() auth: AuthDto, @Body() dto: CreateNestedAlbumDto): Promise<AlbumResponseDto> {
// TODO return this.service.createNestedAlbum(auth, dto.parentId, dto.childId);
} }
@Delete(':id/sub-album/:childAlbumId') @Delete(':id/nested-album/:childAlbumId')
removeSubAlbum( removeNestedAlbum(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Param('childAlbumId', new ParseMeUUIDPipe({ version: '4' })) childAlbumId: string, @Param('childAlbumId', new ParseMeUUIDPipe({ version: '4' })) childAlbumId: string,
) { ): Promise<AlbumResponseDto> {
// TODO return this.service.removeNestedAlbum(auth, id, childAlbumId);
} }
@Get(':id/sub-album') @Get(':id/nested-album')
getAlbumTree(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { getNestedAlbums(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<NestedAlbumResponseDto> {
// TODO return this.service.getNestedAlbums(auth, id);
} }
} }

View File

@ -83,12 +83,6 @@ export class AlbumCountResponseDto {
notShared!: number; notShared!: number;
} }
export class SubAlbumResponseDto {
id!: string;
albumName!: string;
albumThumbnailAssetId!: string | null;
}
export class AlbumResponseDto { export class AlbumResponseDto {
id!: string; id!: string;
ownerId!: string; ownerId!: string;
@ -111,17 +105,22 @@ export class AlbumResponseDto {
@Optional() @Optional()
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
order?: AssetOrder; order?: AssetOrder;
parentAlbums?: SubAlbumResponseDto[];
childAlbums?: SubAlbumResponseDto[];
} }
export class CreateSubAlbumDto { export class CreateNestedAlbumDto {
@IsString() @IsString()
parentId!: string; parentId!: string;
@ValidateUUID() @ValidateUUID()
childrenId!: string; childId!: string;
}
export class NestedAlbumResponseDto {
@ApiProperty()
parents!: AlbumResponseDto[];
@ApiProperty()
children!: AlbumResponseDto[];
} }
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => { export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => {

View File

@ -73,9 +73,9 @@ export class AlbumEntity {
@Entity('nested_albums') @Entity('nested_albums')
export class NestedAlbumEntity { export class NestedAlbumEntity {
@PrimaryColumn() @PrimaryColumn('uuid')
parentId!: string; parentId!: string;
@PrimaryColumn() @PrimaryColumn('uuid')
childId!: string; childId!: string;
} }

View File

@ -24,6 +24,11 @@ export interface AlbumAssets {
assetIds: string[]; assetIds: string[];
} }
export interface NestedAlbums {
parents: AlbumEntity[];
children: AlbumEntity[];
}
export interface IAlbumRepository extends IBulkAsset { export interface IAlbumRepository extends IBulkAsset {
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>; getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
getByIds(ids: string[]): Promise<AlbumEntity[]>; getByIds(ids: string[]): Promise<AlbumEntity[]>;
@ -45,4 +50,8 @@ export interface IAlbumRepository extends IBulkAsset {
update(album: Partial<AlbumEntity>): Promise<AlbumEntity>; update(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
delete(album: AlbumEntity): Promise<void>; delete(album: AlbumEntity): Promise<void>;
updateThumbnails(): Promise<number | undefined>; updateThumbnails(): Promise<number | undefined>;
createNestedAlbum(parentId: string, childId: string): Promise<AlbumEntity>;
removeNestedAlbum(parentId: string, childId: string): Promise<AlbumEntity>;
getNestedAlbums(id: string): Promise<NestedAlbums>;
} }

View File

@ -1,10 +1,10 @@
import { MigrationInterface, QueryRunner } from "typeorm"; import { MigrationInterface, QueryRunner } from "typeorm";
export class AddNestedAlbumTable1712155807366 implements MigrationInterface { export class AddNestedAlbumTable1712161154542 implements MigrationInterface {
name = 'AddNestedAlbumTable1712155807366' name = 'AddNestedAlbumTable1712161154542'
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "nested_albums" ("parentId" character varying NOT NULL, "childId" character varying NOT NULL, CONSTRAINT "PK_702e1e5d9ed4b85d3bdffc934bd" PRIMARY KEY ("parentId", "childId"))`); await queryRunner.query(`CREATE TABLE "nested_albums" ("parentId" uuid NOT NULL, "childId" uuid NOT NULL, CONSTRAINT "PK_702e1e5d9ed4b85d3bdffc934bd" PRIMARY KEY ("parentId", "childId"))`);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {

View File

@ -3,9 +3,15 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import _ from 'lodash'; import _ from 'lodash';
import { dataSource } from 'src/database.config'; import { dataSource } from 'src/database.config';
import { Chunked, ChunkedArray, DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from 'src/decorators'; import { Chunked, ChunkedArray, DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from 'src/decorators';
import { AlbumEntity } from 'src/entities/album.entity'; import { AlbumEntity, NestedAlbumEntity } from 'src/entities/album.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AlbumAsset, AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface'; import {
AlbumAsset,
AlbumAssetCount,
AlbumInfoOptions,
IAlbumRepository,
NestedAlbums,
} from 'src/interfaces/album.interface';
import { Instrumentation } from 'src/utils/instrumentation'; import { Instrumentation } from 'src/utils/instrumentation';
import { setUnion } from 'src/utils/set'; import { setUnion } from 'src/utils/set';
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm'; import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
@ -16,6 +22,7 @@ export class AlbumRepository implements IAlbumRepository {
constructor( constructor(
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>, @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(AlbumEntity) private repository: Repository<AlbumEntity>, @InjectRepository(AlbumEntity) private repository: Repository<AlbumEntity>,
@InjectRepository(NestedAlbumEntity) private nestedAlbumRepository: Repository<NestedAlbumEntity>,
@InjectDataSource() private dataSource: DataSource, @InjectDataSource() private dataSource: DataSource,
) {} ) {}
@ -331,4 +338,48 @@ export class AlbumRepository implements IAlbumRepository {
return result.affected; return result.affected;
} }
@GenerateSql()
async createNestedAlbum(parentId: string, childId: string): Promise<AlbumEntity> {
const nestedAlbum = new NestedAlbumEntity();
nestedAlbum.parentId = parentId;
nestedAlbum.childId = childId;
await this.nestedAlbumRepository.save(nestedAlbum);
return this.repository.findOneOrFail({
where: { id: childId },
});
}
@GenerateSql()
async removeNestedAlbum(parentId: string, childId: string): Promise<AlbumEntity> {
await this.nestedAlbumRepository.delete({ parentId, childId });
return this.repository.findOneOrFail({
where: { id: childId },
});
}
@GenerateSql()
async getNestedAlbums(id: string): Promise<NestedAlbums> {
const children = await this.repository
.createQueryBuilder('albums')
.innerJoin('nested_albums', 'nested', 'nested.childId = albums.id')
.leftJoinAndSelect('albums.owner', 'owner')
.where('nested.parentId = :id', { id })
.getMany();
const parents = await this.repository
.createQueryBuilder('albums')
.innerJoin('nested_albums', 'nested', 'nested.parentId = albums.id')
.leftJoinAndSelect('albums.owner', 'owner')
.where('nested.childId = :id', { id })
.getMany();
return {
parents,
children,
};
}
} }

View File

@ -7,6 +7,7 @@ import {
AlbumResponseDto, AlbumResponseDto,
CreateAlbumDto, CreateAlbumDto,
GetAlbumsDto, GetAlbumsDto,
NestedAlbumResponseDto as NestedAlbumsResponseDto,
UpdateAlbumDto, UpdateAlbumDto,
mapAlbum, mapAlbum,
mapAlbumWithAssets, mapAlbumWithAssets,
@ -266,6 +267,33 @@ export class AlbumService {
}); });
} }
async createNestedAlbum(auth: AuthDto, parentId: string, childId: string): Promise<AlbumResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_UPDATE, parentId);
const nestedAlbum = await this.albumRepository.createNestedAlbum(parentId, childId);
return mapAlbumWithoutAssets(nestedAlbum);
}
async removeNestedAlbum(auth: AuthDto, parentId: string, childId: string): Promise<AlbumResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_UPDATE, parentId);
const deletedNestedAlbum = await this.albumRepository.removeNestedAlbum(parentId, childId);
return mapAlbumWithoutAssets(deletedNestedAlbum);
}
async getNestedAlbums(auth: AuthDto, id: string): Promise<NestedAlbumsResponseDto> {
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
const { parents, children } = await this.albumRepository.getNestedAlbums(id);
return {
parents: parents.length > 0 ? parents.map(mapAlbumWithoutAssets) : [],
children: children.length > 0 ? children.map(mapAlbumWithoutAssets) : [],
};
}
private async findOrFail(id: string, options: AlbumInfoOptions) { private async findOrFail(id: string, options: AlbumInfoOptions) {
const album = await this.albumRepository.getById(id, options); const album = await this.albumRepository.getById(id, options);
if (!album) { if (!album) {