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
48 changes: 48 additions & 0 deletions src/modules/creator/creator.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { handleCreatorParamNotFound } from './creator.utils';
import { sendNotFound } from '../../utils/api-response.utils';

jest.mock('../../utils/api-response.utils', () => ({
sendNotFound: jest.fn(),
}));

function makeRes(): any {
return { status: jest.fn().mockReturnThis(), json: jest.fn() };
}

describe('handleCreatorParamNotFound', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('returns false and sends 404 when result is null', () => {
const res = makeRes();
const result = null;

const ok = handleCreatorParamNotFound(res, result);

expect(ok).toBe(false);
expect(sendNotFound).toHaveBeenCalledWith(res, 'Creator');
});

it('returns true and does not send error when result exists', () => {
const res = makeRes();
const result = { id: 'creator-123', handle: 'alice' };

const ok = handleCreatorParamNotFound(res, result);

expect(ok).toBe(true);
expect(sendNotFound).not.toHaveBeenCalled();
});

it('narrows type so caller can access id after check', () => {
const res = makeRes();
const result = { id: 'creator-123', handle: 'alice' };

if (!handleCreatorParamNotFound(res, result)) {
throw new Error('Should not reach here');
}

expect(result.id).toBe('creator-123');
expect(result.handle).toBe('alice');
});
});
36 changes: 36 additions & 0 deletions src/modules/creator/creator.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
isRecognizedCreatorListSortField,
warnIfUnrecognizedCreatorListSort,
} from '../creators/creators.sort-field.utils';
import { sendNotFound } from '../../utils/api-response.utils';
import type { Response } from 'express';

export type CreatorSortField = CreatorListSortField;
export type SortOrder = CreatorListSortOrder;
Expand Down Expand Up @@ -75,3 +77,37 @@ export async function resolveCreatorSlugCollision(
return !existing;
});
}

/**
* Result of a creator param lookup: either the creator exists or has gone missing.
* Used by endpoint handlers to centralize the not-found response pattern.
*/
export type CreatorParamCheckResult = { id: string; handle: string } | null;

/**
* Shortcut for returning a 404 when a creator param lookup fails.
*
* Encapsulates the common pattern of checking a creator lookup result
* and returning a not-found response if the creator does not exist.
* Use this to reduce boilerplate in route handlers that need to validate
* creator params before proceeding.
*
* @param res - Express Response object (must be passed directly)
* @param result - The result from findCreatorByIdOrHandle or similar lookup
* @returns true if the creator exists (caller should proceed), false if 404 was sent
*
* @example
* const creator = await findCreatorByIdOrHandle(creatorId);
* if (!handleCreatorParamNotFound(res, creator)) return;
* // ... proceed with handler logic knowing creator exists
*/
export function handleCreatorParamNotFound<T extends { id: string } | null>(
res: Response,
result: T
): result is T extends null ? never : T {
if (!result) {
sendNotFound(res, 'Creator');
return false as const;
}
return true as const;
}
58 changes: 28 additions & 30 deletions src/modules/creators/creator-holders.controller.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { AsyncController } from '../../types/auth.types';
import { CreatorHoldersQuerySchema } from './creator-holders.schemas';
import {
findCreatorByIdOrHandle,
fetchCreatorHolders,
findCreatorByIdOrHandle,
fetchCreatorHolders,
} from './creator-holders.service';
import {
sendSuccess,
sendNotFound,
sendValidationError,
sendSuccess,
sendValidationError,
} from '../../utils/api-response.utils';
import { attachTimestampHeader } from '../../utils/timestamp-headers.utils';
import { parsePublicQuery } from '../../utils/public-query-parse.utils';
import { buildOffsetPaginationMeta } from '../../utils/pagination.utils';
import { handleCreatorParamNotFound } from '../creator/creator.utils';

/**
* Controller for GET /api/v1/creators/:id/holders
Expand All @@ -25,34 +25,32 @@ import { buildOffsetPaginationMeta } from '../../utils/pagination.utils';
* - Optional ?sort=held_since returns earliest buyers first.
*/
export const httpGetCreatorHolders: AsyncController = async (req, res, next) => {
try {
const rawId = req.params['id'];
const id = typeof rawId === 'string' ? rawId : String(rawId ?? '');
try {
const rawId = req.params['id'];
const id = typeof rawId === 'string' ? rawId : String(rawId ?? '');

const parsed = parsePublicQuery(CreatorHoldersQuerySchema, req.query, {
debugContext: 'creator-holders-query',
});
const parsed = parsePublicQuery(CreatorHoldersQuerySchema, req.query, {
debugContext: 'creator-holders-query',
});

if (!parsed.ok) {
return sendValidationError(res, 'Invalid query parameters', parsed.details);
}
if (!parsed.ok) {
return sendValidationError(res, 'Invalid query parameters', parsed.details);
}

const creator = await findCreatorByIdOrHandle(id);
if (!creator) {
return sendNotFound(res, 'Creator');
}
const creator = await findCreatorByIdOrHandle(id);
if (!handleCreatorParamNotFound(res, creator)) return;

const [holders, total] = await fetchCreatorHolders(creator.id, parsed.data);
const [holders, total] = await fetchCreatorHolders(creator.id, parsed.data);

const meta = buildOffsetPaginationMeta({
limit: parsed.data.limit,
offset: parsed.data.offset,
total,
});
const meta = buildOffsetPaginationMeta({
limit: parsed.data.limit,
offset: parsed.data.offset,
total,
});

attachTimestampHeader(res);
sendSuccess(res, { items: holders, meta });
} catch (error) {
next(error);
}
};
attachTimestampHeader(res);
sendSuccess(res, { items: holders, meta });
} catch (error) {
next(error);
}
};
Loading