feat(server/web): album description (#3558)

* feat(server): add album description

* chore: open api

* fix: tests

* show and edit description on the web

* fix test

* remove unused code

* type event

* format fix

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen
2023-08-05 22:43:26 -04:00
committed by GitHub
parent deaf81e2a4
commit 2f26a7edae
28 changed files with 287 additions and 41 deletions

View File

@@ -4754,6 +4754,9 @@
"format": "date-time",
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
@@ -4786,6 +4789,7 @@
"id",
"ownerId",
"albumName",
"description",
"createdAt",
"updatedAt",
"albumThumbnailAssetId",
@@ -5264,6 +5268,9 @@
},
"type": "array"
},
"description": {
"type": "string"
},
"sharedWithUserIds": {
"items": {
"format": "uuid",
@@ -6903,6 +6910,9 @@
"albumThumbnailAssetId": {
"format": "uuid",
"type": "string"
},
"description": {
"type": "string"
}
},
"type": "object"

View File

@@ -7,6 +7,7 @@ export class AlbumResponseDto {
id!: string;
ownerId!: string;
albumName!: string;
description!: string;
createdAt!: Date;
updatedAt!: Date;
albumThumbnailAssetId!: string | null;
@@ -19,7 +20,7 @@ export class AlbumResponseDto {
lastModifiedAssetTimestamp?: Date;
}
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
const _map = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => {
const sharedUsers: UserResponseDto[] = [];
entity.sharedUsers?.forEach((user) => {
@@ -29,6 +30,7 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
return {
albumName: entity.albumName,
description: entity.description,
albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
@@ -37,33 +39,13 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
owner: mapUser(entity.owner),
sharedUsers,
shared: sharedUsers.length > 0 || entity.sharedLinks?.length > 0,
assets: entity.assets?.map((asset) => mapAsset(asset)) || [],
assets: withAssets ? entity.assets?.map((asset) => mapAsset(asset)) || [] : [],
assetCount: entity.assets?.length || 0,
};
}
};
export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto {
const sharedUsers: UserResponseDto[] = [];
entity.sharedUsers?.forEach((user) => {
const userDto = mapUser(user);
sharedUsers.push(userDto);
});
return {
albumName: entity.albumName,
albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
id: entity.id,
ownerId: entity.ownerId,
owner: mapUser(entity.owner),
sharedUsers,
shared: sharedUsers.length > 0 || entity.sharedLinks?.length > 0,
assets: [],
assetCount: entity.assets?.length || 0,
};
}
export const mapAlbum = (entity: AlbumEntity) => _map(entity, true);
export const mapAlbumExcludeAssetInfo = (entity: AlbumEntity) => _map(entity, false);
export class AlbumCountResponseDto {
@ApiProperty({ type: 'integer' })

View File

@@ -156,6 +156,7 @@ describe(AlbumService.name, () => {
await expect(sut.create(authStub.admin, { albumName: 'Empty album' })).resolves.toEqual({
albumName: 'Empty album',
description: '',
albumThumbnailAssetId: null,
assetCount: 0,
assets: [],

View File

@@ -94,6 +94,7 @@ export class AlbumService {
const album = await this.albumRepository.create({
ownerId: authUser.id,
albumName: dto.albumName,
description: dto.description,
sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value } as UserEntity)) ?? [],
assets: (dto.assetIds || []).map((id) => ({ id } as AssetEntity)),
albumThumbnailAssetId: dto.assetIds?.[0] || null,
@@ -118,6 +119,7 @@ export class AlbumService {
const updatedAlbum = await this.albumRepository.update({
id: album.id,
albumName: dto.albumName,
description: dto.description,
albumThumbnailAssetId: dto.albumThumbnailAssetId,
});

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { ValidateUUID } from '../../domain.util';
export class CreateAlbumDto {
@@ -8,6 +8,10 @@ export class CreateAlbumDto {
@ApiProperty()
albumName!: string;
@IsString()
@IsOptional()
description?: string;
@ValidateUUID({ optional: true, each: true })
sharedWithUserIds?: string[];

View File

@@ -1,12 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from 'class-validator';
import { IsOptional, IsString } from 'class-validator';
import { ValidateUUID } from '../../domain.util';
export class UpdateAlbumDto {
@IsOptional()
@ApiProperty()
@IsString()
albumName?: string;
@IsOptional()
@IsString()
description?: string;
@ValidateUUID({ optional: true })
albumThumbnailAssetId?: string;
}

View File

@@ -5,8 +5,8 @@ import {
AuthUserDto,
BulkIdResponseDto,
BulkIdsDto,
CreateAlbumDto,
UpdateAlbumDto,
CreateAlbumDto as CreateDto,
UpdateAlbumDto as UpdateDto,
} from '@app/domain';
import { GetAlbumsDto } from '@app/domain/album/dto/get-albums.dto';
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
@@ -34,7 +34,7 @@ export class AlbumController {
}
@Post()
createAlbum(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateAlbumDto) {
createAlbum(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto) {
return this.service.create(authUser, dto);
}
@@ -45,7 +45,7 @@ export class AlbumController {
}
@Patch(':id')
updateAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateAlbumDto) {
updateAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto) {
return this.service.update(authUser, id, dto);
}

View File

@@ -27,6 +27,9 @@ export class AlbumEntity {
@Column({ default: 'Untitled Album' })
albumName!: string;
@Column({ type: 'text', default: '' })
description!: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date;

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddAlbumDescription1691209138541 implements MigrationInterface {
name = 'AddAlbumDescription1691209138541';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "albums" ADD "description" text NOT NULL DEFAULT ''`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "description"`);
}
}

View File

@@ -234,7 +234,7 @@ export class TypesenseRepository implements ISearchRepository {
.documents()
.search({
q: query,
query_by: 'albumName',
query_by: ['albumName', 'description'].join(','),
filter_by: this.getAlbumFilters(filters),
});

View File

@@ -1,11 +1,12 @@
import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
export const albumSchemaVersion = 1;
export const albumSchemaVersion = 2;
export const albumSchema: CollectionCreateSchema = {
name: `albums-v${albumSchemaVersion}`,
fields: [
{ name: 'ownerId', type: 'string', facet: false },
{ name: 'albumName', type: 'string', facet: false, sort: true },
{ name: 'description', type: 'string', facet: false },
{ name: 'createdAt', type: 'string', facet: false, sort: true },
{ name: 'updatedAt', type: 'string', facet: false, sort: true },
],

View File

@@ -4,7 +4,7 @@ import { SharedLinkType } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { errorStub } from '../fixtures';
import { errorStub, uuidStub } from '../fixtures';
import { api, db } from '../test-utils';
const user1SharedUser = 'user1SharedUser';
@@ -193,6 +193,7 @@ describe(`${AlbumController.name} (e2e)`, () => {
updatedAt: expect.any(String),
ownerId: user1.userId,
albumName: 'New album',
description: '',
albumThumbnailAssetId: null,
shared: false,
sharedUsers: [],
@@ -202,4 +203,32 @@ describe(`${AlbumController.name} (e2e)`, () => {
});
});
});
describe('PATCH /album/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server)
.patch(`/album/${uuidStub.notFound}`)
.send({ albumName: 'New album name' });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should update an album', async () => {
const album = await api.albumApi.create(server, user1.accessToken, { albumName: 'New album' });
const { status, body } = await request(server)
.patch(`/album/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
albumName: 'New album name',
description: 'An album description',
});
expect(status).toBe(200);
expect(body).toEqual({
...album,
updatedAt: expect.any(String),
albumName: 'New album name',
description: 'An album description',
});
});
});
});

View File

@@ -7,6 +7,7 @@ export const albumStub = {
empty: Object.freeze<AlbumEntity>({
id: 'album-1',
albumName: 'Empty album',
description: '',
ownerId: authStub.admin.id,
owner: userStub.admin,
assets: [],
@@ -20,6 +21,7 @@ export const albumStub = {
sharedWithUser: Object.freeze<AlbumEntity>({
id: 'album-2',
albumName: 'Empty album shared with user',
description: '',
ownerId: authStub.admin.id,
owner: userStub.admin,
assets: [],
@@ -33,6 +35,7 @@ export const albumStub = {
sharedWithMultiple: Object.freeze<AlbumEntity>({
id: 'album-3',
albumName: 'Empty album shared with users',
description: '',
ownerId: authStub.admin.id,
owner: userStub.admin,
assets: [],
@@ -46,6 +49,7 @@ export const albumStub = {
sharedWithAdmin: Object.freeze<AlbumEntity>({
id: 'album-3',
albumName: 'Empty album shared with admin',
description: '',
ownerId: authStub.user1.id,
owner: userStub.user1,
assets: [],
@@ -59,6 +63,7 @@ export const albumStub = {
oneAsset: Object.freeze<AlbumEntity>({
id: 'album-4',
albumName: 'Album with one asset',
description: '',
ownerId: authStub.admin.id,
owner: userStub.admin,
assets: [assetStub.image],
@@ -72,6 +77,7 @@ export const albumStub = {
twoAssets: Object.freeze<AlbumEntity>({
id: 'album-4a',
albumName: 'Album with two assets',
description: '',
ownerId: authStub.admin.id,
owner: userStub.admin,
assets: [assetStub.image, assetStub.withLocation],
@@ -85,6 +91,7 @@ export const albumStub = {
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5',
albumName: 'Empty album with invalid thumbnail',
description: '',
ownerId: authStub.admin.id,
owner: userStub.admin,
assets: [],
@@ -98,6 +105,7 @@ export const albumStub = {
emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5',
albumName: 'Empty album with invalid thumbnail',
description: '',
ownerId: authStub.admin.id,
owner: userStub.admin,
assets: [],
@@ -111,6 +119,7 @@ export const albumStub = {
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6',
albumName: 'Album with one asset and invalid thumbnail',
description: '',
ownerId: authStub.admin.id,
owner: userStub.admin,
assets: [assetStub.image],
@@ -124,6 +133,7 @@ export const albumStub = {
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6',
albumName: 'Album with one asset and invalid thumbnail',
description: '',
ownerId: authStub.admin.id,
owner: userStub.admin,
assets: [assetStub.image],

View File

@@ -68,6 +68,7 @@ const assetResponse: AssetResponseDto = {
const albumResponse: AlbumResponseDto = {
albumName: 'Test Album',
description: '',
albumThumbnailAssetId: null,
createdAt: today,
updatedAt: today,
@@ -146,6 +147,7 @@ export const sharedLinkStub = {
ownerId: authStub.admin.id,
owner: userStub.admin,
albumName: 'Test Album',
description: '',
createdAt: today,
updatedAt: today,
albumThumbnailAsset: null,