Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
59 changes: 24 additions & 35 deletions frontend/server/api/collection/[slug].ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) 2025 The Linux Foundation and each contributor.
// SPDX-License-Identifier: MIT
import { fetchFromTinybird } from '~~/server/data/tinybird/tinybird';
import type { Pool } from 'pg';
import type { Collection } from '~~/types/collection';
import { CommunityCollectionRepository } from '~~/server/repo/communityCollection.repo';

/**
* API Endpoint: Fetch Collection Details by Slug
Expand All @@ -16,45 +17,33 @@ import type { Collection } from '~~/types/collection';
* - slug (string) [URL Parameter]: The unique slug identifier for the collection.
*
* Response:
* - 200 OK: Returns the details of the collection in the following structure:
* {
* id: string;
* name: string;
* slug: string;
* description: string;
* isLf: number;
* projectCount: number;
* featuredProjects: {
* name: string;
* slug: string;
* logo: string;
* }[];
* }
*
* - 200 OK: Returns the collection details.
* - 404 Not Found: If the collection with the provided slug does not exist.
* {
* statusCode: 404;
* statusMessage: "Collection not found"
* }
*
* - 500 Internal Server Error: If an unexpected error occurs while processing the request.
* {
* statusCode: 500;
* statusMessage: "Internal server error"
* }
* - 500 Internal Server Error: If an unexpected error occurs.
*/
export default defineEventHandler(async (event): Promise<Collection | Error> => {
const { slug } = event.context.params as Record<string, string>;

const cmDbPool = event.context.cmDbPool as Pool | undefined;

if (!cmDbPool) {
throw createError({ statusCode: 503, statusMessage: 'Database not available' });
}

try {
const res = await fetchFromTinybird<Collection[]>('/v0/pipes/collections_list.json', {
slug,
});
if (!res.data || res.data.length === 0) {
return createError({ statusCode: 404, statusMessage: 'Collection not found' });
const repo = new CommunityCollectionRepository(cmDbPool);
const collection = await repo.findBySlug(slug);

if (!collection) {
throw createError({ statusCode: 404, statusMessage: 'Collection not found' });
}

return collection as unknown as Collection;
} catch (error: unknown) {
if (error && typeof error === 'object' && 'statusCode' in error) {
throw error;
}
return res.data[0];
} catch (err) {
console.error('Error fetching collection details:', err);
return createError({ statusCode: 500, statusMessage: 'Internal server error' });
console.error('Error fetching collection from DB:', error);
throw createError({ statusCode: 500, statusMessage: 'Internal server error' });
}
});
64 changes: 64 additions & 0 deletions frontend/server/api/collection/community/[id].delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) 2025 The Linux Foundation and each contributor.
// SPDX-License-Identifier: MIT
import type { Pool } from 'pg';
import { CommunityCollectionRepository } from '~~/server/repo/communityCollection.repo';
import { InsightsSsoUserRepository } from '~~/server/repo/insightsSsoUser.repo';
import type { DecodedOidcToken } from '~~/types/auth/auth-jwt.types';

/**
* API Endpoint: DELETE /api/collection/community/:id
* Description: Soft-deletes a community collection owned by the authenticated user.
*
* URL Parameters:
* - id (string, required): Collection ID
*
* Response:
* - 200: Success
* - 401: Unauthorized
* - 403: Forbidden (not the owner)
* - 404: Collection not found
* - 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 { id } = event.context.params as Record<string, string>;

if (!id) {
throw createError({ statusCode: 400, statusMessage: 'Collection ID is required' });
}

const cmDbPool = event.context.cmDbPool as Pool | undefined;

if (!cmDbPool) {
throw createError({ statusCode: 503, statusMessage: 'Database not available' });
}

try {
const username = user.sub.includes('|') ? user.sub.split('|').pop()! : 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 repo = new CommunityCollectionRepository(cmDbPool);
await repo.destroy(id, ssoUser.id);

return { success: true };
} catch (error: unknown) {
if (error && typeof error === 'object' && 'statusCode' in error) {
throw error;
}
console.error('Error deleting community collection:', error);
throw createError({ statusCode: 500, statusMessage: 'Internal Server Error' });
}
});
79 changes: 79 additions & 0 deletions frontend/server/api/collection/community/[id].put.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) 2025 The Linux Foundation and each contributor.
// SPDX-License-Identifier: MIT
import type { Pool } from 'pg';
import {
type CommunityCollection,
CommunityCollectionRepository,
type UpdateCommunityCollectionInput,
} from '~~/server/repo/communityCollection.repo';
import { InsightsSsoUserRepository } from '~~/server/repo/insightsSsoUser.repo';
import type { DecodedOidcToken } from '~~/types/auth/auth-jwt.types';

/**
* API Endpoint: PUT /api/collection/community/:id
* Description: Updates a community collection owned by the authenticated user.
*
* URL Parameters:
* - id (string, required): Collection ID
*
* Request Body:
* - name (string, optional): Collection name
* - description (string, optional): Collection description
* - isPrivate (boolean, optional): Whether the collection is private
* - projects (string[], optional): List of project IDs
*
* Response:
* - 200: Updated collection
* - 401: Unauthorized
* - 403: Forbidden (not the owner)
* - 404: Collection not found
* - 500: Internal Server Error
*/
export default defineEventHandler(async (event): Promise<CommunityCollection | Error> => {
const user = event.context.user as DecodedOidcToken | undefined;

if (!user?.sub) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
}

const { id } = event.context.params as Record<string, string>;

if (!id) {
throw createError({ statusCode: 400, statusMessage: 'Collection ID is required' });
}

const body = await readBody<Partial<UpdateCommunityCollectionInput>>(event);

const cmDbPool = event.context.cmDbPool as Pool | undefined;

if (!cmDbPool) {
throw createError({ statusCode: 503, statusMessage: 'Database not available' });
}

try {
const username = user.sub.includes('|') ? user.sub.split('|').pop()! : 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 repo = new CommunityCollectionRepository(cmDbPool);
return await repo.update(id, ssoUser.id, {
name: body.name?.trim(),
description: body.description?.trim(),
isPrivate: body.isPrivate,
projects: body.projects,
});
Comment on lines +65 to +71
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

When updating, name: body.name?.trim() will turn a whitespace-only value into an empty string, which then updates the collection name/slug to ''. If name is provided, validate it after trimming (non-empty), or treat empty/whitespace-only input as "no update" (i.e. undefined).

Copilot uses AI. Check for mistakes.
} catch (error: unknown) {
if (error && typeof error === 'object' && 'statusCode' in error) {
throw error;
}
console.error('Error updating community collection:', error);
throw createError({ statusCode: 500, statusMessage: 'Internal Server Error' });
}
});
79 changes: 79 additions & 0 deletions frontend/server/api/collection/community/index.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) 2025 The Linux Foundation and each contributor.
// SPDX-License-Identifier: MIT
import type { Pool } from 'pg';
import {
CommunityCollectionRepository,
type CommunityCollection,
type CreateCommunityCollectionInput,
} from '~~/server/repo/communityCollection.repo';
import { InsightsSsoUserRepository } from '~~/server/repo/insightsSsoUser.repo';
import type { DecodedOidcToken } from '~~/types/auth/auth-jwt.types';

