Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 89 additions & 1 deletion src/collection/collection.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { CollectionService } from './collection.service';
import { AddToCollectionDto } from './dto/add-to-collection.dto';
import { AddToWantlistDto } from './dto/add-to-wantlist.dto';
import { AddToSuggestionsDto } from './dto/add-to-suggestions.dto';
import {
CollectionQueryDto,
WantlistQueryDto,
Expand Down Expand Up @@ -98,6 +99,37 @@ export class CollectionController {
);
}

@Get(':userId/suggestions')
@ApiOperation({ summary: 'Get user suggestions' })
@ApiParam({ name: 'userId', description: 'User ID' })
@ApiResponse({
status: 200,
description: 'User suggestions retrieved successfully',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - Invalid or missing API key',
})
@ApiResponse({
status: 404,
description: 'User not found',
})
async getUserSuggestions(
@Param('userId') userId: string,
@Query() query: CollectionQueryDto,
) {
this.logger.log(
`Getting suggestions for user ${userId} - sort: ${query.sort_by} ${query.sort_order}`,
);
return this.collectionService.getUserSuggestions(
userId,
query.limit,
query.offset,
query.sort_by,
query.sort_order,
);
}

@Get(':userId/stats')
@ApiOperation({ summary: 'Get user collection and wantlist stats' })
@ApiParam({ name: 'userId', description: 'User ID' })
Expand Down Expand Up @@ -125,7 +157,7 @@ export class CollectionController {
status: 401,
description: 'Unauthorized - Invalid or missing API key',
})
async getSortOptions() {
getSortOptions() {
this.logger.log('Getting available sort options');
return {
collection: this.collectionService.getCollectionSortOptions(),
Expand Down Expand Up @@ -193,6 +225,36 @@ export class CollectionController {
return this.collectionService.addToWantlist(userId, data);
}

@Post(':userId/suggestions')
@ApiOperation({ summary: 'Add release to suggestions' })
@ApiParam({ name: 'userId', description: 'User ID' })
@ApiBody({ type: AddToSuggestionsDto })
@ApiResponse({
status: 201,
description: 'Release added to suggestions successfully',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - Invalid or missing API key',
})
@ApiResponse({
status: 409,
description: 'Release already in suggestions',
})
@ApiResponse({
status: 400,
description: 'Invalid request data',
})
async addToSuggestions(
@Param('userId') userId: string,
@Body() data: AddToSuggestionsDto,
) {
this.logger.log(
`Adding release ${data.releaseId} to suggestions for user ${userId}`,
);
return this.collectionService.addToSuggestions(userId, data);
}

@Delete(':userId/collection/:releaseId')
@ApiOperation({ summary: 'Remove release from collection' })
@ApiParam({ name: 'userId', description: 'User ID' })
Expand Down Expand Up @@ -244,4 +306,30 @@ export class CollectionController {
);
return this.collectionService.removeFromWantlist(userId, releaseId);
}

@Delete(':userId/suggestions/:releaseId')
@ApiOperation({ summary: 'Remove release from suggestions' })
@ApiParam({ name: 'userId', description: 'User ID' })
@ApiParam({ name: 'releaseId', description: 'Release ID' })
@ApiResponse({
status: 200,
description: 'Release removed from suggestions successfully',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - Invalid or missing API key',
})
@ApiResponse({
status: 404,
description: 'Release not found in suggestions',
})
async removeFromSuggestions(
@Param('userId') userId: string,
@Param('releaseId', ParseIntPipe) releaseId: number,
) {
this.logger.log(
`Removing release ${releaseId} from suggestions for user ${userId}`,
);
return this.collectionService.removeFromSuggestions(userId, releaseId);
}
}
13 changes: 12 additions & 1 deletion src/collection/collection.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,34 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserCollection } from '../database/entities/user-collection.entity';
import { UserWantlist } from '../database/entities/user-wantlist.entity';
import { UserSuggestion } from '../database/entities/user-suggestion.entity';
import { Release } from '../database/entities/release.entity';
import { CollectionController } from './collection.controller';
import { CollectionService } from './collection.service';
import { UserCollectionRepository } from './repositories/user-collection.repository';
import { UserWantlistRepository } from './repositories/user-wantlist.repository';
import { UserSuggestionRepository } from './repositories/user-suggestion.repository';

