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
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import router from './modules/index';
import { corsMiddleware } from './middlewares/cors.middleware';
import helmet from 'helmet';
import morgan from 'morgan';
import tspecOptions from './tspec.config';
import tspecOptions from './tspec.config';``
import { SendMail } from './utils/mail.utils';
import { appRateLimit } from './middlewares/rate.middleware';
import { requestIdMiddleware } from './middlewares/request-id.middleware';
Expand Down
40 changes: 40 additions & 0 deletions src/modules/creator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Creator Module

Initial scaffold for creator-facing API surfaces.

## Route structure

All routes are mounted under `/api/v1/creators`.

- `GET /` — existing paginated creator list.
- `GET /:creatorId/profile` — placeholder creator profile read endpoint.
- `PUT /:creatorId/profile` — placeholder creator profile write endpoint.

## Handler surface (scaffold)

### Read profile (`GET /:creatorId/profile`)

- Validates `creatorId` path parameter.
- Returns explicit placeholder response shape:
- `creatorId`
- `displayName`
- `bio`
- `avatarUrl`
- `links[]`
- `metadata.source` and `metadata.isProfileComplete`

### Write profile (`PUT /:creatorId/profile`)

- Validates `creatorId` path parameter.
- Validates payload fields:
- `displayName`
- `bio`
- `avatarUrl`
- `links[]` (`label`, `url`)
- Returns `202 Accepted` with validated payload echo + placeholder metadata.

## Notes for follow-up issues

- Authentication and authorization are intentionally deferred.
- Persistence/indexing integration is intentionally deferred.
- Current handlers are designed so storage/indexing can be added without changing route contracts.
99 changes: 99 additions & 0 deletions src/modules/creator/creator-profile.handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Request, Response } from 'express';
import {
sendError,
sendSuccess,
sendValidationError,
ErrorCode,
} from '../../utils/api-response.utils';
import {
CreatorProfileParamsSchema,
UpsertCreatorProfileBodySchema,
} from './creator-profile.schemas';
import {
getCreatorProfile,
upsertCreatorProfile,
} from './creator-profile.service';

/**
* @route GET /api/v1/creators/:creatorId/profile
* @desc Placeholder creator profile read endpoint
* @access Public (for scaffold only)
*/
export async function getCreatorProfileHandler(req: Request, res: Response) {
try {
const paramsResult = CreatorProfileParamsSchema.safeParse(req.params);
if (!paramsResult.success) {
return sendValidationError(
res,
'Invalid creator profile path parameters',
paramsResult.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
}))
);
}

const profile = await getCreatorProfile(paramsResult.data.creatorId);
return sendSuccess(res, profile, 200, 'Creator profile retrieved');
} catch (error) {
console.error('Error retrieving creator profile:', error);
return sendError(
res,
500,
ErrorCode.INTERNAL_ERROR,
'Failed to retrieve creator profile'
);
}
}

/**
* @route PUT /api/v1/creators/:creatorId/profile
* @desc Placeholder creator profile write endpoint
* @access Auth will be required in a follow-up issue
*/
export async function upsertCreatorProfileHandler(req: Request, res: Response) {
try {
const paramsResult = CreatorProfileParamsSchema.safeParse(req.params);
if (!paramsResult.success) {
return sendValidationError(
res,
'Invalid creator profile path parameters',
paramsResult.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
}))
);
}

const bodyResult = UpsertCreatorProfileBodySchema.safeParse(req.body);
if (!bodyResult.success) {
return sendValidationError(
res,
'Invalid creator profile payload',
bodyResult.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
}))
);
}

const profile = await upsertCreatorProfile(
paramsResult.data.creatorId,
bodyResult.data
);
return sendSuccess(
res,
profile,
202,
'Creator profile write accepted (placeholder)'
);
} catch (error) {
console.error('Error upserting creator profile:', error);
return sendError(
res,
500,
ErrorCode.INTERNAL_ERROR,
'Failed to upsert creator profile'
);
}
}
75 changes: 75 additions & 0 deletions src/modules/creator/creator-profile.schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { z } from 'zod';

/**
* Shared creator profile identifier schema for route params.
*
* We use a conservative format now (UUID-like or CUID-like IDs can be added later)
* and keep this centralized for future route extensions.
*/
export const CreatorProfileParamsSchema = z.object({
creatorId: z
.string()
.trim()
.min(1, 'Creator ID is required')
.max(128, 'Creator ID is too long'),
});

/**
* Placeholder read response shape for GET /api/v1/creators/:creatorId/profile.
*
* The shape is explicit now so future indexing-backed values can be dropped in
* without changing API contracts.
*/
export const CreatorProfileReadResponseSchema = z.object({
creatorId: z.string(),
displayName: z.string().nullable(),
bio: z.string().nullable(),
avatarUrl: z.string().url().nullable(),
links: z.array(z.object({ label: z.string(), url: z.string().url() })),
metadata: z.object({
source: z.enum(['placeholder']),
isProfileComplete: z.boolean(),
}),
});

