Added series subscriptions. Added series searching. Fixed database relations. Added logging for library controller.
This commit is contained in:
@ -54,7 +54,6 @@ CREATE TABLE
|
||||
published_at timestamp default NULL,
|
||||
added_at timestamp default NULL,
|
||||
PRIMARY KEY (book_id),
|
||||
-- FOREIGN KEY (series_id) REFERENCES series (series_id),
|
||||
FOREIGN KEY (provider, provider_series_id) REFERENCES series (provider, provider_series_id) ON DELETE CASCADE,
|
||||
UNIQUE NULLS NOT DISTINCT (provider_series_id, provider_book_id, book_volume)
|
||||
);
|
||||
@ -131,7 +130,7 @@ CREATE INDEX book_statuses_user_id_login_idx ON users (user_id);
|
||||
CREATE TABLE
|
||||
series_subscriptions (
|
||||
user_id uuid,
|
||||
provider text,
|
||||
provider varchar(12) NOT NULL,
|
||||
provider_series_id text,
|
||||
added_at timestamp default NULL,
|
||||
PRIMARY KEY (user_id, provider, provider_series_id),
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { BooksController } from './books.controller';
|
||||
|
||||
describe('BooksController', () => {
|
||||
let controller: BooksController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [BooksController],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<BooksController>(BooksController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { BookEntity } from './entities/book.entity';
|
||||
import { In, InsertResult, Repository } from 'typeorm';
|
||||
import { DeleteResult, In, InsertResult, Repository } from 'typeorm';
|
||||
import { BookOriginEntity } from './entities/book-origin.entity';
|
||||
import { BookStatusEntity } from './entities/book-status.entity';
|
||||
import { UUID } from 'crypto';
|
||||
@ -10,6 +10,8 @@ import { CreateBookOriginDto } from './dto/create-book-origin.dto';
|
||||
import { CreateBookStatusDto } from './dto/create-book-status.dto';
|
||||
import { DeleteBookStatusDto } from './dto/delete-book-status.dto';
|
||||
import { SeriesDto } from 'src/series/dto/series.dto';
|
||||
import { SeriesSubscriptionDto } from 'src/series/dto/series-subscription.dto';
|
||||
import { BookOriginDto } from './dto/book-origin.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BooksService {
|
||||
@ -37,16 +39,18 @@ export class BooksService {
|
||||
return await this.bookOriginRepository.insert(origin);
|
||||
}
|
||||
|
||||
async deleteBookOrigin(origin: CreateBookOriginDto) {
|
||||
async deleteBookOrigin(origin: BookOriginDto[]): Promise<DeleteResult> {
|
||||
return await this.bookOriginRepository.createQueryBuilder()
|
||||
.delete()
|
||||
.where({
|
||||
whereFactory: origin,
|
||||
whereFactory: {
|
||||
bookOriginId: In(origin.map(o => o.bookOriginId)),
|
||||
},
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteBookStatus(status: DeleteBookStatusDto) {
|
||||
async deleteBookStatus(status: DeleteBookStatusDto): Promise<DeleteResult> {
|
||||
return await this.bookStatusRepository.createQueryBuilder()
|
||||
.delete()
|
||||
.where({
|
||||
@ -55,7 +59,7 @@ export class BooksService {
|
||||
.execute();
|
||||
}
|
||||
|
||||
async findBooksByIds(bookIds: UUID[]) {
|
||||
async findBooksByIds(bookIds: UUID[]): Promise<BookEntity[]> {
|
||||
return await this.bookRepository.find({
|
||||
where: {
|
||||
bookId: In(bookIds)
|
||||
@ -63,7 +67,7 @@ export class BooksService {
|
||||
});
|
||||
}
|
||||
|
||||
async findBooksFromSeries(series: SeriesDto) {
|
||||
async findBooksFromSeries(series: SeriesDto): Promise<BookEntity[]> {
|
||||
return await this.bookRepository.find({
|
||||
where: {
|
||||
providerSeriesId: series.providerSeriesId,
|
||||
@ -72,25 +76,23 @@ export class BooksService {
|
||||
});
|
||||
}
|
||||
|
||||
async findBookStatusesTrackedBy(userId: UUID): Promise<BookStatusEntity[]> {
|
||||
async findActualBookStatusesTrackedBy(userId: UUID, series: SeriesDto): Promise<BookStatusEntity[]> {
|
||||
return await this.bookStatusRepository.createQueryBuilder('s')
|
||||
.select(['s.book_id', 's.user_id'])
|
||||
.where('s.user_id = :id', { id: userId })
|
||||
.innerJoin('s.book', 'b')
|
||||
.where('s.user_id = :id', { id: userId })
|
||||
.andWhere('b.provider = :provider', { provider: series.provider })
|
||||
.andWhere('b.providerSeriesId = :id', { id: series.providerSeriesId })
|
||||
.addSelect(['b.book_title', 'b.book_desc', 'b.book_volume', 'b.provider', 'b.providerSeriesId'])
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async findSeriesTrackedBy(userId: UUID) {
|
||||
return await this.bookStatusRepository.createQueryBuilder('s')
|
||||
.where({
|
||||
whereFactory: {
|
||||
userId: userId
|
||||
}
|
||||
})
|
||||
.innerJoin('s.book', 'b')
|
||||
.addSelect(['b.provider', 'b.providerSeriesId'])
|
||||
.distinctOn(['b.provider', 'b.providerSeriesId'])
|
||||
async findBookStatusesTrackedBy(subscription: SeriesSubscriptionDto): Promise<any> {
|
||||
return await this.bookRepository.createQueryBuilder('b')
|
||||
.where('b.provider = :provider', { provider: subscription.provider })
|
||||
.andWhere(`b.provider_series_id = :id`, { id: subscription.providerSeriesId })
|
||||
.leftJoin('b.statuses', 's')
|
||||
.where(`s.user_id = :id`, { id: subscription.userId })
|
||||
.addSelect(['s.state'])
|
||||
.getMany();
|
||||
}
|
||||
|
||||
@ -111,7 +113,7 @@ export class BooksService {
|
||||
await this.bookStatusRepository.createQueryBuilder()
|
||||
.insert()
|
||||
.values(status)
|
||||
.orUpdate(['state', 'modified_at'], ['user_id', 'book_id'], { skipUpdateIfNoValuesChanged: true })
|
||||
.orUpdate(['state', 'modified_at'], ['user_id', 'book_id'])
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { IsNotEmpty, IsString } from "class-validator";
|
||||
|
||||
export class BookOriginDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
bookOriginId: string;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import { IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator';
|
||||
import { UUID } from 'crypto';
|
||||
|
||||
export class CreateBookStatusDto {
|
||||
@ -11,8 +11,10 @@ export class CreateBookStatusDto {
|
||||
@IsOptional()
|
||||
readonly userId: UUID;
|
||||
|
||||
@IsString()
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
@Min(0)
|
||||
@Max(6)
|
||||
state: number;
|
||||
|
||||
modifiedAt: Date;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { UUID } from 'crypto';
|
||||
import { BookOriginType } from 'src/shared/enums/book_origin_type';
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryColumn, Unique } from 'typeorm';
|
||||
import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn, Unique } from 'typeorm';
|
||||
import { BookEntity } from './book.entity';
|
||||
|
||||
@Entity("book_origins")
|
||||
@ -19,6 +19,9 @@ export class BookOriginEntity {
|
||||
value: string;
|
||||
|
||||
@OneToOne(type => BookEntity, book => book.metadata)
|
||||
@JoinColumn({ name: 'book_id' })
|
||||
@JoinColumn({
|
||||
name: 'book_id',
|
||||
referencedColumnName: 'bookId',
|
||||
})
|
||||
book: BookEntity;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { UUID } from 'crypto';
|
||||
import { UserEntity } from 'src/users/entities/users.entity';
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryColumn } from 'typeorm';
|
||||
import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm';
|
||||
import { BookEntity } from './book.entity';
|
||||
|
||||
@Entity("book_statuses")
|
||||
@ -21,10 +21,16 @@ export class BookStatusEntity {
|
||||
modifiedAt: Date;
|
||||
|
||||
@OneToOne(type => BookEntity, book => book.statuses)
|
||||
@JoinColumn({ name: 'book_id' })
|
||||
@JoinColumn({
|
||||
name: 'book_id',
|
||||
referencedColumnName: 'bookId',
|
||||
})
|
||||
book: BookEntity;
|
||||
|
||||
@OneToOne(type => UserEntity, user => user.bookStatuses)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
@JoinColumn({
|
||||
name: 'user_id',
|
||||
referencedColumnName: 'userId',
|
||||
})
|
||||
user: UserEntity;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { UUID } from 'crypto';
|
||||
import { Column, Entity, JoinColumn, OneToMany, OneToOne, PrimaryColumn, Unique } from 'typeorm';
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, PrimaryColumn, Unique } from 'typeorm';
|
||||
import { BookOriginEntity } from './book-origin.entity';
|
||||
import { BookStatusEntity } from './book-status.entity';
|
||||
import { SeriesEntity } from 'src/series/entities/series.entity';
|
||||
@ -34,14 +34,20 @@ export class BookEntity {
|
||||
@Column({ name: 'added_at', type: 'timestamptz', nullable: false })
|
||||
addedAt: Date;
|
||||
|
||||
@OneToMany(type => BookOriginEntity, origin => origin.bookId)
|
||||
@OneToMany(type => BookOriginEntity, origin => origin.book)
|
||||
metadata: BookOriginEntity[];
|
||||
|
||||
@OneToMany(type => BookStatusEntity, status => status.bookId)
|
||||
@OneToMany(type => BookStatusEntity, status => status.book)
|
||||
statuses: BookStatusEntity[];
|
||||
|
||||
@OneToOne(type => SeriesEntity, series => series.volumes)
|
||||
@JoinColumn({ name: 'provider_series_id' })
|
||||
@JoinColumn({ name: 'provider' })
|
||||
@JoinColumn([{
|
||||
name: 'provider_series_id',
|
||||
referencedColumnName: 'providerSeriesId',
|
||||
},
|
||||
{
|
||||
name: 'provider',
|
||||
referencedColumnName: 'provider',
|
||||
}])
|
||||
series: SeriesEntity;
|
||||
}
|
@ -5,7 +5,7 @@ import { PinoLogger } from 'nestjs-pino';
|
||||
import { GoogleSearchContext } from 'src/providers/contexts/google.search.context';
|
||||
import { BookSearchResultDto } from 'src/providers/dto/book-search-result.dto';
|
||||
import { ProvidersService } from 'src/providers/providers.service';
|
||||
import { CreateSeriesSubscriptionJobDto } from 'src/series/dto/create-series-subscription-job.dto';
|
||||
import { SeriesSubscriptionJobDto } from 'src/series/dto/series-subscription-job.dto';
|
||||
import { LibraryService } from './library.service';
|
||||
|
||||
@Processor('library')
|
||||
@ -27,7 +27,7 @@ export class LibraryConsumer extends WorkerHost {
|
||||
});
|
||||
|
||||
if (job.name == 'new_series') {
|
||||
const series: CreateSeriesSubscriptionJobDto = job.data;
|
||||
const series: SeriesSubscriptionJobDto = job.data;
|
||||
const books = await this.search(job, series, null);
|
||||
|
||||
let counter = 0;
|
||||
@ -51,8 +51,8 @@ export class LibraryConsumer extends WorkerHost {
|
||||
}
|
||||
}
|
||||
} else if (job.name == 'update_series') {
|
||||
const series: CreateSeriesSubscriptionJobDto = job.data;
|
||||
const existingBooks = await this.library.getBooksFromSeries(series);
|
||||
const series: SeriesSubscriptionJobDto = job.data;
|
||||
const existingBooks = await this.library.findBooksFromSeries(series);
|
||||
const existingVolumes = existingBooks.map(b => b.volume);
|
||||
const lastPublishedBook = existingBooks.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime())[0];
|
||||
const books = await this.search(job, series, lastPublishedBook?.publishedAt);
|
||||
@ -100,7 +100,7 @@ export class LibraryConsumer extends WorkerHost {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async search(job: Job, series: CreateSeriesSubscriptionJobDto, after: Date|null): Promise<{ result: BookSearchResultDto, score: number }[]> {
|
||||
private async search(job: Job, series: SeriesSubscriptionJobDto, after: Date | null): Promise<{ result: BookSearchResultDto, score: number }[]> {
|
||||
let context = this.provider.generateSearchContext(series.provider, series.title) as GoogleSearchContext;
|
||||
context.maxResults = '40';
|
||||
if (after) {
|
||||
@ -131,7 +131,7 @@ export class LibraryConsumer extends WorkerHost {
|
||||
.sort((a, b) => a.result.volume - b.result.volume || b.score - a.score)
|
||||
.filter((_, index, arr) => index == 0 || arr[index - 1].result.volume != arr[index].result.volume);
|
||||
job.updateProgress(25);
|
||||
|
||||
|
||||
this.logger.debug({
|
||||
class: LibraryConsumer.name,
|
||||
method: this.search.name,
|
||||
@ -185,7 +185,7 @@ export class LibraryConsumer extends WorkerHost {
|
||||
});
|
||||
}
|
||||
|
||||
private toScore(book: BookSearchResultDto, series: CreateSeriesSubscriptionJobDto): ({ result: BookSearchResultDto, score: number }) {
|
||||
private toScore(book: BookSearchResultDto, series: SeriesSubscriptionJobDto): ({ result: BookSearchResultDto, score: number }) {
|
||||
if (!book) {
|
||||
return {
|
||||
result: null,
|
||||
|
@ -10,11 +10,13 @@ import { QueryFailedError } from 'typeorm';
|
||||
import { UpdateBookDto } from 'src/books/dto/update-book.dto';
|
||||
import { UpdateBookOriginDto } from 'src/books/dto/update-book-origin.dto';
|
||||
import { LibraryService } from './library.service';
|
||||
import { CreateSeriesSubscriptionDto } from 'src/series/dto/create-series-subscription.dto';
|
||||
import { JwtAccessGuard } from 'src/auth/guards/jwt-access.guard';
|
||||
import { SeriesDto } from 'src/series/dto/series.dto';
|
||||
import { CreateBookOriginDto } from 'src/books/dto/create-book-origin.dto';
|
||||
import { DeleteBookStatusDto } from 'src/books/dto/delete-book-status.dto';
|
||||
import { CreateBookStatusDto } from 'src/books/dto/create-book-status.dto';
|
||||
import { JwtAccessAdminGuard } from 'src/auth/guards/jwt-access.admin.guard';
|
||||
import { BookOriginDto } from 'src/books/dto/book-origin.dto';
|
||||
import { CreateSeriesDto } from 'src/series/dto/create-series.dto';
|
||||
|
||||
@UseGuards(JwtAccessGuard)
|
||||
@Controller('library')
|
||||
@ -30,7 +32,6 @@ export class LibraryController {
|
||||
@Get('series')
|
||||
async getSeries(
|
||||
@Request() req,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
return {
|
||||
success: true,
|
||||
@ -41,16 +42,11 @@ export class LibraryController {
|
||||
@Post('series')
|
||||
async createSeries(
|
||||
@Request() req,
|
||||
@Body() body: CreateSeriesSubscriptionDto,
|
||||
@Body() body: CreateSeriesDto,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
try {
|
||||
await this.library.addSeries({
|
||||
provider: body.provider,
|
||||
providerSeriesId: body.providerSeriesId,
|
||||
title: body.title,
|
||||
mediaType: body.mediaType,
|
||||
});
|
||||
await this.library.addSeries(body);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@ -58,15 +54,32 @@ export class LibraryController {
|
||||
} catch (err) {
|
||||
if (err instanceof QueryFailedError) {
|
||||
if (err.driverError.code == '23505') {
|
||||
// Subscription already exist.
|
||||
this.logger.warn({
|
||||
class: LibraryController.name,
|
||||
method: this.createSeries.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to create a series.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
response.statusCode = 409;
|
||||
return {
|
||||
success: false,
|
||||
error_message: 'Series subscription already exists.',
|
||||
error_message: 'Series already exists.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.error({
|
||||
class: LibraryController.name,
|
||||
method: this.createSeries.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to create a series.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
response.statusCode = 500;
|
||||
return {
|
||||
success: false,
|
||||
@ -78,34 +91,93 @@ export class LibraryController {
|
||||
@Patch('series')
|
||||
async updateSeries(
|
||||
@Request() req,
|
||||
@Body() body: CreateSeriesSubscriptionDto,
|
||||
@Body() body: CreateSeriesDto,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
try {
|
||||
const series = await this.series.getSeries({
|
||||
provider: body.provider,
|
||||
providerSeriesId: body.providerSeriesId,
|
||||
});
|
||||
|
||||
if (!series) {
|
||||
response.statusCode = 404;
|
||||
return {
|
||||
success: false,
|
||||
error_message: 'Series has not been added.'
|
||||
};
|
||||
}
|
||||
|
||||
await this.library.updateSeries({
|
||||
provider: body.provider,
|
||||
providerSeriesId: body.providerSeriesId,
|
||||
title: body.title,
|
||||
mediaType: body.mediaType,
|
||||
});
|
||||
await this.library.updateSeries(body);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof QueryFailedError) {
|
||||
if (err.driverError.code == '23505') {
|
||||
this.logger.warn({
|
||||
class: LibraryController.name,
|
||||
method: this.updateSeries.name,
|
||||
user: req.user,
|
||||
msg: 'Failed to update a series.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
// Subscription already exist.
|
||||
response.statusCode = 409;
|
||||
return {
|
||||
success: false,
|
||||
error_message: 'Series subscription already exists.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.error({
|
||||
class: LibraryController.name,
|
||||
method: this.updateSeries.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to update a series.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
response.statusCode = 500;
|
||||
return {
|
||||
success: false,
|
||||
error_message: 'Something went wrong.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(JwtAccessAdminGuard)
|
||||
@Delete('series')
|
||||
async deleteSeries(
|
||||
@Request() req,
|
||||
@Body() body: SeriesDto,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
try {
|
||||
const del = await this.series.deleteSeries(body);
|
||||
return {
|
||||
success: del && del.affected > 0,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof QueryFailedError) {
|
||||
if (err.driverError.code == '23503') {
|
||||
this.logger.warn({
|
||||
class: LibraryController.name,
|
||||
method: this.deleteSeries.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to delete a series.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
response.statusCode = 404;
|
||||
return {
|
||||
success: false,
|
||||
error_message: 'The series does not exist.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.error({
|
||||
class: LibraryController.name,
|
||||
method: this.deleteSeries.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to delete a series.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
response.statusCode = 500;
|
||||
return {
|
||||
success: false,
|
||||
@ -117,7 +189,6 @@ export class LibraryController {
|
||||
@Get('series/subscriptions')
|
||||
async getSeriesSubscriptions(
|
||||
@Request() req,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
return {
|
||||
success: true,
|
||||
@ -143,6 +214,15 @@ export class LibraryController {
|
||||
} catch (err) {
|
||||
if (err instanceof QueryFailedError) {
|
||||
if (err.driverError.code == '23505') {
|
||||
this.logger.warn({
|
||||
class: LibraryController.name,
|
||||
method: this.subscribe.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to subscribe to a series.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
// Subscription already exists.
|
||||
response.statusCode = 409;
|
||||
return {
|
||||
@ -150,6 +230,15 @@ export class LibraryController {
|
||||
error_message: 'Series subscription already exists.',
|
||||
};
|
||||
} else if (err.driverError.code == '23503') {
|
||||
this.logger.warn({
|
||||
class: LibraryController.name,
|
||||
method: this.subscribe.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to subscribe to a series.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
// Series does not exist.
|
||||
response.statusCode = 404;
|
||||
return {
|
||||
@ -159,6 +248,15 @@ export class LibraryController {
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.error({
|
||||
class: LibraryController.name,
|
||||
method: this.subscribe.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to subscribe to a series.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
response.statusCode = 500;
|
||||
return {
|
||||
success: false,
|
||||
@ -167,13 +265,31 @@ export class LibraryController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete('series/subscribe')
|
||||
async deleteSeriesSubscription(
|
||||
@Request() req,
|
||||
@Body() body: SeriesDto,
|
||||
) {
|
||||
const del = await this.series.deleteSeriesSubscription({
|
||||
...body,
|
||||
userId: req.user.userId,
|
||||
});
|
||||
return {
|
||||
success: del && del.affected > 0,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('books')
|
||||
async getBooksFromUser(
|
||||
@Request() req,
|
||||
@Body() body: SeriesDto,
|
||||
) {
|
||||
return {
|
||||
success: true,
|
||||
data: await this.books.findBookStatusesTrackedBy(req.user.userId),
|
||||
data: await this.library.findBooksFromSeries({
|
||||
provider: body.provider,
|
||||
providerSeriesId: body.providerSeriesId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -184,32 +300,18 @@ export class LibraryController {
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
if (body.provider && body.providerSeriesId) {
|
||||
try {
|
||||
await this.series.updateSeries({
|
||||
provider: body.provider,
|
||||
providerSeriesId: body.providerSeriesId,
|
||||
title: body.title,
|
||||
mediaType: body.mediaType,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof QueryFailedError) {
|
||||
this.logger.error({
|
||||
class: LibraryController.name,
|
||||
method: this.createBook.name,
|
||||
user: req.user,
|
||||
msg: 'Failed to create a series for a book.',
|
||||
error: err,
|
||||
});
|
||||
this.logger.warn({
|
||||
class: LibraryController.name,
|
||||
method: this.createBook.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to create book due to book being part of a series.',
|
||||
});
|
||||
|
||||
// Ignore if the series already exist.
|
||||
if (err.driverError.code != '23505') {
|
||||
response.statusCode = 500;
|
||||
return {
|
||||
success: false,
|
||||
error_message: 'Something went wrong.',
|
||||
};
|
||||
}
|
||||
}
|
||||
response.statusCode = 400;
|
||||
return {
|
||||
success: false,
|
||||
error_message: 'This book is part of a seris. Use the series route to create a series.',
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,15 +322,16 @@ export class LibraryController {
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof QueryFailedError) {
|
||||
this.logger.error({
|
||||
class: LibraryController.name,
|
||||
method: this.createBook.name,
|
||||
user: req.user,
|
||||
msg: 'Failed to create book.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
if (err.driverError.code == '23505') {
|
||||
this.logger.warn({
|
||||
class: LibraryController.name,
|
||||
method: this.createBook.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to create book.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
// Book exists already.
|
||||
response.statusCode = 409;
|
||||
return {
|
||||
@ -236,15 +339,33 @@ export class LibraryController {
|
||||
error_message: 'The book has already been added previously.',
|
||||
};
|
||||
} else if (err.driverError.code == '23503') {
|
||||
this.logger.warn({
|
||||
class: LibraryController.name,
|
||||
method: this.createBook.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to create book.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
// Series is missing.
|
||||
response.statusCode = 500;
|
||||
response.statusCode = 404;
|
||||
return {
|
||||
success: false,
|
||||
error_message: 'Series has not been added.',
|
||||
error_message: 'Series does not exist.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.error({
|
||||
class: LibraryController.name,
|
||||
method: this.createBook.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to create a book.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
response.statusCode = 500;
|
||||
return {
|
||||
success: false,
|
||||
@ -262,33 +383,17 @@ export class LibraryController {
|
||||
|
||||
const result = await this.books.updateBook(body.bookId, data);
|
||||
return {
|
||||
success: result?.affected == 1,
|
||||
success: result && result.affected > 0,
|
||||
};
|
||||
}
|
||||
|
||||
@Delete('books/origins')
|
||||
async deleteBookOrigin(
|
||||
@Body() body: CreateBookOriginDto,
|
||||
@Body() body: BookOriginDto[],
|
||||
) {
|
||||
const data = { ...body };
|
||||
delete data['bookOriginId'];
|
||||
|
||||
const result = await this.books.deleteBookOrigin(body);
|
||||
return {
|
||||
success: result?.affected == 1,
|
||||
};
|
||||
}
|
||||
|
||||
@Delete('books/status')
|
||||
async deleteBookStatus(
|
||||
@Body() body: DeleteBookStatusDto,
|
||||
) {
|
||||
const data = { ...body };
|
||||
delete data['bookOriginId'];
|
||||
|
||||
const result = await this.books.deleteBookStatus(body);
|
||||
return {
|
||||
success: result?.affected == 1,
|
||||
success: result && result.affected > 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -301,7 +406,76 @@ export class LibraryController {
|
||||
|
||||
const result = await this.books.updateBookOrigin(body.bookOriginId, data);
|
||||
return {
|
||||
success: result?.affected == 1,
|
||||
success: result && result.affected > 0,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('books/status')
|
||||
async getBookStatus(
|
||||
@Request() req,
|
||||
@Body() body: SeriesDto,
|
||||
) {
|
||||
return {
|
||||
success: true,
|
||||
data: await this.books.findActualBookStatusesTrackedBy(req.user.userId, body),
|
||||
};
|
||||
}
|
||||
|
||||
@Put('books/status')
|
||||
async updateBookStatus(
|
||||
@Request() req,
|
||||
@Body() body: CreateBookStatusDto,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
try {
|
||||
await this.books.updateBookStatus(body);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof QueryFailedError) {
|
||||
if (err.driverError.code == '23503') {
|
||||
this.logger.warn({
|
||||
class: LibraryController.name,
|
||||
method: this.updateBookStatus.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to update the user\'s status of a book.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
response.statusCode = 404;
|
||||
return {
|
||||
success: false,
|
||||
error_message: 'The book does not exist.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.error({
|
||||
class: LibraryController.name,
|
||||
method: this.updateBookStatus.name,
|
||||
user: req.user,
|
||||
body: body,
|
||||
msg: 'Failed to update the user\'s status of a book.',
|
||||
error: err,
|
||||
});
|
||||
|
||||
response.statusCode = 500;
|
||||
return {
|
||||
success: false,
|
||||
error_message: 'Something went wrong.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Delete('books/status')
|
||||
async deleteBookStatus(
|
||||
@Body() body: DeleteBookStatusDto,
|
||||
) {
|
||||
const result = await this.books.deleteBookStatus(body);
|
||||
return {
|
||||
success: result && result.affected > 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -136,11 +136,11 @@ export class LibraryService {
|
||||
return bookId;
|
||||
}
|
||||
|
||||
async findBooksFromSeries(series: SeriesDto) {
|
||||
return await this.books.findBooksFromSeries(series);
|
||||
}
|
||||
|
||||
async updateSeries(series: CreateSeriesDto) {
|
||||
return await this.jobs.add('update_series', series);
|
||||
}
|
||||
|
||||
async getBooksFromSeries(series: SeriesDto) {
|
||||
return await this.books.findBooksFromSeries(series);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { SeriesDto } from './series.dto';
|
||||
|
||||
export class CreateSeriesSubscriptionDto extends SeriesDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
mediaType: string;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { SeriesSubscriptionDto } from './series-subscription.dto';
|
||||
|
||||
export class CreateSeriesSubscriptionJobDto extends SeriesSubscriptionDto {
|
||||
export class SeriesSubscriptionJobDto extends SeriesSubscriptionDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title: string;
|
@ -1,5 +1,6 @@
|
||||
import { UUID } from 'crypto';
|
||||
import { Column, Entity, PrimaryColumn, Unique } from 'typeorm';
|
||||
import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn, Unique } from 'typeorm';
|
||||
import { SeriesEntity } from './series.entity';
|
||||
|
||||
@Entity("series_subscriptions")
|
||||
export class SeriesSubscriptionEntity {
|
||||
@ -14,4 +15,15 @@ export class SeriesSubscriptionEntity {
|
||||
|
||||
@Column({ name: 'added_at', type: 'timestamptz', nullable: false })
|
||||
addedAt: Date;
|
||||
|
||||
@OneToOne(type => SeriesEntity, series => series.subscriptions)
|
||||
@JoinColumn([{
|
||||
name: 'provider_series_id',
|
||||
referencedColumnName: 'providerSeriesId',
|
||||
},
|
||||
{
|
||||
name: 'provider',
|
||||
referencedColumnName: 'provider',
|
||||
}])
|
||||
series: SeriesSubscriptionEntity[];
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { UUID } from 'crypto';
|
||||
import { BookEntity } from 'src/books/entities/book.entity';
|
||||
import { Column, Entity, OneToMany, PrimaryColumn, Unique } from 'typeorm';
|
||||
import { SeriesSubscriptionEntity } from './series-subscription.entity';
|
||||
|
||||
@Entity("series")
|
||||
@Unique(['provider', 'providerSeriesId'])
|
||||
@ -25,4 +26,7 @@ export class SeriesEntity {
|
||||
|
||||
@OneToMany(type => BookEntity, book => [book.provider, book.providerSeriesId])
|
||||
volumes: BookEntity[];
|
||||
|
||||
@OneToMany(type => SeriesSubscriptionEntity, subscription => [subscription.provider, subscription.providerSeriesId])
|
||||
subscriptions: SeriesSubscriptionEntity[];
|
||||
}
|
@ -12,9 +12,9 @@ import { SeriesSubscriptionDto } from './dto/series-subscription.dto';
|
||||
export class SeriesService {
|
||||
constructor(
|
||||
@InjectRepository(SeriesEntity)
|
||||
private seriesRepository: Repository<SeriesEntity>,
|
||||
private readonly seriesRepository: Repository<SeriesEntity>,
|
||||
@InjectRepository(SeriesSubscriptionEntity)
|
||||
private seriesSubscriptionRepository: Repository<SeriesSubscriptionEntity>,
|
||||
private readonly seriesSubscriptionRepository: Repository<SeriesSubscriptionEntity>,
|
||||
) { }
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ export class SeriesService {
|
||||
async getSeries(series: SeriesDto) {
|
||||
return await this.seriesRepository.findOne({
|
||||
where: series
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async getAllSeries() {
|
||||
@ -45,11 +45,15 @@ export class SeriesService {
|
||||
}
|
||||
|
||||
async getSeriesSubscribedBy(userId: UUID) {
|
||||
return await this.seriesSubscriptionRepository.find({
|
||||
where: {
|
||||
userId,
|
||||
}
|
||||
});
|
||||
return await this.seriesRepository.createQueryBuilder('s')
|
||||
.select(['s.seriesId', 's.providerSeriesId', 's.provider', 's.title', 's.mediaType', 's.addedAt'])
|
||||
.innerJoinAndMapOne('s.subscriptions',
|
||||
qb => qb
|
||||
.select(['subscription.provider', 'subscription.provider_series_id', 'subscription.user_id'])
|
||||
.from(SeriesSubscriptionEntity, 'subscription'),
|
||||
'ss', `"ss"."subscription_provider" = "s"."provider" AND "ss"."provider_series_id" = "s"."provider_series_id"`)
|
||||
.where(`ss.user_id = :id`, { id: userId })
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async updateSeries(series: CreateSeriesDto) {
|
||||
|
@ -7,7 +7,7 @@ import { LoginUserDto } from './dto/login-user.dto';
|
||||
import { UUID } from 'crypto';
|
||||
|
||||
class UserDto {
|
||||
userId: string;
|
||||
userId: UUID;
|
||||
userLogin: string;
|
||||
userName: string;
|
||||
isAdmin: boolean;
|
||||
|
Reference in New Issue
Block a user