-
Notifications
You must be signed in to change notification settings - Fork 41
feat: collection likes management #1730
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
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| // Copyright (c) 2025 The Linux Foundation and each contributor. | ||
| // SPDX-License-Identifier: MIT | ||
| import type { Pool } from 'pg'; | ||
| import { CollectionLikeRepository } from '~~/server/repo/collectionLike.repo'; | ||
| import { InsightsSsoUserRepository } from '~~/server/repo/insightsSsoUser.repo'; | ||
| import type { DecodedOidcToken } from '~~/types/auth/auth-jwt.types'; | ||
| import { getAuthUsername } from '~~/server/utils/common'; | ||
|
|
||
| /** | ||
| * API Endpoint: DELETE /api/collection/like | ||
| * Description: Unlikes a collection for the authenticated user. | ||
| * | ||
| * Request Body: | ||
| * - collectionId (string, required): The ID of the collection to unlike | ||
| * | ||
| * Response: | ||
| * - 200: Success | ||
| * - 400: Validation error | ||
| * - 401: Unauthorized | ||
| * - 500: Internal Server Error | ||
| */ | ||
| export default defineEventHandler(async (event): Promise<{ success: boolean } | Error> => { | ||
| const user = event.context.user as DecodedOidcToken | undefined; | ||
|
|
||
| if (!user?.sub) { | ||
| throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }); | ||
| } | ||
|
|
||
| const body = await readBody<{ collectionId?: string }>(event); | ||
|
|
||
| if (!body?.collectionId?.trim()) { | ||
| throw createError({ statusCode: 400, statusMessage: 'collectionId is required' }); | ||
| } | ||
|
|
||
| const cmDbPool = event.context.cmDbPool as Pool | undefined; | ||
|
|
||
| if (!cmDbPool) { | ||
| throw createError({ statusCode: 503, statusMessage: 'Database not available' }); | ||
| } | ||
|
|
||
| try { | ||
| const username = getAuthUsername(user.sub); | ||
|
|
||
| const ssoUserRepo = new InsightsSsoUserRepository(cmDbPool); | ||
| const ssoUser = await ssoUserRepo.findByUsername(username); | ||
|
|
||
| if (!ssoUser) { | ||
| return { success: true }; | ||
| } | ||
|
|
||
| const repo = new CollectionLikeRepository(cmDbPool); | ||
| await repo.unlike(body.collectionId.trim(), ssoUser.id); | ||
|
|
||
| return { success: true }; | ||
| } catch (error: unknown) { | ||
| if (error && typeof error === 'object' && 'statusCode' in error) { | ||
| throw error; | ||
| } | ||
| console.error('Error unliking collection:', error); | ||
| throw createError({ statusCode: 500, statusMessage: 'Internal Server Error' }); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| // Copyright (c) 2025 The Linux Foundation and each contributor. | ||
| // SPDX-License-Identifier: MIT | ||
| import type { Pool } from 'pg'; | ||
| import { CollectionLikeRepository, type LikedCollection } from '~~/server/repo/collectionLike.repo'; | ||
| import { InsightsSsoUserRepository } from '~~/server/repo/insightsSsoUser.repo'; | ||
| import type { DecodedOidcToken } from '~~/types/auth/auth-jwt.types'; | ||
| import type { Pagination } from '~~/types/shared/pagination'; | ||
| import { getAuthUsername } from '~~/server/utils/common'; | ||
|
|
||
| /** | ||
| * API Endpoint: GET /api/collection/like | ||
| * Description: Returns a paginated list of collections liked by the authenticated user. | ||
| * | ||
| * Query Parameters: | ||
| * - page (number, optional): The page number to fetch (default is 0). | ||
| * - pageSize (number, optional): The number of items per page (default is 10). | ||
| * | ||
| * Response: | ||
| * - 200: Paginated list of liked collections with author, name, description, logo, project count, and updatedAt | ||
| * - 401: Unauthorized | ||
| * - 500: Internal Server Error | ||
| */ | ||
| export default defineEventHandler(async (event): Promise<Pagination<LikedCollection> | Error> => { | ||
| const user = event.context.user as DecodedOidcToken | undefined; | ||
|
|
||
| if (!user?.sub) { | ||
| throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }); | ||
| } | ||
|
|
||
| const cmDbPool = event.context.cmDbPool as Pool | undefined; | ||
|
|
||
| if (!cmDbPool) { | ||
| throw createError({ statusCode: 503, statusMessage: 'Database not available' }); | ||
| } | ||
|
|
||
| const query = getQuery(event); | ||
| const page: number = Number(query?.page) || 0; | ||
| const pageSize: number = Number(query?.pageSize) || 10; | ||
|
|
||
| try { | ||
| const username = getAuthUsername(user.sub); | ||
|
|
||
| const ssoUserRepo = new InsightsSsoUserRepository(cmDbPool); | ||
| const ssoUser = await ssoUserRepo.findByUsername(username); | ||
|
|
||
| if (!ssoUser) { | ||
| return { | ||
| page, | ||
| pageSize, | ||
| total: 0, | ||
| data: [], | ||
| }; | ||
| } | ||
|
|
||
| const repo = new CollectionLikeRepository(cmDbPool); | ||
| const result = await repo.findLikedByUser(ssoUser.id, { page, pageSize }); | ||
|
|
||
| return { | ||
| page, | ||
| pageSize, | ||
| total: result.total, | ||
| data: result.data, | ||
| }; | ||
| } catch (error) { | ||
| console.error('Error fetching liked collections:', error); | ||
| throw createError({ statusCode: 500, statusMessage: 'Internal Server Error' }); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| // Copyright (c) 2025 The Linux Foundation and each contributor. | ||
| // SPDX-License-Identifier: MIT | ||
| import type { Pool } from 'pg'; | ||
| import { CollectionLikeRepository } from '~~/server/repo/collectionLike.repo'; | ||
| import { InsightsSsoUserRepository } from '~~/server/repo/insightsSsoUser.repo'; | ||
| import type { DecodedOidcToken } from '~~/types/auth/auth-jwt.types'; | ||
| import { getAuthUsername } from '~~/server/utils/common'; | ||
|
|
||
| /** | ||
| * API Endpoint: POST /api/collection/like | ||
| * Description: Likes a collection for the authenticated user. | ||
| * | ||
| * Request Body: | ||
| * - collectionId (string, required): The ID of the collection to like | ||
| * | ||
| * Response: | ||
| * - 200: Success | ||
| * - 400: Validation error | ||
| * - 401: Unauthorized | ||
| * - 500: Internal Server Error | ||
| */ | ||
| export default defineEventHandler(async (event): Promise<{ success: boolean } | Error> => { | ||
| const user = event.context.user as DecodedOidcToken | undefined; | ||
|
|
||
| if (!user?.sub) { | ||
| throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }); | ||
| } | ||
|
|
||
| const body = await readBody<{ collectionId?: string }>(event); | ||
|
|
||
| if (!body?.collectionId?.trim()) { | ||
| throw createError({ statusCode: 400, statusMessage: 'collectionId is required' }); | ||
| } | ||
|
|
||
| const cmDbPool = event.context.cmDbPool as Pool | undefined; | ||
|
|
||
| if (!cmDbPool) { | ||
| throw createError({ statusCode: 503, statusMessage: 'Database not available' }); | ||
| } | ||
|
|
||
| try { | ||
| const username = getAuthUsername(user.sub); | ||
|
|
||
| const ssoUserRepo = new InsightsSsoUserRepository(cmDbPool); | ||
| const ssoUser = await ssoUserRepo.upsert({ | ||
| id: user.sub, | ||
| displayName: user.name, | ||
| avatarUrl: user.picture, | ||
| email: user.email, | ||
| username, | ||
| }); | ||
|
|
||
| const collectionId = body.collectionId.trim(); | ||
|
|
||
| const repo = new CollectionLikeRepository(cmDbPool); | ||
| await repo.like(collectionId, ssoUser.id); | ||
|
|
||
| return { success: true }; | ||
| } catch (error: unknown) { | ||
| if (error && typeof error === 'object' && 'statusCode' in error) { | ||
| throw error; | ||
| } | ||
| // FK violation means the collection doesn't exist | ||
| if ( | ||
| error && | ||
| typeof error === 'object' && | ||
| 'code' in error && | ||
| (error as { code: string }).code === '23503' | ||
| ) { | ||
| throw createError({ statusCode: 404, statusMessage: 'Collection not found' }); | ||
| } | ||
| console.error('Error liking collection:', error); | ||
| throw createError({ statusCode: 500, statusMessage: 'Internal Server Error' }); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // Copyright (c) 2025 The Linux Foundation and each contributor. | ||
| // SPDX-License-Identifier: MIT | ||
| import type { ProjectInsights } from '~~/types/project'; | ||
| import { fetchFromTinybird } from '~~/server/data/tinybird/tinybird'; | ||
|
|
||
| /** | ||
| * API Endpoint: /api/project/insights | ||
| * Method: GET | ||
| * Description: Fetches project insights for multiple projects by slugs or IDs. | ||
| * | ||
| * Query Parameters: | ||
| * - slugs (string | string[]): One or more project slugs to fetch insights for. | ||
| * - ids (string | string[]): One or more project IDs to fetch insights for. | ||
| * | ||
| * At least one of `slugs` or `ids` must be provided. | ||
| * | ||
| * Response: | ||
| * - Array of ProjectInsights objects with achievements mapped to structured objects. | ||
| */ | ||
| export default defineEventHandler(async (event) => { | ||
| const query = getQuery(event); | ||
|
|
||
| const slugs = Array.isArray(query.slugs) | ||
| ? (query.slugs as string[]) | ||
| : query.slugs | ||
| ? [query.slugs as string] | ||
| : []; | ||
|
|
||
| const ids = Array.isArray(query.ids) | ||
| ? (query.ids as string[]) | ||
| : query.ids | ||
| ? [query.ids as string] | ||
| : []; | ||
|
|
||
| if (slugs.length === 0 && ids.length === 0) { | ||
| throw createError({ | ||
| statusCode: 400, | ||
| statusMessage: 'At least one project slug or id is required', | ||
| }); | ||
| } | ||
|
|
||
| try { | ||
| const response = await fetchFromTinybird<ProjectInsights[]>('/v0/pipes/project_insights.json', { | ||
| slugs: slugs.length > 0 ? slugs : undefined, | ||
| ids: ids.length > 0 ? ids : undefined, | ||
| }); | ||
|
|
||
| return response.data.map((project) => ({ | ||
| ...project, | ||
| isLF: !!project.isLF, | ||
| achievements: | ||
| project.achievements?.map(([leaderboardType, rank, totalCount]) => ({ | ||
| leaderboardType, | ||
| rank, | ||
| totalCount, | ||
| })) ?? [], | ||
|
||
| })); | ||
| } catch (error: unknown) { | ||
| console.error('Error fetching project insights:', error); | ||
| throw createError({ | ||
| statusCode: 500, | ||
| statusMessage: 'Failed to fetch project insights', | ||
| }); | ||
| } | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This endpoint returns a transformed shape (
isLFboolean + structuredachievements) but it fetches Tinybird data typed asProjectInsights[](which currently modelsisLFasnumberandachievementsas tuples). Please align the types by introducing a Tinybird/raw type for the fetch and a separate API response type for the mapped return value, then type the handler accordingly.