Added series subscriptions. Added series searching. Fixed database relations. Added logging for library controller.

This commit is contained in:
Tom
2025-03-07 16:06:08 +00:00
parent 4aafe86ef0
commit c7ece75e7a
17 changed files with 364 additions and 175 deletions

View File

@ -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),

View File

@ -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();
});
});

View File

@ -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();
}
}

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from "class-validator";
export class BookOriginDto {
@IsString()
@IsNotEmpty()
bookOriginId: string;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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,
};
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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[];
}

View File

@ -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[];
}

View File

@ -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) {

View File

@ -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;