view data fixes

This commit is contained in:
2026-01-24 00:52:27 +01:00
parent 62e8b768ce
commit ae59df61eb
321 changed files with 1157 additions and 2234 deletions

View File

@@ -2216,6 +2216,41 @@
"incidents"
]
},
"DashboardStatsResponseDTO": {
"type": "object",
"properties": {
"totalUsers": {
"type": "number"
},
"activeUsers": {
"type": "number"
},
"suspendedUsers": {
"type": "number"
},
"deletedUsers": {
"type": "number"
},
"systemAdmins": {
"type": "number"
},
"recentLogins": {
"type": "number"
},
"newUsersToday": {
"type": "number"
}
},
"required": [
"totalUsers",
"activeUsers",
"suspendedUsers",
"deletedUsers",
"systemAdmins",
"recentLogins",
"newUsersToday"
]
},
"DeleteMediaOutputDTO": {
"type": "object",
"properties": {
@@ -4235,6 +4270,9 @@
"LeagueScheduleDTO": {
"type": "object",
"properties": {
"leagueId": {
"type": "string"
},
"seasonId": {
"type": "string"
},
@@ -4473,6 +4511,16 @@
},
"isParallelActive": {
"type": "boolean"
},
"totalRaces": {
"type": "number"
},
"completedRaces": {
"type": "number"
},
"nextRaceAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
@@ -4480,7 +4528,9 @@
"name",
"status",
"isPrimary",
"isParallelActive"
"isParallelActive",
"totalRaces",
"completedRaces"
]
},
"LeagueSettingsDTO": {
@@ -4515,6 +4565,18 @@
},
"races": {
"type": "number"
},
"positionChange": {
"type": "number"
},
"lastRacePoints": {
"type": "number"
},
"droppedRaceIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
@@ -4524,7 +4586,10 @@
"position",
"wins",
"podiums",
"races"
"races",
"positionChange",
"lastRacePoints",
"droppedRaceIds"
]
},
"LeagueStandingsDTO": {
@@ -4658,6 +4723,15 @@
"logoUrl": {
"type": "string",
"nullable": true
},
"pendingJoinRequestsCount": {
"type": "number"
},
"pendingProtestsCount": {
"type": "number"
},
"walletBalance": {
"type": "number"
}
},
"required": [
@@ -5449,8 +5523,34 @@
"type": "string"
},
"leagueName": {
"type": "string",
"nullable": true
"type": "string"
},
"track": {
"type": "string"
},
"car": {
"type": "string"
},
"sessionType": {
"type": "string"
},
"leagueId": {
"type": "string"
},
"strengthOfField": {
"type": "number"
},
"isUpcoming": {
"type": "boolean"
},
"isLive": {
"type": "boolean"
},
"isPast": {
"type": "boolean"
},
"status": {
"type": "string"
}
},
"required": [

View File

@@ -1,80 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
class UserGrowthDto {
@ApiProperty({ description: 'Label for the time period' })
label!: string;
@ApiProperty({ description: 'Number of new users' })
value!: number;
@ApiProperty({ description: 'Color class for the bar' })
color!: string;
}
class RoleDistributionDto {
@ApiProperty({ description: 'Role name' })
label!: string;
@ApiProperty({ description: 'Number of users with this role' })
value!: number;
@ApiProperty({ description: 'Color class for the bar' })
color!: string;
}
class StatusDistributionDto {
@ApiProperty({ description: 'Number of active users' })
active!: number;
@ApiProperty({ description: 'Number of suspended users' })
suspended!: number;
@ApiProperty({ description: 'Number of deleted users' })
deleted!: number;
}
class ActivityTimelineDto {
@ApiProperty({ description: 'Date label' })
date!: string;
@ApiProperty({ description: 'Number of new users' })
newUsers!: number;
@ApiProperty({ description: 'Number of logins' })
logins!: number;
}
export class DashboardStatsResponseDto {
@ApiProperty({ description: 'Total number of users' })
totalUsers!: number;
@ApiProperty({ description: 'Number of active users' })
activeUsers!: number;
@ApiProperty({ description: 'Number of suspended users' })
suspendedUsers!: number;
@ApiProperty({ description: 'Number of deleted users' })
deletedUsers!: number;
@ApiProperty({ description: 'Number of system admins' })
systemAdmins!: number;
@ApiProperty({ description: 'Number of recent logins (last 24h)' })
recentLogins!: number;
@ApiProperty({ description: 'Number of new users today' })
newUsersToday!: number;
@ApiProperty({ type: [UserGrowthDto], description: 'User growth over last 7 days' })
userGrowth!: UserGrowthDto[];
@ApiProperty({ type: [RoleDistributionDto], description: 'Distribution of user roles' })
roleDistribution!: RoleDistributionDto[];
@ApiProperty({ type: StatusDistributionDto, description: 'Distribution of user statuses' })
statusDistribution!: StatusDistributionDto;
@ApiProperty({ type: [ActivityTimelineDto], description: 'Activity timeline for last 7 days' })
activityTimeline!: ActivityTimelineDto[];
}

View File

@@ -0,0 +1,24 @@
export interface HealthDTO {
status: 'ok' | 'degraded' | 'error' | 'unknown';
timestamp: string;
uptime?: number;
responseTime?: number;
errorRate?: number;
lastCheck?: string;
checksPassed?: number;
checksFailed?: number;
components?: Array<{
name: string;
status: 'ok' | 'degraded' | 'error' | 'unknown';
lastCheck?: string;
responseTime?: number;
errorRate?: number;
}>;
alerts?: Array<{
id: string;
type: 'critical' | 'warning' | 'info';
title: string;
message: string;
timestamp: string;
}>;
}

View File

@@ -1,9 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsBoolean, IsString, ValidateNested } from 'class-validator';
import { RaceDTO } from '../../race/dtos/RaceDTO';
export class LeagueScheduleDTO {
@ApiPropertyOptional()
@IsString()
leagueId?: string;
@ApiProperty()
@IsString()
seasonId!: string;

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class RaceDTO {
@ApiProperty()
@@ -10,6 +10,33 @@ export class RaceDTO {
@ApiProperty()
date!: string;
@ApiProperty({ nullable: true })
@ApiPropertyOptional({ nullable: true })
leagueName?: string;
@ApiPropertyOptional()
track?: string;
@ApiPropertyOptional()
car?: string;
@ApiPropertyOptional()
sessionType?: string;
@ApiPropertyOptional()
leagueId?: string;
@ApiPropertyOptional()
strengthOfField?: number;
@ApiPropertyOptional()
isUpcoming?: boolean;
@ApiPropertyOptional()
isLive?: boolean;
@ApiPropertyOptional()
isPast?: boolean;
@ApiPropertyOptional()
status?: string;
}

View File

@@ -1,11 +1,11 @@
import { describe, it, expect } from 'vitest';
import { AdminDashboardViewDataBuilder } from './AdminDashboardViewDataBuilder';
import type { DashboardStats } from '@/lib/types/admin';
import type { DashboardStatsResponseDTO } from '@/lib/types/generated/DashboardStatsResponseDTO';
describe('AdminDashboardViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform DashboardStats DTO to AdminDashboardViewData correctly', () => {
const dashboardStats: DashboardStats = {
it('should transform DashboardStatsResponseDTO to AdminDashboardViewData correctly', () => {
const dashboardStats: DashboardStatsResponseDTO = {
totalUsers: 1000,
activeUsers: 800,
suspendedUsers: 50,
@@ -31,7 +31,7 @@ describe('AdminDashboardViewDataBuilder', () => {
});
it('should handle zero values correctly', () => {
const dashboardStats: DashboardStats = {
const dashboardStats: DashboardStatsResponseDTO = {
totalUsers: 0,
activeUsers: 0,
suspendedUsers: 0,
@@ -55,100 +55,5 @@ describe('AdminDashboardViewDataBuilder', () => {
},
});
});
it('should handle large numbers correctly', () => {
const dashboardStats: DashboardStats = {
totalUsers: 1000000,
activeUsers: 750000,
suspendedUsers: 25000,
deletedUsers: 225000,
systemAdmins: 50,
recentLogins: 50000,
newUsersToday: 1000,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(1000000);
expect(result.stats.activeUsers).toBe(750000);
expect(result.stats.systemAdmins).toBe(50);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const dashboardStats: DashboardStats = {
totalUsers: 500,
activeUsers: 400,
suspendedUsers: 25,
deletedUsers: 75,
systemAdmins: 3,
recentLogins: 80,
newUsersToday: 10,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(dashboardStats.totalUsers);
expect(result.stats.activeUsers).toBe(dashboardStats.activeUsers);
expect(result.stats.suspendedUsers).toBe(dashboardStats.suspendedUsers);
expect(result.stats.deletedUsers).toBe(dashboardStats.deletedUsers);
expect(result.stats.systemAdmins).toBe(dashboardStats.systemAdmins);
expect(result.stats.recentLogins).toBe(dashboardStats.recentLogins);
expect(result.stats.newUsersToday).toBe(dashboardStats.newUsersToday);
});
it('should not modify the input DTO', () => {
const dashboardStats: DashboardStats = {
totalUsers: 100,
activeUsers: 80,
suspendedUsers: 5,
deletedUsers: 15,
systemAdmins: 2,
recentLogins: 20,
newUsersToday: 5,
};
const originalStats = { ...dashboardStats };
AdminDashboardViewDataBuilder.build(dashboardStats);
expect(dashboardStats).toEqual(originalStats);
});
});
describe('edge cases', () => {
it('should handle negative values (if API returns them)', () => {
const dashboardStats: DashboardStats = {
totalUsers: -1,
activeUsers: -1,
suspendedUsers: -1,
deletedUsers: -1,
systemAdmins: -1,
recentLogins: -1,
newUsersToday: -1,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(-1);
expect(result.stats.activeUsers).toBe(-1);
});
it('should handle very large numbers', () => {
const dashboardStats: DashboardStats = {
totalUsers: Number.MAX_SAFE_INTEGER,
activeUsers: Number.MAX_SAFE_INTEGER - 1000,
suspendedUsers: 100,
deletedUsers: 100,
systemAdmins: 10,
recentLogins: 1000,
newUsersToday: 100,
};
const result = AdminDashboardViewDataBuilder.build(dashboardStats);
expect(result.stats.totalUsers).toBe(Number.MAX_SAFE_INTEGER);
expect(result.stats.activeUsers).toBe(Number.MAX_SAFE_INTEGER - 1000);
});
});
});

View File

@@ -1,22 +1,11 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import type { DashboardStats } from '@/lib/types/admin';
'use client';
import type { DashboardStatsResponseDTO } from '@/lib/types/generated/DashboardStatsResponseDTO';
import type { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
/**
* AdminDashboardViewDataBuilder
*
* Transforms DashboardStats API DTO into AdminDashboardViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class AdminDashboardViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return AdminDashboardViewDataBuilder.build(input);
}
static build(
static build(apiDto: DashboardStats): AdminDashboardViewData {
export class AdminDashboardViewDataBuilder {
public static build(apiDto: DashboardStatsResponseDTO): AdminDashboardViewData {
return {
stats: {
totalUsers: apiDto.totalUsers,
@@ -30,3 +19,5 @@ export class AdminDashboardViewDataBuilder implements ViewDataBuilder<any, any>
};
}
}
AdminDashboardViewDataBuilder satisfies ViewDataBuilder<DashboardStatsResponseDTO, AdminDashboardViewData>;

View File

@@ -1,11 +1,11 @@
import { describe, it, expect } from 'vitest';
import { AdminUsersViewDataBuilder } from './AdminUsersViewDataBuilder';
import type { UserListResponse } from '@/lib/types/admin';
import type { UserListResponseDTO } from '@/lib/types/generated/UserListResponseDTO';
describe('AdminUsersViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform UserListResponse DTO to AdminUsersViewData correctly', () => {
const userListResponse: UserListResponse = {
it('should transform UserListResponseDTO to AdminUsersViewData correctly', () => {
const userListResponse: UserListResponseDTO = {
users: [
{
id: 'user-1',
@@ -53,26 +53,11 @@ describe('AdminUsersViewDataBuilder', () => {
lastLoginAt: '2024-01-20T10:00:00.000Z',
primaryDriverId: 'driver-123',
});
expect(result.users[1]).toEqual({
id: 'user-2',
email: 'user@example.com',
displayName: 'Regular User',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-05T00:00:00.000Z',
updatedAt: '2024-01-10T08:00:00.000Z',
lastLoginAt: '2024-01-18T14:00:00.000Z',
primaryDriverId: 'driver-456',
});
expect(result.total).toBe(2);
expect(result.page).toBe(1);
expect(result.limit).toBe(10);
expect(result.totalPages).toBe(1);
});
it('should calculate derived fields correctly', () => {
const userListResponse: UserListResponse = {
const userListResponse: UserListResponseDTO = {
users: [
{
id: 'user-1',
@@ -104,18 +89,8 @@ describe('AdminUsersViewDataBuilder', () => {
createdAt: '2024-01-03T00:00:00.000Z',
updatedAt: '2024-01-17T12:00:00.000Z',
},
{
id: 'user-4',
email: 'user4@example.com',
displayName: 'User 4',
roles: ['member'],
status: 'deleted',
isSystemAdmin: false,
createdAt: '2024-01-04T00:00:00.000Z',
updatedAt: '2024-01-18T12:00:00.000Z',
},
],
total: 4,
total: 3,
page: 1,
limit: 10,
totalPages: 1,
@@ -123,495 +98,8 @@ describe('AdminUsersViewDataBuilder', () => {
const result = AdminUsersViewDataBuilder.build(userListResponse);
// activeUserCount should count only users with status 'active'
expect(result.activeUserCount).toBe(2);
// adminCount should count only system admins
expect(result.adminCount).toBe(1);
});
it('should handle empty users list', () => {
const userListResponse: UserListResponse = {
users: [],
total: 0,
page: 1,
limit: 10,
totalPages: 0,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users).toHaveLength(0);
expect(result.total).toBe(0);
expect(result.activeUserCount).toBe(0);
expect(result.adminCount).toBe(0);
});
it('should handle users without optional fields', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
// lastLoginAt and primaryDriverId are optional
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].lastLoginAt).toBeUndefined();
expect(result.users[0].primaryDriverId).toBeUndefined();
});
});
describe('date formatting', () => {
it('should handle ISO date strings correctly', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
lastLoginAt: '2024-01-20T10:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].createdAt).toBe('2024-01-01T00:00:00.000Z');
expect(result.users[0].updatedAt).toBe('2024-01-15T12:00:00.000Z');
expect(result.users[0].lastLoginAt).toBe('2024-01-20T10:00:00.000Z');
});
it('should handle Date objects and convert to ISO strings', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: new Date('2024-01-01T00:00:00.000Z'),
updatedAt: new Date('2024-01-15T12:00:00.000Z'),
lastLoginAt: new Date('2024-01-20T10:00:00.000Z'),
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].createdAt).toBe('2024-01-01T00:00:00.000Z');
expect(result.users[0].updatedAt).toBe('2024-01-15T12:00:00.000Z');
expect(result.users[0].lastLoginAt).toBe('2024-01-20T10:00:00.000Z');
});
it('should handle Date objects for lastLoginAt when present', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
lastLoginAt: new Date('2024-01-20T10:00:00.000Z'),
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].lastLoginAt).toBe('2024-01-20T10:00:00.000Z');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['admin', 'owner'],
status: 'active',
isSystemAdmin: true,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
lastLoginAt: '2024-01-20T10:00:00.000Z',
primaryDriverId: 'driver-123',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].id).toBe(userListResponse.users[0].id);
expect(result.users[0].email).toBe(userListResponse.users[0].email);
expect(result.users[0].displayName).toBe(userListResponse.users[0].displayName);
expect(result.users[0].roles).toEqual(userListResponse.users[0].roles);
expect(result.users[0].status).toBe(userListResponse.users[0].status);
expect(result.users[0].isSystemAdmin).toBe(userListResponse.users[0].isSystemAdmin);
expect(result.users[0].createdAt).toBe(userListResponse.users[0].createdAt);
expect(result.users[0].updatedAt).toBe(userListResponse.users[0].updatedAt);
expect(result.users[0].lastLoginAt).toBe(userListResponse.users[0].lastLoginAt);
expect(result.users[0].primaryDriverId).toBe(userListResponse.users[0].primaryDriverId);
expect(result.total).toBe(userListResponse.total);
expect(result.page).toBe(userListResponse.page);
expect(result.limit).toBe(userListResponse.limit);
expect(result.totalPages).toBe(userListResponse.totalPages);
});
it('should not modify the input DTO', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const originalResponse = { ...userListResponse };
AdminUsersViewDataBuilder.build(userListResponse);
expect(userListResponse).toEqual(originalResponse);
});
});
describe('edge cases', () => {
it('should handle users with multiple roles', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['admin', 'owner', 'steward', 'member'],
status: 'active',
isSystemAdmin: true,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].roles).toEqual(['admin', 'owner', 'steward', 'member']);
});
it('should handle users with different statuses', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
{
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['member'],
status: 'suspended',
isSystemAdmin: false,
createdAt: '2024-01-02T00:00:00.000Z',
updatedAt: '2024-01-16T12:00:00.000Z',
},
{
id: 'user-3',
email: 'user3@example.com',
displayName: 'User 3',
roles: ['member'],
status: 'deleted',
isSystemAdmin: false,
createdAt: '2024-01-03T00:00:00.000Z',
updatedAt: '2024-01-17T12:00:00.000Z',
},
],
total: 3,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].status).toBe('active');
expect(result.users[1].status).toBe('suspended');
expect(result.users[2].status).toBe('deleted');
expect(result.activeUserCount).toBe(1);
});
it('should handle pagination metadata correctly', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 100,
page: 5,
limit: 20,
totalPages: 5,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.total).toBe(100);
expect(result.page).toBe(5);
expect(result.limit).toBe(20);
expect(result.totalPages).toBe(5);
});
it('should handle users with empty roles array', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1',
roles: [],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].roles).toEqual([]);
});
it('should handle users with special characters in display name', () => {
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: 'user1@example.com',
displayName: 'User 1 & 2 (Admin)',
roles: ['admin'],
status: 'active',
isSystemAdmin: true,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].displayName).toBe('User 1 & 2 (Admin)');
});
it('should handle users with very long email addresses', () => {
const longEmail = 'verylongemailaddresswithmanycharacters@example.com';
const userListResponse: UserListResponse = {
users: [
{
id: 'user-1',
email: longEmail,
displayName: 'User 1',
roles: ['member'],
status: 'active',
isSystemAdmin: false,
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-15T12:00:00.000Z',
},
],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.users[0].email).toBe(longEmail);
});
});
describe('derived fields calculation', () => {
it('should calculate activeUserCount correctly with mixed statuses', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '3', email: '3@e.com', displayName: '3', roles: ['member'], status: 'suspended', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '4', email: '4@e.com', displayName: '4', roles: ['member'], status: 'deleted', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 4,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.activeUserCount).toBe(2);
});
it('should calculate adminCount correctly with mixed roles', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['admin', 'owner'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '3', email: '3@e.com', displayName: '3', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '4', email: '4@e.com', displayName: '4', roles: ['owner'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 4,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.adminCount).toBe(2);
});
it('should handle all active users', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '3', email: '3@e.com', displayName: '3', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 3,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.activeUserCount).toBe(3);
});
it('should handle no active users', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'suspended', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['member'], status: 'deleted', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 2,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.activeUserCount).toBe(0);
});
it('should handle all system admins', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '3', email: '3@e.com', displayName: '3', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 3,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.adminCount).toBe(3);
});
it('should handle no system admins', () => {
const userListResponse: UserListResponse = {
users: [
{ id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
{ id: '2', email: '2@e.com', displayName: '2', roles: ['owner'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' },
],
total: 2,
page: 1,
limit: 10,
totalPages: 1,
};
const result = AdminUsersViewDataBuilder.build(userListResponse);
expect(result.adminCount).toBe(0);
});
});
});

View File

@@ -1,19 +1,22 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import type { UserListResponse } from '@/lib/types/admin';
import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
'use client';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
import type { UserListResponseDTO } from '@/lib/types/generated/UserListResponseDTO';
import type { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
export class AdminUsersViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return AdminUsersViewDataBuilder.build(input);
}
static build(
public static build(apiDto: UserListResponse): AdminUsersViewData {
export class AdminUsersViewDataBuilder {
public static build(apiDto: UserListResponseDTO): AdminUsersViewData {
const users = apiDto.users.map(u => ({
...u,
joinedAt: new Date(u.joinedAt),
id: u.id,
email: u.email,
displayName: u.displayName,
roles: u.roles,
status: u.status,
isSystemAdmin: u.isSystemAdmin,
createdAt: u.createdAt,
updatedAt: u.updatedAt,
lastLoginAt: u.lastLoginAt,
primaryDriverId: u.primaryDriverId,
}));
return {
@@ -22,9 +25,10 @@ export class AdminUsersViewDataBuilder implements ViewDataBuilder<any, any> {
page: apiDto.page,
limit: apiDto.limit,
totalPages: apiDto.totalPages,
// Pre-computed derived values for template
activeUserCount: users.filter(u => u.status === 'active').length,
adminCount: users.filter(u => u.isSystemAdmin).length,
};
}
}
AdminUsersViewDataBuilder satisfies ViewDataBuilder<UserListResponseDTO, AdminUsersViewData>;

View File

@@ -1,17 +1,17 @@
import { describe, it, expect } from 'vitest';
import { AnalyticsDashboardViewDataBuilder } from './AnalyticsDashboardViewDataBuilder';
import { AnalyticsDashboardInputViewData } from '@/lib/view-data/AnalyticsDashboardInputViewData';
import type { GetDashboardDataOutputDTO } from '@/lib/types/generated/GetDashboardDataOutputDTO';
describe('AnalyticsDashboardViewDataBuilder', () => {
it('builds ViewData from AnalyticsDashboardInputViewData', () => {
const inputViewData: AnalyticsDashboardInputViewData = {
it('builds ViewData from GetDashboardDataOutputDTO', () => {
const inputDto: GetDashboardDataOutputDTO = {
totalUsers: 100,
activeUsers: 40,
totalRaces: 10,
totalLeagues: 5,
};
const viewData = AnalyticsDashboardViewDataBuilder.build(inputViewData);
const viewData = AnalyticsDashboardViewDataBuilder.build(inputDto);
expect(viewData.metrics.totalUsers).toBe(100);
expect(viewData.metrics.activeUsers).toBe(40);
@@ -23,28 +23,28 @@ describe('AnalyticsDashboardViewDataBuilder', () => {
});
it('computes engagement rate and formatted engagement rate', () => {
const inputViewData: AnalyticsDashboardInputViewData = {
const inputDto: GetDashboardDataOutputDTO = {
totalUsers: 200,
activeUsers: 50,
totalRaces: 0,
totalLeagues: 0,
};
const viewData = AnalyticsDashboardViewDataBuilder.build(inputViewData);
const viewData = AnalyticsDashboardViewDataBuilder.build(inputDto);
expect(viewData.metrics.userEngagementRate).toBeCloseTo(25);
expect(viewData.metrics.formattedEngagementRate).toBe('25.0%');
});
it('handles zero users safely', () => {
const inputViewData: AnalyticsDashboardInputViewData = {
const inputDto: GetDashboardDataOutputDTO = {
totalUsers: 0,
activeUsers: 0,
totalRaces: 0,
totalLeagues: 0,
};
const viewData = AnalyticsDashboardViewDataBuilder.build(inputViewData);
const viewData = AnalyticsDashboardViewDataBuilder.build(inputDto);
expect(viewData.metrics.userEngagementRate).toBe(0);
expect(viewData.metrics.formattedEngagementRate).toBe('0.0%');

View File

@@ -1,30 +1,26 @@
import { AnalyticsDashboardInputViewData } from '@/lib/view-data/AnalyticsDashboardInputViewData';
import { AnalyticsDashboardViewData } from '@/lib/view-data/AnalyticsDashboardViewData';
'use client';
/**
* AnalyticsDashboardViewDataBuilder
*
* Transforms AnalyticsDashboardInputViewData into AnalyticsDashboardViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
import type { GetDashboardDataOutputDTO } from '@/lib/types/generated/GetDashboardDataOutputDTO';
import type { AnalyticsDashboardViewData } from '@/lib/view-data/AnalyticsDashboardViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
export class AnalyticsDashboardViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return AnalyticsDashboardViewDataBuilder.build(input);
}
export class AnalyticsDashboardViewDataBuilder {
public static build(apiDto: GetDashboardDataOutputDTO): AnalyticsDashboardViewData {
const totalUsers = apiDto.totalUsers;
const activeUsers = apiDto.activeUsers;
const totalRaces = apiDto.totalRaces;
const totalLeagues = apiDto.totalLeagues;
static build(viewData: AnalyticsDashboardInputViewData): AnalyticsDashboardViewData {
const userEngagementRate = viewData.totalUsers > 0 ? (viewData.activeUsers / viewData.totalUsers) * 100 : 0;
const userEngagementRate = totalUsers > 0 ? (activeUsers / totalUsers) * 100 : 0;
const formattedEngagementRate = `${userEngagementRate.toFixed(1)}%`;
const activityLevel = userEngagementRate > 70 ? 'High' : userEngagementRate > 40 ? 'Medium' : 'Low';
return {
metrics: {
totalUsers: viewData.totalUsers,
activeUsers: viewData.activeUsers,
totalRaces: viewData.totalRaces,
totalLeagues: viewData.totalLeagues,
totalUsers,
activeUsers,
totalRaces,
totalLeagues,
userEngagementRate,
formattedEngagementRate,
activityLevel,
@@ -32,3 +28,5 @@ export class AnalyticsDashboardViewDataBuilder implements ViewDataBuilder<any, a
};
}
}
AnalyticsDashboardViewDataBuilder satisfies ViewDataBuilder<GetDashboardDataOutputDTO, AnalyticsDashboardViewData>;

View File

@@ -1,15 +1,18 @@
import { describe, it, expect } from 'vitest';
import { AvatarViewDataBuilder } from './AvatarViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
describe('AvatarViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform MediaBinaryDTO to AvatarViewData correctly', () => {
it('should transform binary data to AvatarViewData correctly', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
const mediaDto = {
id: '1',
url: 'http://example.com/image.png',
type: 'image/png',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer,
contentType: 'image/png',
};
} as unknown as GetMediaOutputDTO;
const result = AvatarViewDataBuilder.build(mediaDto);
@@ -19,173 +22,36 @@ describe('AvatarViewDataBuilder', () => {
it('should handle JPEG images', () => {
const buffer = new Uint8Array([0xff, 0xd8, 0xff, 0xe0]);
const mediaDto: MediaBinaryDTO = {
const mediaDto = {
id: '2',
url: 'http://example.com/image.jpg',
type: 'image/jpeg',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer,
contentType: 'image/jpeg',
};
} as unknown as GetMediaOutputDTO;
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/jpeg');
});
it('should handle GIF images', () => {
const buffer = new Uint8Array([0x47, 0x49, 0x46, 0x38]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/gif',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/gif');
});
it('should handle SVG images', () => {
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"></svg>');
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/svg+xml',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/svg+xml');
});
it('should handle WebP images', () => {
const buffer = new Uint8Array([0x52, 0x49, 0x46, 0x46]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/webp',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/webp');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBeDefined();
expect(result.contentType).toBe(mediaDto.contentType);
});
it('should not modify the input DTO', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const originalDto = { ...mediaDto };
AvatarViewDataBuilder.build(mediaDto);
expect(mediaDto).toEqual(originalDto);
});
it('should convert buffer to base64 string', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(typeof result.buffer).toBe('string');
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
});
});
describe('edge cases', () => {
it('should handle empty buffer', () => {
const buffer = new Uint8Array([]);
const mediaDto: MediaBinaryDTO = {
const mediaDto = {
id: '3',
url: 'http://example.com/image.png',
type: 'image/png',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer,
contentType: 'image/png',
};
} as unknown as GetMediaOutputDTO;
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe('');
expect(result.contentType).toBe('image/png');
});
it('should handle large buffer', () => {
const buffer = new Uint8Array(1024 * 1024); // 1MB
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with all zeros', () => {
const buffer = new Uint8Array([0x00, 0x00, 0x00, 0x00]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with all ones', () => {
const buffer = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
it('should handle different content types', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const contentTypes = [
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'image/svg+xml',
'image/bmp',
'image/tiff',
];
contentTypes.forEach((contentType) => {
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType,
};
const result = AvatarViewDataBuilder.build(mediaDto);
expect(result.contentType).toBe(contentType);
});
});
});
});

View File

@@ -1,25 +1,23 @@
/**
* AvatarViewDataBuilder
*
* Transforms MediaBinaryDTO into AvatarViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
'use client';
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { AvatarViewData } from '@/lib/view-data/AvatarViewData';
import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
import type { AvatarViewData } from '@/lib/view-data/AvatarViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class AvatarViewDataBuilder {
public static build(apiDto: GetMediaOutputDTO): AvatarViewData {
// Note: GetMediaOutputDTO from OpenAPI doesn't have buffer,
// but the implementation expects it for binary data.
// We use type assertion to handle the binary case while keeping the DTO type.
const binaryDto = apiDto as unknown as { buffer?: ArrayBuffer };
const buffer = binaryDto.buffer;
const contentType = apiDto.type;
export class AvatarViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return AvatarViewDataBuilder.build(input);
}
static build(
static build(apiDto: MediaBinaryDTO): AvatarViewData {
return {
buffer: Buffer.from(apiDto.buffer).toString('base64'),
contentType: apiDto.contentType,
buffer: buffer ? Buffer.from(buffer).toString('base64') : '',
contentType,
};
}
}
AvatarViewDataBuilder satisfies ViewDataBuilder<GetMediaOutputDTO, AvatarViewData>;

View File

@@ -1,15 +1,18 @@
import { describe, it, expect } from 'vitest';
import { CategoryIconViewDataBuilder } from './CategoryIconViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
describe('CategoryIconViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform MediaBinaryDTO to CategoryIconViewData correctly', () => {
it('should transform binary data to CategoryIconViewData correctly', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
const mediaDto = {
id: '1',
url: 'http://example.com/icon.png',
type: 'image/png',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer,
contentType: 'image/png',
};
} as unknown as GetMediaOutputDTO;
const result = CategoryIconViewDataBuilder.build(mediaDto);
@@ -19,97 +22,36 @@ describe('CategoryIconViewDataBuilder', () => {
it('should handle SVG icons', () => {
const buffer = new TextEncoder().encode('<svg xmlns="http://www.w3.org/2000/svg"><circle cx="10" cy="10" r="5"/></svg>');
const mediaDto: MediaBinaryDTO = {
const mediaDto = {
id: '2',
url: 'http://example.com/icon.svg',
type: 'image/svg+xml',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer,
contentType: 'image/svg+xml',
};
} as unknown as GetMediaOutputDTO;
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/svg+xml');
});
it('should handle small icon files', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBeDefined();
expect(result.contentType).toBe(mediaDto.contentType);
});
it('should not modify the input DTO', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const originalDto = { ...mediaDto };
CategoryIconViewDataBuilder.build(mediaDto);
expect(mediaDto).toEqual(originalDto);
});
it('should convert buffer to base64 string', () => {
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(typeof result.buffer).toBe('string');
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
});
});
describe('edge cases', () => {
it('should handle empty buffer', () => {
const buffer = new Uint8Array([]);
const mediaDto: MediaBinaryDTO = {
const mediaDto = {
id: '3',
url: 'http://example.com/icon.png',
type: 'image/png',
uploadedAt: new Date().toISOString(),
buffer: buffer.buffer,
contentType: 'image/png',
};
} as unknown as GetMediaOutputDTO;
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe('');
expect(result.contentType).toBe('image/png');
});
it('should handle buffer with special characters', () => {
const buffer = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe, 0xfd, 0xfc]);
const mediaDto: MediaBinaryDTO = {
buffer: buffer.buffer,
contentType: 'image/png',
};
const result = CategoryIconViewDataBuilder.build(mediaDto);
expect(result.buffer).toBe(Buffer.from(buffer).toString('base64'));
expect(result.contentType).toBe('image/png');
});
});
});

View File

@@ -1,25 +1,21 @@
/**
* CategoryIconViewDataBuilder
*
* Transforms MediaBinaryDTO into CategoryIconViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
'use client';
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { CategoryIconViewData } from '@/lib/view-data/CategoryIconViewData';
import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
import type { CategoryIconViewData } from '@/lib/view-data/CategoryIconViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class CategoryIconViewDataBuilder {
public static build(apiDto: GetMediaOutputDTO): CategoryIconViewData {
// Note: GetMediaOutputDTO from OpenAPI doesn't have buffer,
// but the implementation expects it for binary data.
const binaryDto = apiDto as unknown as { buffer?: ArrayBuffer };
const buffer = binaryDto.buffer;
export class CategoryIconViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return CategoryIconViewDataBuilder.build(input);
}
static build(
static build(apiDto: MediaBinaryDTO): CategoryIconViewData {
return {
buffer: Buffer.from(apiDto.buffer).toString('base64'),
contentType: apiDto.contentType,
buffer: buffer ? Buffer.from(buffer).toString('base64') : '',
contentType: apiDto.type,
};
}
}
CategoryIconViewDataBuilder satisfies ViewDataBuilder<GetMediaOutputDTO, CategoryIconViewData>;

View File

@@ -1,28 +1,11 @@
/**
* CompleteOnboarding ViewData Builder
*
* Transforms onboarding completion result into ViewData for templates.
*/
'use client';
import { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
import { CompleteOnboardingViewData } from './CompleteOnboardingViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import type { CompleteOnboardingOutputDTO } from '@/lib/types/generated/CompleteOnboardingOutputDTO';
import type { CompleteOnboardingViewData } from '@/lib/view-data/CompleteOnboardingViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class CompleteOnboardingViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return CompleteOnboardingViewDataBuilder.build(input);
}
static build(
/**
* Transform DTO into ViewData
*
* @param apiDto - The API DTO to transform
* @returns ViewData for templates
*/
static build(apiDto: CompleteOnboardingOutputDTO): CompleteOnboardingViewData {
export class CompleteOnboardingViewDataBuilder {
public static build(apiDto: CompleteOnboardingOutputDTO): CompleteOnboardingViewData {
return {
success: apiDto.success,
driverId: apiDto.driverId,
@@ -30,3 +13,5 @@ export class CompleteOnboardingViewDataBuilder implements ViewDataBuilder<any, a
};
}
}
CompleteOnboardingViewDataBuilder satisfies ViewDataBuilder<CompleteOnboardingOutputDTO, CompleteOnboardingViewData>;

View File

@@ -1,3 +1,5 @@
'use client';
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
import { DashboardDateDisplay } from '@/lib/display-objects/DashboardDateDisplay';
@@ -6,24 +8,10 @@ import { DashboardRankDisplay } from '@/lib/display-objects/DashboardRankDisplay
import { DashboardConsistencyDisplay } from '@/lib/display-objects/DashboardConsistencyDisplay';
import { DashboardCountDisplay } from '@/lib/display-objects/DashboardCountDisplay';
import { DashboardLeaguePositionDisplay } from '@/lib/display-objects/DashboardLeaguePositionDisplay';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import { number } from 'zod';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
/**
* DashboardViewDataBuilder
*
* Transforms DashboardOverviewDTO (API DTO) into DashboardViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class DashboardViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return DashboardViewDataBuilder.build(input);
}
static build(
static build(apiDto: DashboardOverviewDTO): DashboardViewData {
export class DashboardViewDataBuilder {
public static build(apiDto: DashboardOverviewDTO): DashboardViewData {
return {
currentDriver: {
name: apiDto.currentDriver?.name || '',
@@ -49,11 +37,11 @@ export class DashboardViewDataBuilder implements ViewDataBuilder<any, any> {
id: item.id,
type: item.type,
headline: item.headline,
body: item.body,
body: item.body ?? undefined,
timestamp: item.timestamp,
formattedTime: DashboardDateDisplay.format(new Date(item.timestamp)).relative,
ctaHref: item.ctaHref,
ctaLabel: item.ctaLabel,
ctaHref: item.ctaHref ?? undefined,
ctaLabel: item.ctaLabel ?? undefined,
})),
friends: apiDto.friends.map((friend) => ({
id: friend.id,
@@ -98,3 +86,5 @@ export class DashboardViewDataBuilder implements ViewDataBuilder<any, any> {
};
}
}
DashboardViewDataBuilder satisfies ViewDataBuilder<DashboardOverviewDTO, DashboardViewData>;

View File

@@ -1,6 +0,0 @@
import { ViewData } from "@/lib/contracts/view-data/ViewData";
export interface DeleteMediaViewData extends ViewData {
success: boolean;
error?: string;
}

View File

@@ -1,30 +1,16 @@
/**
* DeleteMedia ViewData Builder
*
* Transforms media deletion result into ViewData for templates.
*/
'use client';
import { DeleteMediaOutputDTO } from '@/lib/types/generated/DeleteMediaOutputDTO';
import { DeleteMediaViewData } from './DeleteMediaViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import type { DeleteMediaOutputDTO } from '@/lib/types/generated/DeleteMediaOutputDTO';
import type { DeleteMediaViewData } from '@/lib/view-data/DeleteMediaViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class DeleteMediaViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return DeleteMediaViewDataBuilder.build(input);
}
/**
* Transform DTO into ViewData
*
* @param apiDto - The API DTO to transform
* @returns ViewData for templates
*/
static build(apiDto: DeleteMediaOutputDTO): DeleteMediaViewData {
export class DeleteMediaViewDataBuilder {
public static build(apiDto: DeleteMediaOutputDTO): DeleteMediaViewData {
return {
success: apiDto.success,
error: apiDto.error,
};
}
}
DeleteMediaViewDataBuilder satisfies ViewDataBuilder<DeleteMediaOutputDTO, DeleteMediaViewData>;

View File

@@ -1,3 +1,5 @@
'use client';
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
import type { DriverProfileViewData } from '@/lib/view-data/DriverProfileViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
@@ -5,22 +7,10 @@ import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
import { FinishDisplay } from '@/lib/display-objects/FinishDisplay';
import { PercentDisplay } from '@/lib/display-objects/PercentDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
/**
* DriverProfileViewDataBuilder
*
* Transforms GetDriverProfileOutputDTO into ViewData for the driver profile page.
* Deterministic, side-effect free, no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class DriverProfileViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return DriverProfileViewDataBuilder.build(input);
}
static build(
static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewData {
export class DriverProfileViewDataBuilder {
public static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewData {
return {
currentDriver: apiDto.currentDriver ? {
id: apiDto.currentDriver.id,
@@ -116,3 +106,5 @@ export class DriverProfileViewDataBuilder implements ViewDataBuilder<any, any> {
};
}
}
DriverProfileViewDataBuilder satisfies ViewDataBuilder<GetDriverProfileOutputDTO, DriverProfileViewData>;

View File

@@ -1,16 +1,13 @@
'use client';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import type { DriverRankingsViewData } from '@/lib/view-data/DriverRankingsViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import { WinRateDisplay } from '@/lib/display-objects/WinRateDisplay';
import { MedalDisplay } from '@/lib/display-objects/MedalDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class DriverRankingsViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return DriverRankingsViewDataBuilder.build(input);
}
static build(
static build(apiDto: DriverLeaderboardItemDTO[]): DriverRankingsViewData {
export class DriverRankingsViewDataBuilder {
public static build(apiDto: DriverLeaderboardItemDTO[]): DriverRankingsViewData {
if (!apiDto || apiDto.length === 0) {
return {
drivers: [],
@@ -58,3 +55,5 @@ export class DriverRankingsViewDataBuilder implements ViewDataBuilder<any, any>
};
}
}
DriverRankingsViewDataBuilder satisfies ViewDataBuilder<DriverLeaderboardItemDTO[], DriverRankingsViewData>;

View File

@@ -1,40 +1,38 @@
'use client';
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
import type { DriversViewData } from '@/lib/view-data/DriversViewData';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class DriversViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return DriversViewDataBuilder.build(input);
}
static build(
static build(dto: DriversLeaderboardDTO): DriversViewData {
export class DriversViewDataBuilder {
public static build(apiDto: DriversLeaderboardDTO): DriversViewData {
return {
drivers: dto.drivers.map(driver => ({
drivers: apiDto.drivers.map(driver => ({
id: driver.id,
name: driver.name,
rating: driver.rating,
ratingLabel: RatingDisplay.format(driver.rating),
skillLevel: driver.skillLevel,
category: driver.category,
category: driver.category ?? undefined,
nationality: driver.nationality,
racesCompleted: driver.racesCompleted,
wins: driver.wins,
podiums: driver.podiums,
isActive: driver.isActive,
rank: driver.rank,
avatarUrl: driver.avatarUrl,
avatarUrl: driver.avatarUrl ?? undefined,
})),
totalRaces: dto.totalRaces,
totalRacesLabel: NumberDisplay.format(dto.totalRaces),
totalWins: dto.totalWins,
totalWinsLabel: NumberDisplay.format(dto.totalWins),
activeCount: dto.activeCount,
activeCountLabel: NumberDisplay.format(dto.activeCount),
totalDriversLabel: NumberDisplay.format(dto.drivers.length),
totalRaces: apiDto.totalRaces,
totalRacesLabel: NumberDisplay.format(apiDto.totalRaces),
totalWins: apiDto.totalWins,
totalWinsLabel: NumberDisplay.format(apiDto.totalWins),
activeCount: apiDto.activeCount,
activeCountLabel: NumberDisplay.format(apiDto.activeCount),
totalDriversLabel: NumberDisplay.format(apiDto.drivers.length),
};
}
}
DriversViewDataBuilder satisfies ViewDataBuilder<DriversLeaderboardDTO, DriversViewData>;

View File

@@ -1,24 +1,11 @@
/**
* Forgot Password View Data Builder
*
* Transforms ForgotPasswordPageDTO into ViewData for the forgot password template.
* Deterministic, side-effect free, no business logic.
*/
'use client';
import { ForgotPasswordPageDTO } from '@/lib/services/auth/types/ForgotPasswordPageDTO';
import { ForgotPasswordViewData } from '../../view-data/ForgotPasswordViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import { error } from 'console';
import type { ForgotPasswordPageDTO } from '@/lib/types/generated/ForgotPasswordPageDTO';
import type { ForgotPasswordViewData } from '@/lib/view-data/ForgotPasswordViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class ForgotPasswordViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return ForgotPasswordViewDataBuilder.build(input);
}
static build(
static build(apiDto: ForgotPasswordPageDTO): ForgotPasswordViewData {
export class ForgotPasswordViewDataBuilder {
public static build(apiDto: ForgotPasswordPageDTO): ForgotPasswordViewData {
return {
returnTo: apiDto.returnTo,
showSuccess: false,
@@ -36,3 +23,5 @@ export class ForgotPasswordViewDataBuilder implements ViewDataBuilder<any, any>
};
}
}
ForgotPasswordViewDataBuilder satisfies ViewDataBuilder<ForgotPasswordPageDTO, ForgotPasswordViewData>;

View File

@@ -1,29 +1,11 @@
/**
* GenerateAvatars ViewData Builder
*
* Transforms avatar generation result into ViewData for templates.
* Must be used in mutations to avoid returning DTOs directly.
*/
'use client';
import { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';
import { GenerateAvatarsViewData } from './GenerateAvatarsViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import type { RequestAvatarGenerationOutputDTO } from '@/lib/types/generated/RequestAvatarGenerationOutputDTO';
import type { GenerateAvatarsViewData } from '@/lib/view-data/GenerateAvatarsViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class GenerateAvatarsViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return GenerateAvatarsViewDataBuilder.build(input);
}
static build(
/**
* Transform DTO into ViewData
*
* @param apiDto - The API DTO to transform
* @returns ViewData for templates
*/
static build(apiDto: RequestAvatarGenerationOutputDTO): GenerateAvatarsViewData {
export class GenerateAvatarsViewDataBuilder {
public static build(apiDto: RequestAvatarGenerationOutputDTO): GenerateAvatarsViewData {
return {
success: apiDto.success,
avatarUrls: apiDto.avatarUrls || [],
@@ -31,3 +13,5 @@ export class GenerateAvatarsViewDataBuilder implements ViewDataBuilder<any, any>
};
}
}
GenerateAvatarsViewDataBuilder satisfies ViewDataBuilder<RequestAvatarGenerationOutputDTO, GenerateAvatarsViewData>;

View File

@@ -1,80 +1,44 @@
/**
* Health View Data Builder
*
* Transforms health DTO data into UI-ready view models.
* This layer isolates the UI from API churn by providing a stable interface
* between the API layer and the presentation layer.
*/
'use client';
import type { HealthDTO } from '@/lib/types/generated/HealthDTO';
import type { HealthViewData, HealthStatus, HealthMetrics, HealthComponent, HealthAlert } from '@/lib/view-data/HealthViewData';
import { HealthStatusDisplay } from '@/lib/display-objects/HealthStatusDisplay';
import { HealthMetricDisplay } from '@/lib/display-objects/HealthMetricDisplay';
import { HealthComponentDisplay } from '@/lib/display-objects/HealthComponentDisplay';
import { HealthAlertDisplay } from '@/lib/display-objects/HealthAlertDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
export interface HealthDTO {
status: 'ok' | 'degraded' | 'error' | 'unknown';
timestamp: string;
uptime?: number;
responseTime?: number;
errorRate?: number;
lastCheck?: string;
checksPassed?: number;
checksFailed?: number;
components?: Array<{
name: string;
status: 'ok' | 'degraded' | 'error' | 'unknown';
lastCheck?: string;
responseTime?: number;
errorRate?: number;
}>;
alerts?: Array<{
id: string;
type: 'critical' | 'warning' | 'info';
title: string;
message: string;
timestamp: string;
}>;
}
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class HealthViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return HealthViewDataBuilder.build(input);
}
static build(
static build(dto: HealthDTO): HealthViewData {
export class HealthViewDataBuilder {
public static build(apiDto: HealthDTO): HealthViewData {
const now = new Date();
const lastUpdated = dto.timestamp || now.toISOString();
const lastUpdated = apiDto.timestamp || now.toISOString();
// Build overall status
const overallStatus: HealthStatus = {
status: dto.status,
timestamp: dto.timestamp,
formattedTimestamp: HealthStatusDisplay.formatTimestamp(dto.timestamp),
relativeTime: HealthStatusDisplay.formatRelativeTime(dto.timestamp),
statusLabel: HealthStatusDisplay.formatStatusLabel(dto.status),
statusColor: HealthStatusDisplay.formatStatusColor(dto.status),
statusIcon: HealthStatusDisplay.formatStatusIcon(dto.status),
status: apiDto.status,
timestamp: apiDto.timestamp,
formattedTimestamp: HealthStatusDisplay.formatTimestamp(apiDto.timestamp),
relativeTime: HealthStatusDisplay.formatRelativeTime(apiDto.timestamp),
statusLabel: HealthStatusDisplay.formatStatusLabel(apiDto.status),
statusColor: HealthStatusDisplay.formatStatusColor(apiDto.status),
statusIcon: HealthStatusDisplay.formatStatusIcon(apiDto.status),
};
// Build metrics
const metrics: HealthMetrics = {
uptime: HealthMetricDisplay.formatUptime(dto.uptime),
responseTime: HealthMetricDisplay.formatResponseTime(dto.responseTime),
errorRate: HealthMetricDisplay.formatErrorRate(dto.errorRate),
lastCheck: dto.lastCheck || lastUpdated,
formattedLastCheck: HealthMetricDisplay.formatTimestamp(dto.lastCheck || lastUpdated),
checksPassed: dto.checksPassed || 0,
checksFailed: dto.checksFailed || 0,
totalChecks: (dto.checksPassed || 0) + (dto.checksFailed || 0),
successRate: HealthMetricDisplay.formatSuccessRate(dto.checksPassed, dto.checksFailed),
uptime: HealthMetricDisplay.formatUptime(apiDto.uptime),
responseTime: HealthMetricDisplay.formatResponseTime(apiDto.responseTime),
errorRate: HealthMetricDisplay.formatErrorRate(apiDto.errorRate),
lastCheck: apiDto.lastCheck || lastUpdated,
formattedLastCheck: HealthMetricDisplay.formatTimestamp(apiDto.lastCheck || lastUpdated),
checksPassed: apiDto.checksPassed || 0,
checksFailed: apiDto.checksFailed || 0,
totalChecks: (apiDto.checksPassed || 0) + (apiDto.checksFailed || 0),
successRate: HealthMetricDisplay.formatSuccessRate(apiDto.checksPassed, apiDto.checksFailed),
};
// Build components
const components: HealthComponent[] = (dto.components || []).map((component) => ({
const components: HealthComponent[] = (apiDto.components || []).map((component) => ({
name: component.name,
status: component.status,
statusLabel: HealthComponentDisplay.formatStatusLabel(component.status),
@@ -87,7 +51,7 @@ export class HealthViewDataBuilder implements ViewDataBuilder<any, any> {
}));
// Build alerts
const alerts: HealthAlert[] = (dto.alerts || []).map((alert) => ({
const alerts: HealthAlert[] = (apiDto.alerts || []).map((alert) => ({
id: alert.id,
type: alert.type,
title: alert.title,
@@ -117,3 +81,5 @@ export class HealthViewDataBuilder implements ViewDataBuilder<any, any> {
};
}
}
HealthViewDataBuilder satisfies ViewDataBuilder<HealthDTO, HealthViewData>;

View File

@@ -1,34 +1,33 @@
import { describe, it, expect } from 'vitest';
import { HomeViewDataBuilder } from './HomeViewDataBuilder';
import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO';
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
describe('HomeViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform HomeDataDTO to HomeViewData correctly', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
it('should transform DashboardOverviewDTO to HomeViewData correctly', () => {
const homeDataDto: DashboardOverviewDTO = {
currentDriver: null,
upcomingRaces: [
{
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2024-01-01T10:00:00Z',
isMyLeague: false,
},
],
topLeagues: [
leagueStandingsSummaries: [
{
id: 'league-1',
name: 'Test League',
description: 'Test Description',
},
],
teams: [
{
id: 'team-1',
name: 'Test Team',
tag: 'TT',
leagueId: 'league-1',
leagueName: 'Test League',
position: 1,
points: 100,
totalDrivers: 20,
},
],
feedSummary: { items: [] },
friends: [],
activeLeaguesCount: 1,
};
const result = HomeViewDataBuilder.build(homeDataDto);
@@ -38,130 +37,58 @@ describe('HomeViewDataBuilder', () => {
upcomingRaces: [
{
id: 'race-1',
name: 'Test Race',
scheduledAt: '2024-01-01T10:00:00Z',
track: 'Test Track',
car: 'Test Car',
formattedDate: 'Mon, Jan 1, 2024',
},
],
topLeagues: [
{
id: 'league-1',
name: 'Test League',
description: 'Test Description',
},
],
teams: [
{
id: 'team-1',
name: 'Test Team',
tag: 'TT',
description: '',
},
],
teams: [],
});
});
it('should handle empty arrays correctly', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: false,
const homeDataDto: DashboardOverviewDTO = {
currentDriver: null,
upcomingRaces: [],
topLeagues: [],
teams: [],
leagueStandingsSummaries: [],
feedSummary: { items: [] },
friends: [],
activeLeaguesCount: 0,
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result).toEqual({
isAlpha: false,
isAlpha: true,
upcomingRaces: [],
topLeagues: [],
teams: [],
});
});
it('should handle multiple items in arrays', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [
{ id: 'race-1', name: 'Race 1', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track 1' },
{ id: 'race-2', name: 'Race 2', scheduledAt: '2024-01-02T10:00:00Z', track: 'Track 2' },
],
topLeagues: [
{ id: 'league-1', name: 'League 1', description: 'Description 1' },
{ id: 'league-2', name: 'League 2', description: 'Description 2' },
],
teams: [
{ id: 'team-1', name: 'Team 1', tag: 'T1' },
{ id: 'team-2', name: 'Team 2', tag: 'T2' },
],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.upcomingRaces).toHaveLength(2);
expect(result.topLeagues).toHaveLength(2);
expect(result.teams).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.isAlpha).toBe(homeDataDto.isAlpha);
expect(result.upcomingRaces).toEqual(homeDataDto.upcomingRaces);
expect(result.topLeagues).toEqual(homeDataDto.topLeagues);
expect(result.teams).toEqual(homeDataDto.teams);
});
it('should not modify the input DTO', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }],
const homeDataDto: DashboardOverviewDTO = {
currentDriver: null,
upcomingRaces: [{ id: 'race-1', track: 'Track', car: 'Car', scheduledAt: '2024-01-01T10:00:00Z', isMyLeague: false }],
leagueStandingsSummaries: [{ leagueId: 'league-1', leagueName: 'League', position: 1, points: 10, totalDrivers: 10 }],
feedSummary: { items: [] },
friends: [],
activeLeaguesCount: 1,
};
const originalDto = { ...homeDataDto };
const originalDto = JSON.parse(JSON.stringify(homeDataDto));
HomeViewDataBuilder.build(homeDataDto);
expect(homeDataDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle false isAlpha value', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: false,
upcomingRaces: [],
topLeagues: [],
teams: [],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.isAlpha).toBe(false);
});
it('should handle null/undefined values in arrays', () => {
const homeDataDto: HomeDataDTO = {
isAlpha: true,
upcomingRaces: [{ id: 'race-1', name: 'Race', scheduledAt: '2024-01-01T10:00:00Z', track: 'Track' }],
topLeagues: [{ id: 'league-1', name: 'League', description: 'Description' }],
teams: [{ id: 'team-1', name: 'Team', tag: 'T' }],
};
const result = HomeViewDataBuilder.build(homeDataDto);
expect(result.upcomingRaces[0].id).toBe('race-1');
expect(result.topLeagues[0].id).toBe('league-1');
expect(result.teams[0].id).toBe('team-1');
});
});
});

View File

@@ -1,32 +1,34 @@
import type { HomeViewData } from '@/templates/HomeTemplate';
import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
'use client';
/**
* HomeViewDataBuilder
*
* Transforms HomeDataDTO to HomeViewData.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
import type { HomeViewData } from '@/lib/view-data/HomeViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { DashboardDateDisplay } from '@/lib/display-objects/DashboardDateDisplay';
export class HomeViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return HomeViewDataBuilder.build(input);
}
static build(
export class HomeViewDataBuilder {
/**
* Build HomeViewData from HomeDataDTO
* Build HomeViewData from DashboardOverviewDTO
*
* @param apiDto - The API DTO
* @returns HomeViewData
*/
static build(apiDto: HomeDataDTO): HomeViewData {
public static build(apiDto: DashboardOverviewDTO): HomeViewData {
return {
isAlpha: apiDto.isAlpha,
upcomingRaces: apiDto.upcomingRaces,
topLeagues: apiDto.topLeagues,
teams: apiDto.teams,
isAlpha: true,
upcomingRaces: (apiDto.upcomingRaces || []).map(race => ({
id: race.id,
track: race.track,
car: race.car,
formattedDate: DashboardDateDisplay.format(new Date(race.scheduledAt)).date,
})),
topLeagues: (apiDto.leagueStandingsSummaries || []).map(league => ({
id: league.leagueId,
name: league.leagueName,
description: '',
})),
teams: [],
};
}
}
HomeViewDataBuilder satisfies ViewDataBuilder<DashboardOverviewDTO, HomeViewData>;

View File

@@ -1,19 +1,17 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
'use client';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO';
import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
type LeaderboardsInputDTO = {
drivers: { drivers: DriverLeaderboardItemDTO[] };
teams: GetTeamsLeaderboardOutputDTO;
}
export class LeaderboardsViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return LeaderboardsViewDataBuilder.build(input);
}
static build(
static build(
apiDto: { drivers: { drivers: DriverLeaderboardItemDTO[] }; teams: GetTeamsLeaderboardOutputDTO }
): LeaderboardsViewData {
export class LeaderboardsViewDataBuilder {
public static build(apiDto: LeaderboardsInputDTO): LeaderboardsViewData {
return {
drivers: apiDto.drivers.drivers.map(driver => ({
id: driver.id,
@@ -45,3 +43,5 @@ export class LeaderboardsViewDataBuilder implements ViewDataBuilder<any, any> {
};
}
}
LeaderboardsViewDataBuilder satisfies ViewDataBuilder<LeaderboardsInputDTO, LeaderboardsViewData>;

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { LeagueCoverViewDataBuilder } from './LeagueCoverViewDataBuilder';
import type { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import type { MediaBinaryDTO } from '@/lib/types/generated/MediaBinaryDTO';
describe('LeagueCoverViewDataBuilder', () => {
describe('happy paths', () => {

View File

@@ -1,25 +1,16 @@
/**
* LeagueCoverViewDataBuilder
*
* Transforms MediaBinaryDTO into LeagueCoverViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
'use client';
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { LeagueCoverViewData } from '@/lib/view-data/LeagueCoverViewData';
import type { MediaBinaryDTO } from '@/lib/types/generated/MediaBinaryDTO';
import type { LeagueCoverViewData } from '@/lib/view-data/LeagueCoverViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LeagueCoverViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return LeagueCoverViewDataBuilder.build(input);
}
static build(
static build(apiDto: MediaBinaryDTO): LeagueCoverViewData {
export class LeagueCoverViewDataBuilder {
public static build(apiDto: MediaBinaryDTO): LeagueCoverViewData {
return {
buffer: Buffer.from(apiDto.buffer).toString('base64'),
contentType: apiDto.contentType,
};
}
}
LeagueCoverViewDataBuilder satisfies ViewDataBuilder<MediaBinaryDTO, LeagueCoverViewData>;

View File

@@ -1,33 +1,32 @@
'use client';
import type { LeagueWithCapacityAndScoringDTO } from '@/lib/types/generated/LeagueWithCapacityAndScoringDTO';
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
import type { GetDriverOutputDTO } from '@/lib/types/generated/GetDriverOutputDTO';
import type { LeagueScoringConfigDTO } from '@/lib/types/generated/LeagueScoringConfigDTO';
import type { LeagueDetailViewData, LeagueInfoData, LiveRaceData, DriverSummaryData, SponsorInfo, NextRaceInfo, SeasonProgress, RecentResult } from '@/lib/view-data/LeagueDetailViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
/**
* LeagueDetailViewDataBuilder
*
* Transforms API DTOs into LeagueDetailViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return LeagueDetailViewDataBuilder.build(input);
}
static build(
static build(input: {
type LeagueDetailInputDTO = {
league: LeagueWithCapacityAndScoringDTO;
owner: GetDriverOutputDTO | null;
scoringConfig: LeagueScoringConfigDTO | null;
memberships: LeagueMembershipsDTO;
races: RaceDTO[];
sponsors: any[];
}): LeagueDetailViewData {
const { league, owner, scoringConfig, memberships, races, sponsors } = input;
sponsors: Array<{
id: string;
name: string;
tier: string;
logoUrl?: string;
websiteUrl?: string;
tagline?: string;
}>;
}
export class LeagueDetailViewDataBuilder {
public static build(apiDto: LeagueDetailInputDTO): LeagueDetailViewData {
const { league, owner, scoringConfig, memberships, races, sponsors } = apiDto;
// Calculate running races - using available fields from RaceDTO
const runningRaces: LiveRaceData[] = races
@@ -44,31 +43,17 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
const membersCount = Array.isArray(memberships.members) ? memberships.members.length : 0;
// League overview wants total races, not just completed.
// (In seed/demo data many races are `status: running`, which should still count.)
const racesCount = races.length;
// Compute real avgSOF from races
const racesWithSOF = races.filter(r => {
const sof = (r as any).strengthOfField;
const sof = (r as RaceDTO & { strengthOfField?: number }).strengthOfField;
return typeof sof === 'number' && sof > 0;
});
const avgSOF = racesWithSOF.length > 0
? Math.round(racesWithSOF.reduce((sum, r) => sum + ((r as any).strengthOfField || 0), 0) / racesWithSOF.length)
? Math.round(racesWithSOF.reduce((sum, r) => sum + ((r as RaceDTO & { strengthOfField?: number }).strengthOfField || 0), 0) / racesWithSOF.length)
: null;
if (process.env.NODE_ENV !== 'production') {
const race0 = races.length > 0 ? races[0] : null;
console.info(
'[LeagueDetailViewDataBuilder] leagueId=%s members=%d races=%d racesWithSOF=%d avgSOF=%s race0=%o',
league.id,
membersCount,
racesCount,
racesWithSOF.length,
String(avgSOF),
race0,
);
}
const info: LeagueInfoData = {
name: league.name,
description: league.description || '',
@@ -111,7 +96,7 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
.map(m => ({
driverId: m.driverId,
driverName: m.driver.name,
avatarUrl: (m.driver as any).avatarUrl || null,
avatarUrl: (m.driver as GetDriverOutputDTO & { avatarUrl?: string }).avatarUrl || null,
rating: null,
rank: null,
roleBadgeText: 'Admin',
@@ -124,7 +109,7 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
.map(m => ({
driverId: m.driverId,
driverName: m.driver.name,
avatarUrl: (m.driver as any).avatarUrl || null,
avatarUrl: (m.driver as GetDriverOutputDTO & { avatarUrl?: string }).avatarUrl || null,
rating: null,
rank: null,
roleBadgeText: 'Steward',
@@ -137,7 +122,7 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
.map(m => ({
driverId: m.driverId,
driverName: m.driver.name,
avatarUrl: (m.driver as any).avatarUrl || null,
avatarUrl: (m.driver as GetDriverOutputDTO & { avatarUrl?: string }).avatarUrl || null,
rating: null,
rank: null,
roleBadgeText: 'Member',
@@ -154,8 +139,8 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
id: r.id,
name: r.name,
date: r.date,
track: (r as any).track,
car: (r as any).car,
track: (r as RaceDTO & { track?: string }).track || '',
car: (r as RaceDTO & { car?: string }).car || '',
}))[0];
// Calculate season progress (completed races vs total races)
@@ -179,8 +164,8 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
.map(r => ({
raceId: r.id,
raceName: r.name,
position: (r as any).position || 0,
points: (r as any).points || 0,
position: (r as RaceDTO & { position?: number }).position || 0,
points: (r as RaceDTO & { points?: number }).points || 0,
finishedAt: r.date,
}));
@@ -196,13 +181,15 @@ export class LeagueDetailViewDataBuilder implements ViewDataBuilder<any, any> {
adminSummaries,
stewardSummaries,
memberSummaries,
sponsorInsights: null, // Only for sponsor mode
sponsorInsights: null,
nextRace,
seasonProgress,
recentResults,
walletBalance: league.walletBalance,
pendingProtestsCount: league.pendingProtestsCount,
pendingJoinRequestsCount: league.pendingJoinRequestsCount,
walletBalance: league.walletBalance ?? 0,
pendingProtestsCount: league.pendingProtestsCount ?? 0,
pendingJoinRequestsCount: league.pendingJoinRequestsCount ?? 0,
};
}
}
LeagueDetailViewDataBuilder satisfies ViewDataBuilder<LeagueDetailInputDTO, LeagueDetailViewData>;

View File

@@ -1,25 +1,14 @@
/**
* LeagueLogoViewDataBuilder
*
* Transforms MediaBinaryDTO into LeagueLogoViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import type { MediaBinaryDTO } from '@/lib/types/generated/MediaBinaryDTO';
import type { LeagueLogoViewData } from '@/lib/view-data/LeagueLogoViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { LeagueLogoViewData } from '@/lib/view-data/LeagueLogoViewData';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LeagueLogoViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return LeagueLogoViewDataBuilder.build(input);
}
static build(
static build(apiDto: MediaBinaryDTO): LeagueLogoViewData {
export class LeagueLogoViewDataBuilder {
public static build(apiDto: MediaBinaryDTO): LeagueLogoViewData {
return {
buffer: Buffer.from(apiDto.buffer).toString('base64'),
contentType: apiDto.contentType,
};
}
}
LeagueLogoViewDataBuilder satisfies ViewDataBuilder<MediaBinaryDTO, LeagueLogoViewData>;

View File

@@ -2,26 +2,16 @@ import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMe
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
import type { LeagueRosterAdminViewData, RosterMemberData, JoinRequestData } from '@/lib/view-data/LeagueRosterAdminViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
/**
* LeagueRosterAdminViewDataBuilder
*
* Transforms API DTOs into LeagueRosterAdminViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LeagueRosterAdminViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return LeagueRosterAdminViewDataBuilder.build(input);
}
static build(
static build(input: {
type LeagueRosterAdminInputDTO = {
leagueId: string;
members: LeagueRosterMemberDTO[];
joinRequests: LeagueRosterJoinRequestDTO[];
}): LeagueRosterAdminViewData {
}
export class LeagueRosterAdminViewDataBuilder {
public static build(input: LeagueRosterAdminInputDTO): LeagueRosterAdminViewData {
const { leagueId, members, joinRequests } = input;
// Transform members
@@ -29,7 +19,7 @@ export class LeagueRosterAdminViewDataBuilder implements ViewDataBuilder<any, an
driverId: member.driverId,
driver: {
id: member.driverId,
name: member.driver?.name || 'Unknown Driver',
name: (member.driver as { name?: string })?.name || 'Unknown Driver',
},
role: member.role,
joinedAt: member.joinedAt,
@@ -41,11 +31,11 @@ export class LeagueRosterAdminViewDataBuilder implements ViewDataBuilder<any, an
id: req.id,
driver: {
id: req.driverId,
name: 'Unknown Driver', // driver field is unknown type
name: (req as { driver?: { name?: string } }).driver?.name || 'Unknown Driver',
},
requestedAt: req.requestedAt,
formattedRequestedAt: DateDisplay.formatShort(req.requestedAt),
message: req.message,
message: req.message ?? undefined,
}));
return {
@@ -55,3 +45,5 @@ export class LeagueRosterAdminViewDataBuilder implements ViewDataBuilder<any, an
};
}
}
LeagueRosterAdminViewDataBuilder satisfies ViewDataBuilder<LeagueRosterAdminInputDTO, LeagueRosterAdminViewData>;

View File

@@ -1,19 +1,24 @@
import { LeagueScheduleViewData } from '@/lib/view-data/leagues/LeagueScheduleViewData';
import { LeagueScheduleApiDto } from '@/lib/types/tbd/LeagueScheduleApiDto';
import type { LeagueScheduleViewData } from '@/lib/view-data/LeagueScheduleViewData';
import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LeagueScheduleViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return LeagueScheduleViewDataBuilder.build(input);
export interface LeagueScheduleInputDTO {
apiDto: LeagueScheduleDTO;
currentDriverId?: string;
isAdmin?: boolean;
}
export class LeagueScheduleViewDataBuilder implements ViewDataBuilder<LeagueScheduleInputDTO, LeagueScheduleViewData> {
build(input: LeagueScheduleInputDTO): LeagueScheduleViewData {
return LeagueScheduleViewDataBuilder.build(input.apiDto, input.currentDriverId, input.isAdmin);
}
static build(
static build(apiDto: LeagueScheduleApiDto, currentDriverId?: string, isAdmin: boolean = false): LeagueScheduleViewData {
public static build(apiDto: LeagueScheduleDTO, currentDriverId?: string, isAdmin: boolean = false): LeagueScheduleViewData {
const now = new Date();
return {
leagueId: apiDto.leagueId,
leagueId: (apiDto as any).leagueId || '',
races: apiDto.races.map((race) => {
const scheduledAt = new Date(race.date);
const isPast = scheduledAt.getTime() <= now.getTime();
@@ -23,12 +28,12 @@ export class LeagueScheduleViewDataBuilder implements ViewDataBuilder<any, any>
id: race.id,
name: race.name,
scheduledAt: race.date,
track: race.track,
car: race.car,
sessionType: race.sessionType,
track: race.track || '',
car: race.car || '',
sessionType: race.sessionType || 'race',
isPast,
isUpcoming,
status: isPast ? 'completed' : 'scheduled',
status: (race.status as any) || (isPast ? 'completed' : 'scheduled'),
// Registration info (would come from API in real implementation)
isUserRegistered: false,
canRegister: isUpcoming,

View File

@@ -1,5 +1,5 @@
import { LeagueSettingsApiDto } from '@/lib/types/tbd/LeagueSettingsApiDto';
import { LeagueSettingsViewData } from '@/lib/view-data/LeagueSettingsViewData';
import type { LeagueSettingsDTO } from '@/lib/types/generated/LeagueSettingsDTO';
import type { LeagueSettingsViewData } from '@/lib/view-data/LeagueSettingsViewData';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -8,12 +8,13 @@ export class LeagueSettingsViewDataBuilder implements ViewDataBuilder<any, any>
return LeagueSettingsViewDataBuilder.build(input);
}
static build(
static build(apiDto: LeagueSettingsApiDto): LeagueSettingsViewData {
static build(apiDto: LeagueSettingsDTO): LeagueSettingsViewData {
return {
leagueId: apiDto.leagueId,
league: apiDto.league,
config: apiDto.config,
league: (apiDto as any).league || { id: '', name: '', ownerId: '', createdAt: '' },
config: (apiDto as any).config || {},
presets: (apiDto as any).presets || [],
owner: (apiDto as any).owner || null,
members: (apiDto as any).members || [],
};
}
}

View File

@@ -10,7 +10,6 @@ export class LeagueSponsorshipsViewDataBuilder implements ViewDataBuilder<any, a
return LeagueSponsorshipsViewDataBuilder.build(input);
}
static build(
static build(apiDto: LeagueSponsorshipsApiDto): LeagueSponsorshipsViewData {
return {
leagueId: apiDto.leagueId,

View File

@@ -1,7 +1,6 @@
import { CurrencyDisplay } from '@/lib/display-objects/CurrencyDisplay';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto';
import { LeagueWalletTransactionViewData, LeagueWalletViewData } from '@/lib/view-data/LeagueWalletViewData';
import type { GetLeagueWalletOutputDTO } from '@/lib/types/generated/GetLeagueWalletOutputDTO';
import type { LeagueWalletViewData } from '@/lib/view-data/LeagueWalletViewData';
import type { WalletTransactionViewData } from '@/lib/view-data/WalletTransactionViewData';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -10,29 +9,29 @@ export class LeagueWalletViewDataBuilder implements ViewDataBuilder<any, any> {
return LeagueWalletViewDataBuilder.build(input);
}
static build(
static build(apiDto: LeagueWalletApiDto): LeagueWalletViewData {
const transactions: LeagueWalletTransactionViewData[] = apiDto.transactions.map(t => ({
...t,
formattedAmount: CurrencyDisplay.format(t.amount, apiDto.currency),
amountColor: t.amount >= 0 ? 'green' : 'red',
formattedDate: DateDisplay.formatShort(t.createdAt),
statusColor: t.status === 'completed' ? 'green' : t.status === 'pending' ? 'yellow' : 'red',
typeColor: 'blue',
static build(apiDto: GetLeagueWalletOutputDTO): LeagueWalletViewData {
const transactions: WalletTransactionViewData[] = apiDto.transactions.map(t => ({
id: t.id,
type: t.type as any,
description: t.description,
amount: t.amount,
fee: t.fee,
netAmount: t.netAmount,
date: (t as any).createdAt || (t as any).date || new Date().toISOString(),
status: t.status as any,
reference: t.reference,
}));
return {
leagueId: apiDto.leagueId,
balance: apiDto.balance,
formattedBalance: CurrencyDisplay.format(apiDto.balance, apiDto.currency),
totalRevenue: apiDto.balance, // Mock
formattedTotalRevenue: CurrencyDisplay.format(apiDto.balance, apiDto.currency),
totalFees: 0, // Mock
formattedTotalFees: CurrencyDisplay.format(0, apiDto.currency),
pendingPayouts: 0, // Mock
formattedPendingPayouts: CurrencyDisplay.format(0, apiDto.currency),
currency: apiDto.currency,
totalRevenue: apiDto.totalRevenue,
totalFees: apiDto.totalFees,
totalWithdrawals: apiDto.totalWithdrawals,
pendingPayouts: apiDto.pendingPayouts,
transactions,
canWithdraw: apiDto.canWithdraw,
withdrawalBlockReason: apiDto.withdrawalBlockReason,
};
}
}

View File

@@ -1,12 +1,6 @@
import type { AllLeaguesWithCapacityAndScoringDTO } from '@/lib/types/generated/AllLeaguesWithCapacityAndScoringDTO';
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
/**
* LeaguesViewDataBuilder
*
* Transforms AllLeaguesWithCapacityAndScoringDTO (API DTO) into LeaguesViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LeaguesViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -14,7 +8,6 @@ export class LeaguesViewDataBuilder implements ViewDataBuilder<any, any> {
return LeaguesViewDataBuilder.build(input);
}
static build(
static build(apiDto: AllLeaguesWithCapacityAndScoringDTO): LeaguesViewData {
return {
leagues: apiDto.leagues.map((league) => ({
@@ -36,7 +29,7 @@ export class LeaguesViewDataBuilder implements ViewDataBuilder<any, any> {
scoring: league.scoring ? {
gameId: league.scoring.gameId,
gameName: league.scoring.gameName,
primaryChampionshipType: league.scoring.primaryChampionshipType,
primaryChampionshipType: league.scoring.primaryChampionshipType as any,
scoringPresetId: league.scoring.scoringPresetId,
scoringPresetName: league.scoring.scoringPresetName,
dropPolicySummary: league.scoring.dropPolicySummary,

View File

@@ -1,24 +1,9 @@
/**
* Login View Data Builder
*
* Transforms LoginPageDTO into ViewData for the login template.
* Deterministic, side-effect free, no business logic.
*/
import type { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO';
import type { LoginViewData } from '@/lib/view-data/LoginViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { LoginPageDTO } from '@/lib/services/auth/types/LoginPageDTO';
import { LoginViewData } from '../../view-data/LoginViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import { error } from 'console';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class LoginViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return LoginViewDataBuilder.build(input);
}
static build(
static build(apiDto: LoginPageDTO): LoginViewData {
export class LoginViewDataBuilder {
public static build(apiDto: LoginPageDTO): LoginViewData {
return {
returnTo: apiDto.returnTo,
hasInsufficientPermissions: apiDto.hasInsufficientPermissions,
@@ -40,3 +25,5 @@ export class LoginViewDataBuilder implements ViewDataBuilder<any, any> {
};
}
}
LoginViewDataBuilder satisfies ViewDataBuilder<LoginPageDTO, LoginViewData>;

View File

@@ -16,7 +16,6 @@ export class OnboardingViewDataBuilder implements ViewDataBuilder<any, any> {
return OnboardingViewDataBuilder.build(input);
}
static build(
static build(apiDto: Result<{ isAlreadyOnboarded: boolean }, PresentationError>): Result<OnboardingPageViewData, PresentationError> {
if (apiDto.isErr()) {
return Result.err(apiDto.getError());

View File

@@ -15,7 +15,6 @@ export class ProfileViewDataBuilder implements ViewDataBuilder<any, any> {
return ProfileViewDataBuilder.build(input);
}
static build(
static build(apiDto: GetDriverProfileOutputDTO): ProfileViewData {
const driver = apiDto.currentDriver;

View File

@@ -1,33 +1,5 @@
import { ProtestDetailViewData } from '@/lib/view-data/ProtestDetailViewData';
interface ProtestDetailApiDto {
id: string;
leagueId: string;
status: string;
submittedAt: string;
incident: {
lap: number;
description: string;
};
protestingDriver: {
id: string;
name: string;
};
accusedDriver: {
id: string;
name: string;
};
race: {
id: string;
name: string;
scheduledAt: string;
};
penaltyTypes: Array<{
type: string;
label: string;
description: string;
}>;
}
import type { RaceProtestDTO } from '@/lib/types/generated/RaceProtestDTO';
import type { ProtestDetailViewData } from '@/lib/view-data/ProtestDetailViewData';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -36,18 +8,20 @@ export class ProtestDetailViewDataBuilder implements ViewDataBuilder<any, any> {
return ProtestDetailViewDataBuilder.build(input);
}
static build(
static build(apiDto: ProtestDetailApiDto): ProtestDetailViewData {
static build(apiDto: RaceProtestDTO): ProtestDetailViewData {
return {
protestId: apiDto.id,
leagueId: apiDto.leagueId,
leagueId: (apiDto as any).leagueId || '',
status: apiDto.status,
submittedAt: apiDto.submittedAt,
incident: apiDto.incident,
protestingDriver: apiDto.protestingDriver,
accusedDriver: apiDto.accusedDriver,
race: apiDto.race,
penaltyTypes: apiDto.penaltyTypes,
submittedAt: (apiDto as any).submittedAt || apiDto.filedAt,
incident: {
lap: (apiDto.incident as any)?.lap || 0,
description: (apiDto.incident as any)?.description || '',
},
protestingDriver: (apiDto as any).protestingDriver || { id: apiDto.protestingDriverId, name: 'Unknown' },
accusedDriver: (apiDto as any).accusedDriver || { id: apiDto.accusedDriverId, name: 'Unknown' },
race: (apiDto as any).race || { id: '', name: '', scheduledAt: '' },
penaltyTypes: (apiDto as any).penaltyTypes || [],
};
}
}

View File

@@ -1,11 +1,6 @@
import { RaceDetailEntry, RaceDetailLeague, RaceDetailRace, RaceDetailRegistration, RaceDetailUserResult, RaceDetailViewData } from '@/lib/view-data/RaceDetailViewData';
import type { RaceDetailDTO } from '@/lib/types/generated/RaceDetailDTO';
import type { RaceDetailEntry, RaceDetailLeague, RaceDetailRace, RaceDetailRegistration, RaceDetailUserResult, RaceDetailViewData } from '@/lib/view-data/RaceDetailViewData';
/**
* Race Detail View Data Builder
*
* Transforms API DTO into ViewData for the race detail template.
* Deterministic, side-effect free.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class RaceDetailViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -13,8 +8,7 @@ export class RaceDetailViewDataBuilder implements ViewDataBuilder<any, any> {
return RaceDetailViewDataBuilder.build(input);
}
static build(
static build(apiDto: any): RaceDetailViewData {
static build(apiDto: RaceDetailDTO): RaceDetailViewData {
if (!apiDto || !apiDto.race) {
return {
race: {
@@ -36,11 +30,11 @@ export class RaceDetailViewDataBuilder implements ViewDataBuilder<any, any> {
const race: RaceDetailRace = {
id: apiDto.race.id,
track: apiDto.race.track,
car: apiDto.race.car,
track: apiDto.race.track || '',
car: apiDto.race.car || '',
scheduledAt: apiDto.race.scheduledAt,
status: apiDto.race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
sessionType: apiDto.race.sessionType,
status: apiDto.race.status as any,
sessionType: apiDto.race.sessionType || 'race',
};
const league: RaceDetailLeague | undefined = apiDto.league ? {
@@ -48,8 +42,8 @@ export class RaceDetailViewDataBuilder implements ViewDataBuilder<any, any> {
name: apiDto.league.name,
description: apiDto.league.description || undefined,
settings: {
maxDrivers: apiDto.league.settings?.maxDrivers || 32,
qualifyingFormat: apiDto.league.settings?.qualifyingFormat || 'Open',
maxDrivers: (apiDto.league.settings as any)?.maxDrivers || 32,
qualifyingFormat: (apiDto.league.settings as any)?.qualifyingFormat || 'Open',
},
} : undefined;
@@ -83,7 +77,7 @@ export class RaceDetailViewDataBuilder implements ViewDataBuilder<any, any> {
entryList,
registration,
userResult,
canReopenRace: apiDto.canReopenRace || false,
canReopenRace: (apiDto as any).canReopenRace || false,
};
}
}

View File

@@ -1,11 +1,6 @@
import { RaceResultsPenalty, RaceResultsResult, RaceResultsViewData } from '@/lib/view-data/RaceResultsViewData';
import type { RaceResultsDetailDTO } from '@/lib/types/generated/RaceResultsDetailDTO';
import type { RaceResultsPenalty, RaceResultsResult, RaceResultsViewData } from '@/lib/view-data/RaceResultsViewData';
/**
* Race Results View Data Builder
*
* Transforms API DTO into ViewData for the race results template.
* Deterministic, side-effect free.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class RaceResultsViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -13,8 +8,7 @@ export class RaceResultsViewDataBuilder implements ViewDataBuilder<any, any> {
return RaceResultsViewDataBuilder.build(input);
}
static build(
static build(apiDto: unknown): RaceResultsViewData {
static build(apiDto: RaceResultsDetailDTO): RaceResultsViewData {
if (!apiDto) {
return {
raceSOF: null,

View File

@@ -1,11 +1,6 @@
import { Driver, Penalty, Protest, RaceStewardingViewData } from '@/lib/view-data/RaceStewardingViewData';
import type { LeagueAdminProtestsDTO } from '@/lib/types/generated/LeagueAdminProtestsDTO';
import type { RaceStewardingViewData } from '@/lib/view-data/RaceStewardingViewData';
/**
* Race Stewarding View Data Builder
*
* Transforms API DTO into ViewData for the race stewarding template.
* Deterministic, side-effect free.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class RaceStewardingViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -13,19 +8,14 @@ export class RaceStewardingViewDataBuilder implements ViewDataBuilder<any, any>
return RaceStewardingViewDataBuilder.build(input);
}
static build(
static build(apiDto: unknown): RaceStewardingViewData {
static build(apiDto: LeagueAdminProtestsDTO): RaceStewardingViewData {
if (!apiDto) {
return {
race: null,
league: null,
pendingProtests: [],
resolvedProtests: [],
protests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
}
@@ -34,15 +24,20 @@ export class RaceStewardingViewDataBuilder implements ViewDataBuilder<any, any>
const race = dto.race ? {
id: dto.race.id,
track: dto.race.track,
track: dto.race.track || '',
scheduledAt: dto.race.scheduledAt,
status: dto.race.status || 'scheduled',
} : null;
const league = dto.league ? {
id: dto.league.id,
name: dto.league.name || '',
} : null;
const pendingProtests: Protest[] = (dto.pendingProtests || []).map((p: any) => ({
const protests = [
...(dto.pendingProtests || []),
...(dto.resolvedProtests || []),
].map((p: any) => ({
id: p.id,
protestingDriverId: p.protestingDriverId,
accusedDriverId: p.accusedDriverId,
@@ -56,21 +51,7 @@ export class RaceStewardingViewDataBuilder implements ViewDataBuilder<any, any>
decisionNotes: p.decisionNotes,
}));
const resolvedProtests: Protest[] = (dto.resolvedProtests || []).map((p: any) => ({
id: p.id,
protestingDriverId: p.protestingDriverId,
accusedDriverId: p.accusedDriverId,
incident: {
lap: p.incident?.lap || 0,
description: p.incident?.description || '',
},
filedAt: p.filedAt,
status: p.status,
proofVideoUrl: p.proofVideoUrl,
decisionNotes: p.decisionNotes,
}));
const penalties: Penalty[] = (dto.penalties || []).map((p: any) => ({
const penalties = (dto.penalties || []).map((p: any) => ({
id: p.id,
driverId: p.driverId,
type: p.type,
@@ -79,18 +60,14 @@ export class RaceStewardingViewDataBuilder implements ViewDataBuilder<any, any>
notes: p.notes,
}));
const driverMap: Record<string, Driver> = dto.driverMap || {};
const driverMap = dto.driverMap || {};
return {
race,
league,
pendingProtests,
resolvedProtests,
protests,
penalties,
driverMap,
pendingCount: dto.pendingCount || pendingProtests.length,
resolvedCount: dto.resolvedCount || resolvedProtests.length,
penaltiesCount: dto.penaltiesCount || penalties.length,
};
}
}

View File

@@ -11,7 +11,6 @@ export class RacesViewDataBuilder implements ViewDataBuilder<any, any> {
return RacesViewDataBuilder.build(input);
}
static build(
static build(apiDto: RacesPageDataDTO): RacesViewData {
const now = new Date();
const races = apiDto.races.map((race): RaceViewData => {

View File

@@ -1,24 +1,9 @@
/**
* Reset Password View Data Builder
*
* Transforms ResetPasswordPageDTO into ViewData for the reset password template.
* Deterministic, side-effect free, no business logic.
*/
import type { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO';
import type { ResetPasswordViewData } from '@/lib/view-data/ResetPasswordViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { ResetPasswordPageDTO } from '@/lib/services/auth/types/ResetPasswordPageDTO';
import { ResetPasswordViewData } from '../../view-data/ResetPasswordViewData';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import { error } from 'console';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class ResetPasswordViewDataBuilder implements ViewDataBuilder<any, any> {
build(input: any): any {
return ResetPasswordViewDataBuilder.build(input);
}
static build(
static build(apiDto: ResetPasswordPageDTO): ResetPasswordViewData {
export class ResetPasswordViewDataBuilder {
public static build(apiDto: ResetPasswordPageDTO): ResetPasswordViewData {
return {
token: apiDto.token,
returnTo: apiDto.returnTo,
@@ -38,3 +23,5 @@ export class ResetPasswordViewDataBuilder implements ViewDataBuilder<any, any> {
};
}
}
ResetPasswordViewDataBuilder satisfies ViewDataBuilder<ResetPasswordPageDTO, ResetPasswordViewData>;

View File

@@ -1,14 +1,6 @@
import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO';
import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardViewData';
import { CurrencyDisplay } from '@/lib/display-objects/CurrencyDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
/**
* Sponsor Dashboard ViewData Builder
*
* Transforms SponsorDashboardDTO into ViewData for templates.
* Deterministic and side-effect free.
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class SponsorDashboardViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -16,32 +8,10 @@ export class SponsorDashboardViewDataBuilder implements ViewDataBuilder<any, any
return SponsorDashboardViewDataBuilder.build(input);
}
static build(
static build(apiDto: SponsorDashboardDTO): SponsorDashboardViewData {
const totalInvestmentValue = apiDto.investment.activeSponsorships * 1000;
return {
sponsorId: (apiDto as any).sponsorId || '',
sponsorName: apiDto.sponsorName,
totalImpressions: NumberDisplay.format(apiDto.metrics.impressions),
totalInvestment: CurrencyDisplay.format(totalInvestmentValue),
metrics: {
impressionsChange: apiDto.metrics.impressions > 1000 ? 15 : -5,
viewersChange: 8,
exposureChange: 12,
},
categoryData: {
leagues: { count: 2, countLabel: '2 active', impressions: 1500, impressionsLabel: '1,500' },
teams: { count: 1, countLabel: '1 active', impressions: 800, impressionsLabel: '800' },
drivers: { count: 3, countLabel: '3 active', impressions: 2200, impressionsLabel: '2,200' },
races: { count: 1, countLabel: '1 active', impressions: 500, impressionsLabel: '500' },
platform: { count: 0, countLabel: '0 active', impressions: 0, impressionsLabel: '0' },
},
sponsorships: apiDto.sponsorships,
activeSponsorships: apiDto.investment.activeSponsorships,
formattedTotalInvestment: CurrencyDisplay.format(totalInvestmentValue),
costPerThousandViews: CurrencyDisplay.format(50),
upcomingRenewals: [], // Mock empty for now
recentActivity: [], // Mock empty for now
};
}
}

View File

@@ -1,12 +1,5 @@
/**
* SponsorLogoViewDataBuilder
*
* Transforms MediaBinaryDTO into SponsorLogoViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
import { MediaBinaryDTO } from '@/lib/types/MediaBinaryDTO';
import { SponsorLogoViewData } from '@/lib/view-data/SponsorLogoViewData';
import type { GetMediaOutputDTO } from '@/lib/types/generated/GetMediaOutputDTO';
import type { SponsorLogoViewData } from '@/lib/view-data/SponsorLogoViewData';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -15,11 +8,10 @@ export class SponsorLogoViewDataBuilder implements ViewDataBuilder<any, any> {
return SponsorLogoViewDataBuilder.build(input);
}
static build(
static build(apiDto: MediaBinaryDTO): SponsorLogoViewData {
static build(apiDto: GetMediaOutputDTO): SponsorLogoViewData {
return {
buffer: Buffer.from(apiDto.buffer).toString('base64'),
contentType: apiDto.contentType,
buffer: (apiDto as any).buffer ? Buffer.from((apiDto as any).buffer).toString('base64') : '',
contentType: (apiDto as any).contentType || apiDto.type,
};
}
}

View File

@@ -8,7 +8,6 @@ export class SponsorshipRequestsViewDataBuilder implements ViewDataBuilder<any,
return SponsorshipRequestsViewDataBuilder.build(input);
}
static build(
static build(apiDto: GetPendingSponsorshipRequestsOutputDTO): SponsorshipRequestsViewData {
return {
sections: [

View File

@@ -1,14 +1,10 @@
import type { TeamDetailPageDto } from '@/lib/page-queries/TeamDetailPageQuery';
import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO';
import type { TeamDetailViewData, TeamDetailData, TeamMemberData, SponsorMetric, TeamTab } from '@/lib/view-data/TeamDetailViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { MemberDisplay } from '@/lib/display-objects/MemberDisplay';
import { LeagueDisplay } from '@/lib/display-objects/LeagueDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
/**
* TeamDetailViewDataBuilder - Transforms TeamDetailPageDto into ViewData
* Deterministic; side-effect free; no HTTP calls
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -16,26 +12,25 @@ export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
return TeamDetailViewDataBuilder.build(input);
}
static build(
static build(apiDto: TeamDetailPageDto): TeamDetailViewData {
static build(apiDto: GetTeamDetailsOutputDTO): TeamDetailViewData {
const team: TeamDetailData = {
id: apiDto.team.id,
name: apiDto.team.name,
tag: apiDto.team.tag,
description: apiDto.team.description,
ownerId: apiDto.team.ownerId,
leagues: apiDto.team.leagues,
leagues: (apiDto.team as any).leagues || [],
createdAt: apiDto.team.createdAt,
foundedDateLabel: apiDto.team.createdAt ? DateDisplay.formatMonthYear(apiDto.team.createdAt) : 'Unknown',
specialization: apiDto.team.specialization,
region: apiDto.team.region,
languages: apiDto.team.languages,
category: apiDto.team.category,
membership: apiDto.team.membership,
canManage: apiDto.team.canManage,
specialization: (apiDto.team as any).specialization || null,
region: (apiDto.team as any).region || null,
languages: (apiDto.team as any).languages || [],
category: (apiDto.team as any).category || null,
membership: (apiDto.team as any).membership || 'open',
canManage: apiDto.canManage,
};
const memberships: TeamMemberData[] = apiDto.memberships.map((membership) => ({
const memberships: TeamMemberData[] = ((apiDto as any).memberships || []).map((membership: any) => ({
driverId: membership.driverId,
driverName: membership.driverName,
role: membership.role,
@@ -46,7 +41,8 @@ export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
}));
// Calculate isAdmin based on current driver's role
const currentDriverMembership = memberships.find(m => m.driverId === apiDto.currentDriverId);
const currentDriverId = (apiDto as any).currentDriverId;
const currentDriverMembership = memberships.find(m => m.driverId === currentDriverId);
const isAdmin = currentDriverMembership?.role === 'owner' || currentDriverMembership?.role === 'manager';
// Build sponsor metrics
@@ -89,7 +85,7 @@ export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
return {
team,
memberships,
currentDriverId: apiDto.currentDriverId,
currentDriverId: currentDriverId || null,
isAdmin,
teamMetrics,
tabs,

View File

@@ -1,4 +1,3 @@
import { ViewData } from '@/lib/contracts/view-data/ViewData';
import type { GetTeamsLeaderboardOutputDTO } from '@/lib/types/generated/GetTeamsLeaderboardOutputDTO';
import type { TeamRankingsViewData } from '@/lib/view-data/TeamRankingsViewData';
@@ -9,7 +8,6 @@ export class TeamRankingsViewDataBuilder implements ViewDataBuilder<any, any> {
return TeamRankingsViewDataBuilder.build(input);
}
static build(
public static build(apiDto: GetTeamsLeaderboardOutputDTO): TeamRankingsViewData {
const allTeams = apiDto.teams.map(t => ({
...t,

View File

@@ -1,13 +1,9 @@
import type { TeamsPageDto } from '@/lib/page-queries/TeamsPageQuery';
import type { GetAllTeamsOutputDTO } from '@/lib/types/generated/GetAllTeamsOutputDTO';
import type { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewData';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
/**
* TeamsViewDataBuilder - Transforms TeamsPageDto into ViewData for TeamsTemplate
* Deterministic; side-effect free; no HTTP calls
*/
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
export class TeamsViewDataBuilder implements ViewDataBuilder<any, any> {
@@ -15,23 +11,22 @@ export class TeamsViewDataBuilder implements ViewDataBuilder<any, any> {
return TeamsViewDataBuilder.build(input);
}
static build(
static build(apiDto: TeamsPageDto): TeamsViewData {
static build(apiDto: GetAllTeamsOutputDTO): TeamsViewData {
const teams: TeamSummaryData[] = apiDto.teams.map((team: TeamListItemDTO): TeamSummaryData => ({
teamId: team.id,
teamName: team.name,
memberCount: team.memberCount,
logoUrl: team.logoUrl,
logoUrl: team.logoUrl || '',
ratingLabel: RatingDisplay.format(team.rating),
ratingValue: team.rating || 0,
winsLabel: NumberDisplay.format(team.totalWins || 0),
racesLabel: NumberDisplay.format(team.totalRaces || 0),
region: team.region,
region: team.region || '',
isRecruiting: team.isRecruiting,
category: team.category,
performanceLevel: team.performanceLevel,
description: team.description,
countryCode: team.region, // Assuming region contains country code for now
category: team.category || '',
performanceLevel: team.performanceLevel || '',
description: team.description || '',
countryCode: team.region || '', // Assuming region contains country code for now
}));
return { teams };

View File

@@ -1,145 +1,25 @@
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
import type { DriverProfileDriverSummaryDTO } from '@/lib/types/generated/DriverProfileDriverSummaryDTO';
import type { DriverProfileStatsDTO } from '@/lib/types/generated/DriverProfileStatsDTO';
import type { DriverProfileFinishDistributionDTO } from '@/lib/types/generated/DriverProfileFinishDistributionDTO';
import type { DriverProfileTeamMembershipDTO } from '@/lib/types/generated/DriverProfileTeamMembershipDTO';
import type { DriverProfileSocialSummaryDTO } from '@/lib/types/generated/DriverProfileSocialSummaryDTO';
import type { DriverProfileExtendedProfileDTO } from '@/lib/types/generated/DriverProfileExtendedProfileDTO';
import { DriverProfileViewModel } from '@/lib/view-models/DriverProfileViewModel';
import type {
DriverProfileDriverSummaryViewModel,
DriverProfileStatsViewModel,
DriverProfileFinishDistributionViewModel,
DriverProfileTeamMembershipViewModel,
DriverProfileSocialSummaryViewModel,
DriverProfileExtendedProfileViewModel,
} from '@/lib/view-models/DriverProfileViewModel';
import { ProfileViewData } from '@/lib/view-data/ProfileViewData';
import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder";
/**
* DriverProfileViewModelBuilder
*
* Transforms GetDriverProfileOutputDTO into DriverProfileViewModel.
* Transforms ProfileViewData into DriverProfileViewModel.
* Deterministic, side-effect free, no HTTP calls.
*/
import { ViewModelBuilder } from "../../contracts/builders/ViewModelBuilder";
export class DriverProfileViewModelBuilder implements ViewModelBuilder<any, any> {
build(input: any): any {
return DriverProfileViewModelBuilder.build(input);
}
static build(
/**
* Build ViewModel from API DTO
* Build ViewModel from ViewData
*
* @param apiDto - The API transport DTO
* @returns ViewModel ready for template
* @param viewData - The template-ready ViewData
* @returns ViewModel ready for client-side state
*/
static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewModel {
return new DriverProfileViewModel({
currentDriver: apiDto.currentDriver ? this.transformCurrentDriver(apiDto.currentDriver) : null,
stats: apiDto.stats ? this.transformStats(apiDto.stats) : null,
finishDistribution: apiDto.finishDistribution ? this.transformFinishDistribution(apiDto.finishDistribution) : null,
teamMemberships: apiDto.teamMemberships.map(m => this.transformTeamMembership(m)),
socialSummary: this.transformSocialSummary(apiDto.socialSummary),
extendedProfile: apiDto.extendedProfile ? this.transformExtendedProfile(apiDto.extendedProfile) : null,
});
}
private static transformCurrentDriver(dto: DriverProfileDriverSummaryDTO): DriverProfileDriverSummaryViewModel {
return {
id: dto.id,
name: dto.name,
country: dto.country,
avatarUrl: dto.avatarUrl || '', // Handle undefined
iracingId: dto.iracingId || null,
joinedAt: dto.joinedAt,
rating: dto.rating ?? null,
globalRank: dto.globalRank ?? null,
consistency: dto.consistency ?? null,
bio: dto.bio || null,
totalDrivers: dto.totalDrivers ?? null,
};
}
private static transformStats(dto: DriverProfileStatsDTO): DriverProfileStatsViewModel {
return {
totalRaces: dto.totalRaces,
wins: dto.wins,
podiums: dto.podiums,
dnfs: dto.dnfs,
avgFinish: dto.avgFinish ?? null,
bestFinish: dto.bestFinish ?? null,
worstFinish: dto.worstFinish ?? null,
finishRate: dto.finishRate ?? null,
winRate: dto.winRate ?? null,
podiumRate: dto.podiumRate ?? null,
percentile: dto.percentile ?? null,
rating: dto.rating ?? null,
consistency: dto.consistency ?? null,
overallRank: dto.overallRank ?? null,
};
}
private static transformFinishDistribution(dto: DriverProfileFinishDistributionDTO): DriverProfileFinishDistributionViewModel {
return {
totalRaces: dto.totalRaces,
wins: dto.wins,
podiums: dto.podiums,
topTen: dto.topTen,
dnfs: dto.dnfs,
other: dto.other,
};
}
private static transformTeamMembership(dto: DriverProfileTeamMembershipDTO): DriverProfileTeamMembershipViewModel {
return {
teamId: dto.teamId,
teamName: dto.teamName,
teamTag: dto.teamTag || null,
role: dto.role,
joinedAt: dto.joinedAt,
isCurrent: dto.isCurrent,
};
}
private static transformSocialSummary(dto: DriverProfileSocialSummaryDTO): DriverProfileSocialSummaryViewModel {
return {
friendsCount: dto.friendsCount,
friends: dto.friends.map(f => ({
id: f.id,
name: f.name,
country: f.country,
avatarUrl: f.avatarUrl || '', // Handle undefined
})),
};
}
private static transformExtendedProfile(dto: DriverProfileExtendedProfileDTO): DriverProfileExtendedProfileViewModel {
return {
socialHandles: dto.socialHandles.map(h => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
platform: h.platform as any, // Type assertion - assuming valid platform
handle: h.handle,
url: h.url,
})),
achievements: dto.achievements.map(a => ({
id: a.id,
title: a.title,
description: a.description,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
icon: a.icon as any, // Type assertion - assuming valid icon
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rarity: a.rarity as any, // Type assertion - assuming valid rarity
earnedAt: a.earnedAt,
})),
racingStyle: dto.racingStyle,
favoriteTrack: dto.favoriteTrack,
favoriteCar: dto.favoriteCar,
timezone: dto.timezone,
availableHours: dto.availableHours,
lookingForTeam: dto.lookingForTeam,
openToRequests: dto.openToRequests,
};
static build(viewData: ProfileViewData): DriverProfileViewModel {
return new DriverProfileViewModel(viewData);
}
}

View File

@@ -1,5 +1,5 @@
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
import { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel';
import { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
/**
* DriversViewModelBuilder
@@ -14,10 +14,7 @@ export class DriversViewModelBuilder implements ViewModelBuilder<any, any> {
return DriversViewModelBuilder.build(input);
}
static build(
static build(apiDto: DriversLeaderboardDTO): DriverLeaderboardViewModel {
return new DriverLeaderboardViewModel({
drivers: apiDto.drivers,
});
static build(viewData: LeaderboardsViewData): DriverLeaderboardViewModel {
return new DriverLeaderboardViewModel(viewData);
}
}

View File

@@ -9,24 +9,6 @@ export class LeagueSummaryViewModelBuilder implements ViewModelBuilder<any, any>
}
static build(league: LeaguesViewData['leagues'][number]): LeagueSummaryViewModel {
return {
id: league.id,
name: league.name,
description: league.description ?? '',
logoUrl: league.logoUrl,
ownerId: league.ownerId,
createdAt: league.createdAt,
maxDrivers: league.maxDrivers,
usedDriverSlots: league.usedDriverSlots,
maxTeams: league.maxTeams ?? 0,
usedTeamSlots: league.usedTeamSlots ?? 0,
structureSummary: league.structureSummary,
timingSummary: league.timingSummary,
category: league.category ?? undefined,
scoring: league.scoring ? {
...league.scoring,
primaryChampionshipType: league.scoring.primaryChampionshipType as 'driver' | 'team' | 'nations' | 'trophy',
} : undefined,
};
return new LeagueSummaryViewModel(league as any);
}
}

View File

@@ -16,12 +16,11 @@ export class OnboardingViewModelBuilder implements ViewModelBuilder<any, any> {
return OnboardingViewModelBuilder.build(input);
}
static build(
static build(apiDto: { isAlreadyOnboarded: boolean }): Result<OnboardingViewModel, DomainError> {
try {
return Result.ok({
return Result.ok(new OnboardingViewModel({
isAlreadyOnboarded: apiDto.isAlreadyOnboarded || false,
});
}));
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to build ViewModel';
return Result.err({ type: 'unknown', message: errorMessage });

View File

@@ -12,17 +12,22 @@
* - Must be named *ViewDataBuilder
* - Must have 'use client' directive
* - Must implement static build() method
* - Must use 'satisfies' for static type enforcement
*/
import { JsonValue } from '../types/primitives';
import { ViewData } from '../view-data/ViewData';
export interface ViewDataBuilder<TDTO extends JsonValue, TViewData extends ViewData> {
/**
* Transform DTO into ViewData
/**
* ViewData Builder Contract (Static)
*
* @param dto - API Transport DTO (JSON-serializable)
* @returns ViewData for template
* TDTO is constrained to object to ensure it is a serializable API DTO.
*
* Usage:
* export class MyViewDataBuilder {
* static build(apiDto: MyDTO): MyViewData { ... }
* }
* MyViewDataBuilder satisfies ViewDataBuilder<MyDTO, MyViewData>;
*/
build(dto: TDTO): TViewData;
export interface ViewDataBuilder<TDTO extends object, TViewData extends ViewData> {
build(apiDto: TDTO): TViewData;
}

View File

@@ -12,17 +12,21 @@
* - Must be named *ViewModelBuilder
* - Must have 'use client' directive
* - Must implement static build() method
* - Must use 'satisfies' for static type enforcement
*/
import { ViewData } from '../view-data/ViewData';
import { ViewModel } from '../view-models/ViewModel';
export interface ViewModelBuilder<TViewData extends ViewData, TViewModel extends ViewModel> {
/**
* Transform ViewData into ViewModel
/**
* ViewModel Builder Contract (Static)
*
* @param viewData - ViewData (JSON-serializable template-ready data)
* @returns ViewModel
* Usage:
* export class MyViewModelBuilder {
* static build(viewData: MyViewData): MyViewModel { ... }
* }
* MyViewModelBuilder satisfies ViewModelBuilder<MyViewData, MyViewModel>;
*/
export interface ViewModelBuilder<TViewData extends ViewData, TViewModel extends ViewModel> {
build(viewData: TViewData): TViewModel;
}

View File

@@ -1,3 +1,5 @@
import { JsonValue } from "../types/primitives";
/**
* Base interface for ViewData objects
*
@@ -8,9 +10,8 @@
* architectural rule is that these must be plain JSON objects.
*/
export interface ViewData {
[key: string]: any;
[key: string]: JsonValue;
}
/**
* Helper type to ensure a type is ViewData-compatible
*

View File

@@ -31,7 +31,7 @@ export abstract class ViewModel {
* Optional: Validate the ViewModel state
*
* Can be used to ensure the ViewModel is in a valid state
* before a Presenter converts it to ViewData.
* before it is used by the UI.
*/
validate?(): boolean;
}

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

View File

@@ -1,6 +1,6 @@
/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ba3fe0d88075dbe87d959880403a13fe16be2835774b1d9cc63b0e021c7adb34
* Spec SHA256: 8bb5f8b21b93ade00f89d1154c676e59212db89b029aa46cde0ba7ce03f04d02
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/

Some files were not shown because too many files have changed in this diff Show More