Skip to content
Merged
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
16 changes: 16 additions & 0 deletions prisma/schema/creator.prisma
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions prisma/schema/user.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ model User {
avatar String?

stellarWallet StellarWallet?
creatorProfile CreatorProfile?
}
31 changes: 31 additions & 0 deletions src/modules/creator/creator.controller.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
14 changes: 14 additions & 0 deletions src/modules/creator/creator.routes.ts
Original file line number Diff line number Diff line change
@@ -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;
45 changes: 45 additions & 0 deletions src/modules/creator/creator.service.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
43 changes: 43 additions & 0 deletions src/modules/creator/creator.utils.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
2 changes: 2 additions & 0 deletions src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
41 changes: 41 additions & 0 deletions src/utils/api-response.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@ interface ApiSuccessResponse<T = unknown> {
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<T = unknown> {
success: true;
data: T[];
meta: PaginationMetadata;
message?: string;
}

// ── Error codes ──────────────────────────────────────────────

export const ErrorCode = {
Expand Down Expand Up @@ -92,6 +114,25 @@ export function sendSuccess<T>(
res.status(statusCode).json(body);
}

/**
* Send a formatted paginated success response.
*/
export function sendPaginatedSuccess<T>(
res: Response,
data: T[],
meta: PaginationMetadata,
statusCode = 200,
message?: string
): void {
const body: PaginatedResponse<T> = {
success: true,
data,
meta,
...(message ? { message } : {}),
};
res.status(statusCode).json(body);
}

// ── Convenience helpers ──────────────────────────────────────

export function sendValidationError(
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"lib": ["es6"],
"lib": ["es2017", "dom"],
"allowJs": true,
"outDir": "dist",
"rootDir": "src",
Expand Down
Loading