/**
* API Endpoint: POST /api/collection/community
* Description: Creates a new community collection for the authenticated user.
*
* Request Body:
* - name (string, required): Collection name
* - description (string, optional): Collection description
* - isPrivate (boolean, optional): Whether the collection is private (default: false)
* - projects (string[], optional): List of project IDs
*
* Response:
* - 201: Created collection
* - 400: Validation error
* - 401: Unauthorized
* - 500: Internal Server Error
*/
export default defineEventHandler(async (event): Promise<CommunityCollection | Error> => {
const user = event.context.user as DecodedOidcToken | undefined;

if (!user?.sub) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
}

const body = await readBody<Partial<CreateCommunityCollectionInput>>(event);

if (!body?.name?.trim()) {
throw createError({ statusCode: 400, statusMessage: 'Name is required' });
}

const cmDbPool = event.context.cmDbPool as Pool | undefined;

if (!cmDbPool) {
throw createError({ statusCode: 503, statusMessage: 'Database not available' });
}

try {
// Derive username from Auth0 sub (e.g. "auth0|abc123" -> "abc123")
const username = user.sub.includes('|') ? user.sub.split('|').pop()! : user.sub;

// Upsert SSO user
const ssoUserRepo = new InsightsSsoUserRepository(cmDbPool);
const ssoUser = await ssoUserRepo.upsert({
id: user.sub,
displayName: user.name,
avatarUrl: user.picture,
email: user.email,
username,
});

const repo = new CommunityCollectionRepository(cmDbPool);
const collection = await repo.create({
name: body.name.trim(),
description: body.description?.trim(),
isPrivate: body.isPrivate,
ssoUserId: ssoUser.id,
projects: body.projects,
});

setResponseStatus(event, 201);
return collection;
} catch (error: unknown) {
if (error && typeof error === 'object' && 'statusCode' in error) {
throw error;
}
console.error('Error creating community collection:', error);
throw createError({ statusCode: 500, statusMessage: 'Internal Server Error' });
}
});
49 changes: 49 additions & 0 deletions frontend/server/api/collection/community/my.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2025 The Linux Foundation and each contributor.
// SPDX-License-Identifier: MIT
import type { Pool } from 'pg';
import {
CommunityCollectionRepository,
type CommunityCollection,
} from '~~/server/repo/communityCollection.repo';
import { InsightsSsoUserRepository } from '~~/server/repo/insightsSsoUser.repo';
import type { DecodedOidcToken } from '~~/types/auth/auth-jwt.types';

/**
* API Endpoint: GET /api/collection/community/my
* Description: Returns all community collections owned by the authenticated user.
*
* Response:
* - 200: List of collections
* - 401: Unauthorized
* - 500: Internal Server Error
*/
export default defineEventHandler(async (event): Promise<CommunityCollection[] | 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' });
}

try {
const username = user.sub.includes('|') ? user.sub.split('|').pop()! : user.sub;

const ssoUserRepo = new InsightsSsoUserRepository(cmDbPool);
const ssoUser = await ssoUserRepo.findByUsername(username);

if (!ssoUser) {
return [];
}

const repo = new CommunityCollectionRepository(cmDbPool);
return await repo.findBySsoUserId(ssoUser.id);
} catch (error) {
console.error('Error fetching user collections:', error);
throw createError({ statusCode: 500, statusMessage: 'Internal Server Error' });
}
});
Loading