From 097e08ae6ae7d25d5f469a47e6108d56ee257dab Mon Sep 17 00:00:00 2001 From: dubemoyibe-star Date: Mon, 1 Jun 2026 00:37:21 +0100 Subject: [PATCH 1/2] Added Structured logs for creator list query --- .../creators/creator-list-item.mapper.test.ts | 82 ++++++++++++++++++- .../creators/creator-list-item.mapper.ts | 46 +++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/src/modules/creators/creator-list-item.mapper.test.ts b/src/modules/creators/creator-list-item.mapper.test.ts index 634be82..9d11b5c 100644 --- a/src/modules/creators/creator-list-item.mapper.test.ts +++ b/src/modules/creators/creator-list-item.mapper.test.ts @@ -3,13 +3,15 @@ import { requestContextStorage } from '../../utils/als.utils'; import { logger } from '../../utils/logger.utils'; jest.mock('../../utils/logger.utils', () => ({ - logger: { warn: jest.fn() }, + logger: { warn: jest.fn(), error: jest.fn() }, })); const warnMock = logger.warn as jest.Mock; +const errorMock = logger.error as jest.Mock; beforeEach(() => { warnMock.mockClear(); + errorMock.mockClear(); }); describe('mapCreatorListItem()', () => { @@ -64,4 +66,82 @@ describe('mapCreatorListItem()', () => { requestId: 'req-333', }); }); + + it('logs an error when a string field receives an unexpected type', () => { + const input = { + id: 'creator-2', + handle: 'test-handle', + displayName: 12345, // number instead of string + avatarUrl: null, + isVerified: false, + createdAt: new Date('2024-01-02T03:04:05.678Z'), + updatedAt: new Date('2024-01-03T03:04:05.678Z'), + } as any; + + const result = requestContextStorage.run( + { path: '/api/v1/creators', method: 'GET', requestId: 'req-type-1' }, + () => mapCreatorListItem(input) + ); + + expect(result).toEqual({ + id: 'creator-2', + name: 12345, + avatar: null, + followers: 0, + createdAt: '2024-01-02T03:04:05.678Z', + updatedAt: '2024-01-03T03:04:05.678Z', + }); + expect(errorMock).toHaveBeenCalledWith({ + msg: 'Creator list field type mismatch', + fieldName: 'displayName', + expectedType: 'string', + receivedType: 'number', + creatorId: 'creator-2', + requestId: 'req-type-1', + }); + }); + + it('logs an error when a Date field receives a string', () => { + const input = { + id: 'creator-3', + handle: 'test-handle', + displayName: 'Alice', + avatarUrl: null, + isVerified: true, + createdAt: '2024-01-02T03:04:05.678Z', // string instead of Date + updatedAt: new Date('2024-01-03T03:04:05.678Z'), + } as any; + + requestContextStorage.run( + { path: '/api/v1/creators', method: 'GET', requestId: 'req-type-2' }, + () => mapCreatorListItem(input) + ); + + expect(errorMock).toHaveBeenCalledWith( + expect.objectContaining({ + msg: 'Creator list field type mismatch', + fieldName: 'createdAt', + expectedType: 'Date', + receivedType: 'string', + creatorId: 'creator-3', + requestId: 'req-type-2', + }) + ); + }); + + it('does not log an error for correctly typed fields', () => { + const input = { + id: 'creator-4', + handle: 'good-handle', + displayName: 'Bob', + avatarUrl: 'https://example.com/avatar.png', + isVerified: false, + createdAt: new Date('2024-01-02T03:04:05.678Z'), + updatedAt: new Date('2024-01-03T03:04:05.678Z'), + } as any; + + mapCreatorListItem(input); + + expect(errorMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/modules/creators/creator-list-item.mapper.ts b/src/modules/creators/creator-list-item.mapper.ts index a3682e7..37c2053 100644 --- a/src/modules/creators/creator-list-item.mapper.ts +++ b/src/modules/creators/creator-list-item.mapper.ts @@ -17,6 +17,44 @@ export type CreatorListItem = { updatedAt: string; }; +type ExpectedFieldType = 'string' | 'boolean' | 'number' | 'Date'; + +// Runtime type expectations for fields projected by CREATOR_LIST_DEFAULT_SELECT. +// A mismatch here signals a DB migration changed a column type without updating the mapping. +const CREATOR_LIST_FIELD_EXPECTED_TYPES: Record = { + id: 'string', + handle: 'string', + displayName: 'string', + avatarUrl: 'string', + isVerified: 'boolean', + createdAt: 'Date', + updatedAt: 'Date', +}; + +function logIfFieldTypeMismatch( + creator: CreatorProfile, + fieldName: keyof typeof CREATOR_LIST_FIELD_EXPECTED_TYPES +): void { + const value = (creator as Record)[fieldName]; + + if (value === null || value === undefined) return; + + const expectedType = CREATOR_LIST_FIELD_EXPECTED_TYPES[fieldName]; + const typeMatches = + expectedType === 'Date' ? value instanceof Date : typeof value === expectedType; + + if (!typeMatches) { + logger.error({ + msg: 'Creator list field type mismatch', + fieldName, + expectedType, + receivedType: value instanceof Date ? 'Date' : typeof value, + creatorId: creator.id, + requestId: requestContextStorage.getStore()?.requestId ?? null, + }); + } +} + function warnIfUnexpectedNullCreatorField( creator: CreatorProfile, fieldName: 'displayName' @@ -44,6 +82,14 @@ export const mapCreatorListItem = ( ): CreatorListItem => { warnIfUnexpectedNullCreatorField(creator, 'displayName'); + logIfFieldTypeMismatch(creator, 'id'); + logIfFieldTypeMismatch(creator, 'handle'); + logIfFieldTypeMismatch(creator, 'displayName'); + logIfFieldTypeMismatch(creator, 'avatarUrl'); + logIfFieldTypeMismatch(creator, 'isVerified'); + logIfFieldTypeMismatch(creator, 'createdAt'); + logIfFieldTypeMismatch(creator, 'updatedAt'); + return { id: creator.id, name: safeRead(creator, 'displayName', null), From 5218976e76440718b0a48e32a33eb0020ac7131e Mon Sep 17 00:00:00 2001 From: dubemoyibe-star Date: Mon, 1 Jun 2026 00:52:10 +0100 Subject: [PATCH 2/2] fixed typescript overlap errors --- src/modules/creators/creator-list-item.mapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/creators/creator-list-item.mapper.ts b/src/modules/creators/creator-list-item.mapper.ts index 37c2053..8569030 100644 --- a/src/modules/creators/creator-list-item.mapper.ts +++ b/src/modules/creators/creator-list-item.mapper.ts @@ -35,7 +35,7 @@ function logIfFieldTypeMismatch( creator: CreatorProfile, fieldName: keyof typeof CREATOR_LIST_FIELD_EXPECTED_TYPES ): void { - const value = (creator as Record)[fieldName]; + const value = (creator as unknown as Record)[fieldName]; if (value === null || value === undefined) return;