Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add admin manage article feature #382

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
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
11 changes: 11 additions & 0 deletions apps/recnet-api/src/database/repository/article.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,15 @@ export default class ArticleRepository {
select: article.select,
});
}

public async updateArticle(
articleId: string,
data: Partial<CreateArticleInput>
): Promise<Article> {
return this.prisma.article.update({
where: { id: articleId },
data,
select: article.select,
});
}
}
64 changes: 58 additions & 6 deletions apps/recnet-api/src/modules/article/article.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get, Query, UseFilters, UsePipes } from "@nestjs/common";
import { Body, Controller, Get, NotFoundException, Patch, Query, UseFilters, UsePipes } from "@nestjs/common";
import {
ApiOkResponse,
ApiOperation,
Expand All @@ -8,13 +8,15 @@ import {

import { Auth } from "@recnet-api/utils/auth/auth.decorator";
import { RecnetExceptionFilter } from "@recnet-api/utils/filters/recnet.exception.filter";
import { ZodValidationQueryPipe } from "@recnet-api/utils/pipes/zod.validation.pipe";
import { ZodValidationBodyPipe, ZodValidationQueryPipe } from "@recnet-api/utils/pipes/zod.validation.pipe";

import { getArticlesParamsSchema } from "@recnet/recnet-api-model";
import { AdminUpdateArticleDtoSchema, getArticlesParamsSchema } from "@recnet/recnet-api-model";

import { GetArticleByLinkResponse } from "./article.response";
import { ArticleService } from "./article.service";
import { QueryArticleDto } from "./dto/query.article.dto";
import { UpdateArticleDto } from "@recnet-api/modules/rec/dto/update.rec.dto";
import { AdminUpdateArticleDto } from "@recnet-api/modules/article/dto/update.article.admin.dto";

@ApiTags("articles")
@Controller("articles")
Expand All @@ -25,7 +27,7 @@ export class ArticleController {
@ApiOperation({
summary: "Get Article By Link",
description:
"Get article by link. If the article is not found in the database, it will try to get metadata using the digital library service. Now it only supports arXiv.",
"Get article by link. If the useDigitalLibrary option is true and article is not found in the database, it will try to get metadata using the digital library service.Otherwise it will only query from database. Now it only supports arXiv.",
})
@ApiOkResponse({ type: GetArticleByLinkResponse })
@ApiBearerAuth()
Expand All @@ -35,7 +37,57 @@ export class ArticleController {
public async getArticleByLink(
@Query() dto: QueryArticleDto
): Promise<GetArticleByLinkResponse> {
const { link } = dto;
return this.articleService.getArticleByLink(link);
const { link, useDigitalLibrary } = dto;
return this.articleService.getArticleByLink(link, useDigitalLibrary);
}

// @ApiOperation({
// summary: "Get Article in Database By Link",
// description:
// "Get article by link. If the article is not found in the database, it will return null",
// })
// @ApiOkResponse({ type: GetArticleByLinkResponse })
// @ApiBearerAuth()
// @Get("db")
// @Auth({ allowedRoles: ["ADMIN"] })
// @UsePipes(new ZodValidationQueryPipe(getArticlesParamsSchema))
// public async getDbArticleByLink(
// @Query() dto: QueryArticleDto
// ): Promise<GetArticleByLinkResponse> {
// const { link } = dto;
// return this.articleService.getDbArticleByLink(link);
// }

@ApiOperation({
summary: "Admin update article by link",
description: "Update an existing article's fields (title, author, etc.)",
})
@ApiOkResponse({
description: "Return the updated article",
type: GetArticleByLinkResponse,
})
@ApiBearerAuth()
@Patch("admin")
@Auth({ allowedRoles: ["ADMIN"] })
@UsePipes(new ZodValidationBodyPipe(AdminUpdateArticleDtoSchema))
public async updateArticleByLink(
@Query("link") link: string,
@Body() dto: AdminUpdateArticleDto
mistydqwq marked this conversation as resolved.
Show resolved Hide resolved
) {
if (!link) {
throw new NotFoundException("Must provide ?link=xxx in query");
}

const existingArticle = await this.articleService.getArticleByLink(link, false);
if (!existingArticle) {
throw new NotFoundException(`Article not found by link=${link}`);
}

const updatedArticle = await this.articleService.updateArticleByLink(
link,
dto
);

return { article: updatedArticle };
}
mistydqwq marked this conversation as resolved.
Show resolved Hide resolved
}
83 changes: 72 additions & 11 deletions apps/recnet-api/src/modules/article/article.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CreateArticleInput } from "@recnet-api/database/repository/article.repo
import { DIGITAL_LIBRARY } from "@recnet-api/modules/digital-library/digital-library.const";
import { DigitalLibraryService } from "@recnet-api/modules/digital-library/digital-library.service";
import { Metadata } from "@recnet-api/modules/digital-library/digital-library.type";
import { NotFoundException } from "@nestjs/common";

import { GetArticleByLinkResponse } from "./article.response";

Expand All @@ -19,39 +20,68 @@ export class ArticleService {
private readonly digitalLibraryService: DigitalLibraryService | null
) {}

// public async getArticleByLink(
// link: string
// ): Promise<GetArticleByLinkResponse> {
// let unifiedLink = link;
//
// // If digital library service is available, try to get the unified link
// if (this.digitalLibraryService) {
// unifiedLink = await this.digitalLibraryService.getUnifiedLink(link);
// }
//
// // Try to find the article in the database using the unified link
// let article = await this.articleRepository.findArticleByLink(unifiedLink);
// if (article) {
// return { article };
// }
//
// // If no article found, try to get metadata using the digital library service
// if (this.digitalLibraryService) {
// try {
// const metadata = await this.digitalLibraryService.getMetadata(link);
// const articleInput = this.transformMetadata(metadata, unifiedLink);
//
// article = await this.articleRepository.createArticle(articleInput);
// return { article };
// } catch (error) {
// this.logger.error(error);
// }
// }
//
// // If no article found and no metadata available, return null
// return {
// article: null,
// };
// }

public async getArticleByLink(
link: string
link: string,
fetchFromDigitalLibrary: boolean = true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fetchFromDigitalLibrary: boolean = true
useDigitalLibraryFallback: boolean = true

I think the naming can be better. It's confusing to name like this since if you set fetchFromDigitalLibrary to true, it will first fetch from our DB and if not found, it will fetch from Digital Library. Therefore, I think naming fetchFromDigitalLibrary is not accurate.

Just provide one of my random idea, we can name it useDigitalLibraryFallback. @joannechen1223 any thought?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not to use this parameter and create another method instead, just like what you commented out in getDbArticleByLink as the logic of the digital library here is relatively complicated. Adding another API will be more straightforward if you are not going to reuse the digital library related logic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually like what I commented out, at first I implemented a separated API to get article directly from the database, but our professor suggests me to combine the getDbArticleByLink with the original getArticleByLink because these two function are pretty similar and he doesn't want the API to explode at the beginning. Also make sense. So I changed the getArticleByLink function to the current version. May need to discuss.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using separate method might be more scalable in this case given the original method was already really complicated. It also makes sense to try not to make too many APIs but I would prefer make functions easy to maintain than try to add complex logic in a function which is already heavy.
@joannechen1223 thoughts?

): Promise<GetArticleByLinkResponse> {
let unifiedLink = link;

// If digital library service is available, try to get the unified link
if (this.digitalLibraryService) {
if (fetchFromDigitalLibrary && this.digitalLibraryService) {
unifiedLink = await this.digitalLibraryService.getUnifiedLink(link);
}

// Try to find the article in the database using the unified link
let article = await this.articleRepository.findArticleByLink(unifiedLink);
if (article) {
return { article };
}

// If no article found, try to get metadata using the digital library service
if (this.digitalLibraryService) {
if (fetchFromDigitalLibrary && this.digitalLibraryService) {
try {
const metadata = await this.digitalLibraryService.getMetadata(link);
const articleInput = this.transformMetadata(metadata, unifiedLink);

article = await this.articleRepository.createArticle(articleInput);
return { article };
} catch (error) {
this.logger.error(error);
}
}

// If no article found and no metadata available, return null
return {
article: null,
};
return { article: null };
mistydqwq marked this conversation as resolved.
Show resolved Hide resolved
}

private transformMetadata(
Expand All @@ -64,4 +94,35 @@ export class ArticleService {
doi: null,
};
}

// public async getDbArticleByLink(
// link: string
// ): Promise<GetArticleByLinkResponse> {
// const article = await this.articleRepository.findArticleByLink(link);
// if (article) {
// return { article };
// }
//
// // If no article found and no metadata available, return null
// return {
// article: null,
// };
// }

public async updateArticleByLink(
link: string,
updateData: Partial<CreateArticleInput>
) {
const existing = await this.articleRepository.findArticleByLink(link);
if (!existing) {
throw new NotFoundException(`Article not found by link=${link}`);
mistydqwq marked this conversation as resolved.
Show resolved Hide resolved
}

const updated = await this.articleRepository.updateArticle(
existing.id,
updateData
);

return updated;
}
}
4 changes: 4 additions & 0 deletions apps/recnet-api/src/modules/article/dto/query.article.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ export class QueryArticleDto {
description: "The article's link",
})
link: string;
@ApiProperty({
description: "Use digital library service or not",
})
useDigitalLibrary?: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ApiProperty } from '@nestjs/swagger';

export class AdminUpdateArticleDto {
@ApiProperty({
example: 'https://example.com/your-article-link',
description: 'The URL link to the article.',
})
link?: string;

@ApiProperty({
example: 'How AI Is Transforming Healthcare',
description: 'The title of the article.',
})
title?: string;

@ApiProperty({
example: 'John Doe',
description: 'The author of the article.',
})
author?: string;

@ApiProperty({
example: 2023,
description: 'The publication year of the article.',
})
year?: number;

@ApiProperty({
example: 7,
description: 'The publication month of the article (1-12).',
})
month?: number;
mistydqwq marked this conversation as resolved.
Show resolved Hide resolved

@ApiProperty({
example: '10.1234/abcd.efgh.2023',
description: 'The DOI (Digital Object Identifier) of the article.',
})
doi?: string;
mistydqwq marked this conversation as resolved.
Show resolved Hide resolved

@ApiProperty({
example: 'This article explores how AI can be used to improve patient outcomes...',
description: 'A brief abstract or summary of the article.',
})
abstract?: string;
mistydqwq marked this conversation as resolved.
Show resolved Hide resolved

@ApiProperty({
example: true,
description: 'Verification status of the article.',
})
isVerified?: boolean;
}
3 changes: 3 additions & 0 deletions apps/recnet/src/app/admin/AdminPanelNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ export function AdminPanelNavbar() {
label="Provision code"
/>
</AdminPanelNav.Section>
<AdminPanelNav.Section label="Article">
<AdminPanelNav.Item route="article/management" label="Management" />
</AdminPanelNav.Section>
</AdminPanelNav>
);
}
Loading
Loading