@Module({
imports: [TypeOrmModule.forFeature([UserCollection, UserWantlist, Release])],
imports: [
TypeOrmModule.forFeature([
UserCollection,
UserWantlist,
UserSuggestion,
Release,
]),
],
providers: [
UserCollectionRepository,
UserWantlistRepository,
UserSuggestionRepository,
CollectionService,
],
controllers: [CollectionController],
exports: [
UserCollectionRepository,
UserWantlistRepository,
UserSuggestionRepository,
CollectionService,
],
})
Expand Down
104 changes: 99 additions & 5 deletions src/collection/collection.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@nestjs/common';
import { UserWantlistRepository } from './repositories/user-wantlist.repository';
import { UserCollectionRepository } from './repositories/user-collection.repository';
import { UserSuggestionRepository } from './repositories/user-suggestion.repository';
import {
CollectionSortField,
WantlistSortField,
Expand All @@ -21,6 +22,7 @@ export class CollectionService {
constructor(
private readonly collectionRepo: UserCollectionRepository,
private readonly wantlistRepo: UserWantlistRepository,
private readonly suggestionRepo: UserSuggestionRepository,
) {}

async getUserCollection(
Expand Down Expand Up @@ -89,19 +91,60 @@ export class CollectionService {
};
}

async getUserSuggestions(
userId: string,
limit?: number,
offset?: number,
sortBy?: string,
sortOrder?: string,
) {
const sortField = this.mapCollectionSortField(sortBy);
const order = this.mapSortOrder(sortOrder);

this.logger.log(
`Getting suggestions for user ${userId} - sort: ${sortField} ${order}`,
);

const [items, total] = await this.suggestionRepo.findByUserIdSorted(
userId,
limit || DEFAULT_LIMIT,
offset || DEFAULT_OFFSET,
sortField,
order,
);

return {
data: items,
total,
limit: limit || DEFAULT_LIMIT,
offset: offset || DEFAULT_OFFSET,
hasMore: (offset || 0) + items.length < total,
sortBy: sortField,
sortOrder: order,
};
}

async getUserStats(userId: string) {
const [collectionStats, wantlistStats] = await Promise.all([
this.collectionRepo.getCollectionStats(userId),
this.wantlistRepo.getWantlistStats(userId),
]);
const [collectionStats, wantlistStats, suggestionStats] = await Promise.all(
[
this.collectionRepo.getCollectionStats(userId),
this.wantlistRepo.getWantlistStats(userId),
this.suggestionRepo.getSuggestionsStats(userId),
],
);

return {
collection: collectionStats,
wantlist: wantlistStats,
suggestions: suggestionStats,
summary: {
totalItems: collectionStats.totalItems + wantlistStats.totalItems,
totalItems:
collectionStats.totalItems +
wantlistStats.totalItems +
suggestionStats.totalItems,
collectionItems: collectionStats.totalItems,
wantlistItems: wantlistStats.totalItems,
suggestionItems: suggestionStats.totalItems,
},
};
}
Expand Down Expand Up @@ -209,6 +252,57 @@ export class CollectionService {
}
}

async addToSuggestions(
userId: string,
data: { releaseId: number; notes?: string },
) {
const existing = await this.suggestionRepo.findByUserAndRelease(
userId,
data.releaseId,
);

if (existing) {
throw new ConflictException('Release already in suggestions');
}

try {
return await this.suggestionRepo.addToSuggestions({
userId,
releaseId: data.releaseId,
notes: data.notes,
dateAdded: new Date(),
});
} catch (error) {
this.logger.error(
`Failed to add release ${data.releaseId} to suggestions for user ${userId}`,
error,
);
throw error;
}
}

async removeFromSuggestions(userId: string, releaseId: number) {
const existing = await this.suggestionRepo.findByUserAndRelease(
userId,
releaseId,
);

if (!existing) {
throw new NotFoundException('Release not found in suggestions');
}

try {
await this.suggestionRepo.removeFromSuggestions(userId, releaseId);
return { message: 'Release removed from suggestions', releaseId };
} catch (error) {
this.logger.error(
`Failed to remove release ${releaseId} from suggestions for user ${userId}`,
error,
);
throw error;
}
}

getCollectionSortOptions() {
return this.collectionRepo.getAvailableSortOptions();
}
Expand Down
18 changes: 18 additions & 0 deletions src/collection/dto/add-to-suggestions.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { IsNumber, IsOptional, IsString } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';

export class AddToSuggestionsDto {
@ApiProperty({ description: 'Release ID', example: 12345 })
@IsNumber()
@Type(() => Number)
releaseId: number;

@ApiPropertyOptional({
description: 'Notes about this release suggestion',
example: 'Recommended by friend, similar to other albums I like',
})
@IsOptional()
@IsString()
notes?: string;
}
Loading