diff --git a/prisma/schema/creator.prisma b/prisma/schema/creator.prisma new file mode 100644 index 0000000..6cfc6d5 --- /dev/null +++ b/prisma/schema/creator.prisma @@ -0,0 +1,16 @@ +// prisma/schema/creator.prisma + +model CreatorProfile { + id String @id @default(cuid()) + userId String @unique + handle String @unique + displayName String + bio String? + avatarUrl String? + perkSummary String? + isVerified Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} diff --git a/prisma/schema/user.prisma b/prisma/schema/user.prisma index 7e43289..c33571d 100644 --- a/prisma/schema/user.prisma +++ b/prisma/schema/user.prisma @@ -12,4 +12,5 @@ model User { avatar String? stellarWallet StellarWallet? + creatorProfile CreatorProfile? } \ No newline at end of file diff --git a/src/modules/creator/creator.controller.ts b/src/modules/creator/creator.controller.ts new file mode 100644 index 0000000..bdde209 --- /dev/null +++ b/src/modules/creator/creator.controller.ts @@ -0,0 +1,31 @@ +// src/modules/creator/creator.controller.ts +import { Request, Response } from 'express'; +import { sendPaginatedSuccess, sendError, ErrorCode } from '../../utils/api-response.utils'; +import { getPaginatedCreators } from './creator.service'; +import { parseCreatorSortOptions } from './creator.utils'; + +export async function listCreators(req: Request, res: Response) { + try { + const page = parseInt(req.query.page as string) || 1; + const limit = parseInt(req.query.limit as string) || 10; + const sortBy = req.query.sortBy as string; + const sortOrder = req.query.sortOrder as string; + + if (page < 1 || limit < 1) { + return sendError(res, 400, ErrorCode.VALIDATION_ERROR, 'Invalid pagination parameters'); + } + + const sort = parseCreatorSortOptions(sortBy, sortOrder); + + const { creators, meta } = await getPaginatedCreators({ + page, + limit, + sort, + }); + + return sendPaginatedSuccess(res, creators, meta, 200, 'Creators retrieved successfully'); + } catch (error) { + console.error('Error listing creators:', error); + return sendError(res, 500, ErrorCode.INTERNAL_ERROR, 'Failed to retrieve creators'); + } +} diff --git a/src/modules/creator/creator.routes.ts b/src/modules/creator/creator.routes.ts new file mode 100644 index 0000000..aa131ea --- /dev/null +++ b/src/modules/creator/creator.routes.ts @@ -0,0 +1,14 @@ +// src/modules/creator/creator.routes.ts +import { Router } from 'express'; +import { listCreators } from './creator.controller'; + +const router = Router(); + +/** + * @route GET /api/v1/creators + * @desc Get a paginated list of creators + * @access Public + */ +router.get('/', listCreators); + +export default router; diff --git a/src/modules/creator/creator.service.ts b/src/modules/creator/creator.service.ts new file mode 100644 index 0000000..b90bb6e --- /dev/null +++ b/src/modules/creator/creator.service.ts @@ -0,0 +1,45 @@ +import { CreatorSortOptions, toPrismaOrderBy } from './creator.utils'; +import { PaginationMetadata } from '../../utils/api-response.utils'; +import { prisma } from '../../utils/prisma.utils'; + +export interface GetCreatorsParams { + page: number; + limit: number; + sort: CreatorSortOptions; +} + +export async function getPaginatedCreators(params: GetCreatorsParams) { + const { page, limit, sort } = params; + const skip = (page - 1) * limit; + + const [creators, totalCount] = await Promise.all([ + prisma.creatorProfile.findMany({ + skip, + take: limit, + orderBy: toPrismaOrderBy(sort), + include: { + user: { + select: { + avatar: true, + firstName: true, + lastName: true, + }, + }, + }, + }), + prisma.creatorProfile.count(), + ]); + + const totalPages = Math.ceil(totalCount / limit); + + const meta: PaginationMetadata = { + page, + limit, + totalCount, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1, + }; + + return { creators, meta }; +} diff --git a/src/modules/creator/creator.utils.ts b/src/modules/creator/creator.utils.ts new file mode 100644 index 0000000..2631840 --- /dev/null +++ b/src/modules/creator/creator.utils.ts @@ -0,0 +1,43 @@ +// src/modules/creator/creator.utils.ts +import { Prisma } from '@prisma/client'; + +export type CreatorSortField = 'createdAt' | 'handle' | 'displayName'; +export type SortOrder = 'asc' | 'desc'; + +export interface CreatorSortOptions { + field: CreatorSortField; + order: SortOrder; +} + +/** + * Parse and validate creator sort options. + * Defaults to createdAt: desc if input is invalid or missing. + */ +export function parseCreatorSortOptions( + sortBy?: string, + sortOrder?: string +): CreatorSortOptions { + const validFields: CreatorSortField[] = ['createdAt', 'handle', 'displayName']; + const validOrders: SortOrder[] = ['asc', 'desc']; + + const field = validFields.includes(sortBy as CreatorSortField) + ? (sortBy as CreatorSortField) + : 'createdAt'; + + const order = validOrders.includes(sortOrder as SortOrder) + ? (sortOrder as SortOrder) + : 'desc'; + + return { field, order }; +} + +/** + * Convert sort options to Prisma orderBy object. + */ +export function toPrismaOrderBy( + options: CreatorSortOptions +): Prisma.CreatorProfileOrderByWithRelationInput { + return { + [options.field]: options.order, + }; +} diff --git a/src/modules/index.ts b/src/modules/index.ts index ff59f82..eefeac0 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -2,11 +2,13 @@ import { Router } from 'express'; import authRouter from './auth/auth.routes'; import healthRouter from './health/health.routes'; import configRouter from './config/config.routes'; +import creatorRouter from './creator/creator.routes'; const router = Router(); router.use('/health', healthRouter); router.use('/auth', authRouter); router.use('/config', configRouter); +router.use('/creators', creatorRouter); export default router; diff --git a/src/utils/api-response.utils.ts b/src/utils/api-response.utils.ts index 7db7cc0..11baddb 100644 --- a/src/utils/api-response.utils.ts +++ b/src/utils/api-response.utils.ts @@ -37,6 +37,28 @@ interface ApiSuccessResponse { message?: string; } +/** + * Standard API pagination metadata. + */ +export interface PaginationMetadata { + page: number; + limit: number; + totalCount: number; + totalPages: number; + hasNextPage: boolean; + hasPrevPage: boolean; +} + +/** + * Standard paginated API response shape. + */ +interface PaginatedResponse { + success: true; + data: T[]; + meta: PaginationMetadata; + message?: string; +} + // ── Error codes ────────────────────────────────────────────── export const ErrorCode = { @@ -92,6 +114,25 @@ export function sendSuccess( res.status(statusCode).json(body); } +/** + * Send a formatted paginated success response. + */ +export function sendPaginatedSuccess( + res: Response, + data: T[], + meta: PaginationMetadata, + statusCode = 200, + message?: string +): void { + const body: PaginatedResponse = { + success: true, + data, + meta, + ...(message ? { message } : {}), + }; + res.status(statusCode).json(body); +} + // ── Convenience helpers ────────────────────────────────────── export function sendValidationError( diff --git a/tsconfig.json b/tsconfig.json index 4bb9819..ffbdc7e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "es2017", "module": "commonjs", - "lib": ["es6"], + "lib": ["es2017", "dom"], "allowJs": true, "outDir": "dist", "rootDir": "src",