/**
* Placeholder write payload for PUT /api/v1/creators/:creatorId/profile.
*
* Validation is intentionally strict and explicit so the eventual persistence layer
* can safely trust handler inputs.
*/
export const UpsertCreatorProfileBodySchema = z.object({
displayName: z
.string()
.trim()
.min(2, 'Display name must be at least 2 characters')
.max(80, 'Display name must be at most 80 characters')
.optional(),
bio: z
.string()
.trim()
.max(1000, 'Bio must be at most 1000 characters')
.optional(),
avatarUrl: z.string().trim().url('Avatar URL must be a valid URL').optional(),
links: z
.array(
z.object({
label: z
.string()
.trim()
.min(1, 'Link label is required')
.max(40, 'Link label must be at most 40 characters'),
url: z.string().trim().url('Link URL must be a valid URL'),
})
)
.max(8, 'At most 8 profile links are allowed')
.optional(),
});

export type CreatorProfileParams = z.infer<typeof CreatorProfileParamsSchema>;
export type CreatorProfileReadResponse = z.infer<
typeof CreatorProfileReadResponseSchema
>;
export type UpsertCreatorProfileBody = z.infer<
typeof UpsertCreatorProfileBodySchema
>;
50 changes: 50 additions & 0 deletions src/modules/creator/creator-profile.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
CreatorProfileReadResponse,
UpsertCreatorProfileBody,
} from './creator-profile.schemas';

/**
* Placeholder profile read service.
*
* TODO(accesslayer): Replace this placeholder source with database/indexing-backed
* reads in a follow-up issue.
*/
export async function getCreatorProfile(
creatorId: string
): Promise<CreatorProfileReadResponse> {
return {
creatorId,
displayName: null,
bio: null,
avatarUrl: null,
links: [],
metadata: {
source: 'placeholder',
isProfileComplete: false,
},
};
}

/**
* Placeholder profile upsert service.
*
* TODO(accesslayer): Wire this to authenticated profile persistence when
* creator identity and ownership rules are finalized.
*/
export async function upsertCreatorProfile(
creatorId: string,
payload: UpsertCreatorProfileBody
): Promise<{
creatorId: string;
acceptedProfile: UpsertCreatorProfileBody;
metadata: { source: 'placeholder'; persisted: false };
}> {
return {
creatorId,
acceptedProfile: payload,
metadata: {
source: 'placeholder',
persisted: false,
},
};
}
26 changes: 26 additions & 0 deletions src/modules/creator/creator.routes.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
// src/modules/creator/creator.routes.ts
import { Router } from 'express';
import { listCreators } from './creator.controller';
import {
getCreatorProfileHandler,
upsertCreatorProfileHandler,
} from './creator-profile.handlers';
import { ROOT as CREATORS_ROOT } from '../../constants/creator.constants';

const router = Router();

/**
* Creator module route map (initial scaffold):
*
* - GET /api/v1/creators
* - GET /api/v1/creators/:creatorId/profile
* - PUT /api/v1/creators/:creatorId/profile
*/

/**
* @route GET /api/v1/creators
* @desc Get a paginated list of creators
* @access Public
*/
router.get(CREATORS_ROOT, listCreators);

/**
* @route GET /api/v1/creators/:creatorId/profile
* @desc Get creator profile scaffold payload
* @access Public
*/
router.get('/:creatorId/profile', getCreatorProfileHandler);

/**
* @route PUT /api/v1/creators/:creatorId/profile
* @desc Upsert creator profile scaffold payload
* @access Public for scaffold (auth follow-up required)
*/
router.put('/:creatorId/profile', upsertCreatorProfileHandler);

export default router;
6 changes: 3 additions & 3 deletions src/modules/creators/creators.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
sendValidationError,
} from '../../utils/api-response.utils';
import { parsePublicQuery } from '../../utils/public-query-parse.utils';
import { buildOffsetPaginationMeta } from '../../utils/pagination.utils';

/**
* Controller for GET /api/v1/creators
Expand All @@ -33,12 +34,11 @@ export const httpListCreators: AsyncController = async (req, res, next) => {
// Serialize response
const response: CreatorListResponse = {
creators: serializeCreatorList(creators),
pagination: {
pagination: buildOffsetPaginationMeta({
limit: validatedQuery.limit,
offset: validatedQuery.offset,
total,
hasMore: validatedQuery.offset + validatedQuery.limit < total,
},
}),
};

sendSuccess(res, response);
Expand Down
6 changes: 3 additions & 3 deletions src/modules/creators/creators.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CreatorProfile } from '../../types/profile.types';
import { CreatorListQueryType } from './creators.schemas';
import { mapCreatorListSort } from './creators.sort';
import { CreatorListResponse } from './creators.serializers';
import { buildOffsetPaginationMeta } from '../../utils/pagination.utils';

type CreatorListWhere = {
isVerified?: boolean;
Expand Down Expand Up @@ -71,11 +72,10 @@ export function createEmptyCreatorListResponse(
): CreatorListResponse {
return {
creators: [],
pagination: {
pagination: buildOffsetPaginationMeta({
limit: query.limit,
offset: query.offset,
total: 0,
hasMore: false,
},
}),
};
}
5 changes: 5 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import app from './app';
import { envConfig } from './config';
import { logger } from './utils/logger.utils';
import { prisma } from './utils/prisma.utils';
import dotenv from 'dotenv'


dotenv.config()


async function startServer() {
try {
Expand Down
Loading
Loading