feature flags

This commit is contained in:
2026-01-07 22:05:53 +01:00
parent 1b63fa646c
commit 606b64cec7
530 changed files with 2092 additions and 2943 deletions

View File

@@ -1,7 +1,7 @@
# ========================================== # ==========================================
# GridPilot Development Environment # GridPilot Development Environment
# ========================================== # ==========================================
# Used by `docker-compose.dev.yml` via `env_file: .env.development`. # This file is consumed by `docker-compose.dev.yml` (API, Website, Postgres).
# ------------------------------------------ # ------------------------------------------
# Runtime # Runtime
@@ -15,14 +15,9 @@ NEXT_TELEMETRY_DISABLED=1
# API persistence is inferred from DATABASE_URL by default. # API persistence is inferred from DATABASE_URL by default.
# GRIDPILOT_API_PERSISTENCE=postgres # GRIDPILOT_API_PERSISTENCE=postgres
# Force reseed on every startup in development
GRIDPILOT_API_FORCE_RESEED=1
GRIDPILOT_API_BOOTSTRAP=1
GRIDPILOT_API_PERSISTENCE=postgres
DATABASE_URL=postgres://gridpilot_user:gridpilot_dev_pass@db:5432/gridpilot_dev DATABASE_URL=postgres://gridpilot_user:gridpilot_dev_pass@db:5432/gridpilot_dev
# Postgres container vars (used by `docker-compose.dev.yml` -> `db`) # Postgres container vars (used by docker `db` service)
POSTGRES_DB=gridpilot_dev POSTGRES_DB=gridpilot_dev
POSTGRES_USER=gridpilot_user POSTGRES_USER=gridpilot_user
POSTGRES_PASSWORD=gridpilot_dev_pass POSTGRES_PASSWORD=gridpilot_dev_pass
@@ -30,16 +25,14 @@ POSTGRES_PASSWORD=gridpilot_dev_pass
# ------------------------------------------ # ------------------------------------------
# Website (Next.js) - public (exposed to browser) # Website (Next.js) - public (exposed to browser)
# ------------------------------------------ # ------------------------------------------
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
NEXT_PUBLIC_SITE_URL=http://localhost:3000 NEXT_PUBLIC_SITE_URL=http://localhost:3000
# Browser → API base URL (host port 3001 -> container port 3000) # Browser → API base URL (host port 3001 -> container port 3000)
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001 NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
# Optional links / metadata
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
NEXT_PUBLIC_X_URL=https://x.com/your-handle NEXT_PUBLIC_X_URL=https://x.com/your-handle
# Optional site/legal metadata (defaults used when unset)
# NEXT_PUBLIC_SITE_NAME=GridPilot # NEXT_PUBLIC_SITE_NAME=GridPilot
# NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com # NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com
# NEXT_PUBLIC_SPONSOR_EMAIL=sponsors@example.com # NEXT_PUBLIC_SPONSOR_EMAIL=sponsors@example.com
@@ -87,3 +80,7 @@ SCREENSHOT_ON_ERROR=true
# LOG_FILE_PATH=./logs/gridpilot # LOG_FILE_PATH=./logs/gridpilot
# LOG_MAX_FILES=7 # LOG_MAX_FILES=7
# LOG_MAX_SIZE=10m # LOG_MAX_SIZE=10m
# Start Chrome with debugging enabled:
# /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug
# Or use: npm run chrome:debug

View File

@@ -27,7 +27,6 @@ POSTGRES_PASSWORD=gridpilot_dev_pass
# ------------------------------------------ # ------------------------------------------
# Website (Next.js) - public (exposed to browser) # Website (Next.js) - public (exposed to browser)
# ------------------------------------------ # ------------------------------------------
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
NEXT_PUBLIC_SITE_URL=http://localhost:3000 NEXT_PUBLIC_SITE_URL=http://localhost:3000
# Browser → API base URL (host port 3001 -> container port 3000) # Browser → API base URL (host port 3001 -> container port 3000)

View File

@@ -39,7 +39,6 @@ REDIS_PASSWORD=CHANGE_ME_IN_PRODUCTION
# ------------------------------------------ # ------------------------------------------
# Website (Next.js) - public (exposed to browser) # Website (Next.js) - public (exposed to browser)
# ------------------------------------------ # ------------------------------------------
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
NEXT_PUBLIC_SITE_URL=https://your-domain.com NEXT_PUBLIC_SITE_URL=https://your-domain.com
# Browser → API base URL. # Browser → API base URL.

View File

@@ -31,7 +31,6 @@ REDIS_PASSWORD=CHANGE_ME
# ------------------------------------------ # ------------------------------------------
# Website (Next.js) - public (exposed to browser) # Website (Next.js) - public (exposed to browser)
# ------------------------------------------ # ------------------------------------------
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
NEXT_PUBLIC_SITE_URL=https://your-domain.com NEXT_PUBLIC_SITE_URL=https://your-domain.com
# Browser → API base URL. # Browser → API base URL.

View File

@@ -132,6 +132,15 @@
} }
} }
}, },
"/auth/signup-sponsor": {
"post": {
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/dashboard/overview": { "/dashboard/overview": {
"get": { "get": {
"responses": { "responses": {
@@ -1449,6 +1458,9 @@
"type": "string", "type": "string",
"nullable": true "nullable": true
}, },
"companyId": {
"type": "string"
},
"role": { "role": {
"type": "string" "type": "string"
} }
@@ -6013,6 +6025,29 @@
"displayName" "displayName"
] ]
}, },
"SignupSponsorParamsDTO": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
},
"displayName": {
"type": "string"
},
"companyName": {
"type": "string"
}
},
"required": [
"email",
"password",
"displayName",
"companyName"
]
},
"SponsorDashboardDTO": { "SponsorDashboardDTO": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -17,6 +17,7 @@ import { ProtestsModule } from './domain/protests/ProtestsModule';
import { RaceModule } from './domain/race/RaceModule'; import { RaceModule } from './domain/race/RaceModule';
import { SponsorModule } from './domain/sponsor/SponsorModule'; import { SponsorModule } from './domain/sponsor/SponsorModule';
import { TeamModule } from './domain/team/TeamModule'; import { TeamModule } from './domain/team/TeamModule';
import { FeaturesModule } from './features/features.module';
import { getApiPersistence, getEnableBootstrap } from './env'; import { getApiPersistence, getEnableBootstrap } from './env';
import { RequestContextMiddleware } from './shared/http/RequestContext'; import { RequestContextMiddleware } from './shared/http/RequestContext';
@@ -46,6 +47,7 @@ const ENABLE_BOOTSTRAP = getEnableBootstrap();
PaymentsModule, PaymentsModule,
PolicyModule, PolicyModule,
AdminModule, AdminModule,
FeaturesModule,
], ],
}) })
export class AppModule implements NestModule { export class AppModule implements NestModule {

View File

@@ -27,8 +27,20 @@ describe('Feature Flag Configuration', () => {
expect(result.loadedFrom).toBe('config-file'); expect(result.loadedFrom).toBe('config-file');
expect(result.features).toBeDefined(); expect(result.features).toBeDefined();
// Core platform features (alpha mode - all enabled)
expect(result.features['platform.dashboard']).toBe('enabled');
expect(result.features['platform.leagues']).toBe('enabled');
// Sponsor features
expect(result.features['sponsors.portal']).toBe('enabled'); expect(result.features['sponsors.portal']).toBe('enabled');
expect(result.features['sponsors.management']).toBe('enabled');
// Admin features
expect(result.features['admin.dashboard']).toBe('enabled'); expect(result.features['admin.dashboard']).toBe('enabled');
// Beta features (development has them enabled for testing)
expect(result.features['beta.newUI']).toBe('enabled');
}); });
it('should load test config when NODE_ENV=test', async () => { it('should load test config when NODE_ENV=test', async () => {
@@ -36,8 +48,18 @@ describe('Feature Flag Configuration', () => {
const result = await loadFeatureConfig(); const result = await loadFeatureConfig();
expect(result.loadedFrom).toBe('config-file'); expect(result.loadedFrom).toBe('config-file');
// Core platform features
expect(result.features['platform.dashboard']).toBe('enabled');
// Sponsor features
expect(result.features['sponsors.portal']).toBe('enabled'); expect(result.features['sponsors.portal']).toBe('enabled');
// Admin features
expect(result.features['admin.dashboard']).toBe('enabled'); expect(result.features['admin.dashboard']).toBe('enabled');
// Beta features (disabled in test)
expect(result.features['beta.newUI']).toBe('disabled');
}); });
it('should load production config when NODE_ENV=production', async () => { it('should load production config when NODE_ENV=production', async () => {
@@ -45,11 +67,22 @@ describe('Feature Flag Configuration', () => {
const result = await loadFeatureConfig(); const result = await loadFeatureConfig();
expect(result.loadedFrom).toBe('config-file'); expect(result.loadedFrom).toBe('config-file');
// Core platform features (stable)
expect(result.features['platform.dashboard']).toBe('enabled');
// Sponsor features (gradual rollout - management disabled)
expect(result.features['sponsors.portal']).toBe('enabled'); expect(result.features['sponsors.portal']).toBe('enabled');
expect(result.features['sponsors.management']).toBe('disabled'); expect(result.features['sponsors.management']).toBe('disabled');
// Admin features (analytics disabled)
expect(result.features['admin.dashboard']).toBe('enabled');
expect(result.features['admin.analytics']).toBe('disabled');
// Beta features (disabled in production)
expect(result.features['beta.newUI']).toBe('disabled');
}); });
it('should handle invalid environment', async () => { it('should handle invalid environment', async () => {
process.env.NODE_ENV = 'invalid-env'; process.env.NODE_ENV = 'invalid-env';
@@ -107,4 +140,24 @@ describe('Feature Flag Configuration', () => {
expect(result.features['admin.dashboard']).toBeDefined(); expect(result.features['admin.dashboard']).toBeDefined();
}); });
}); });
describe('Feature state differences', () => {
it('should correctly handle all feature states', async () => {
process.env.NODE_ENV = 'staging';
const result = await loadFeatureConfig();
// 'enabled' - fully available
expect(result.features['platform.dashboard']).toBe('enabled');
// 'disabled' - turned off completely
expect(result.features['beta.experimental']).toBe('hidden');
// 'coming_soon' - visible but not available
expect(result.features['sponsors.management']).toBe('coming_soon');
expect(result.features['beta.newUI']).toBe('coming_soon');
// 'hidden' - completely invisible
expect(result.features['beta.experimental']).toBe('hidden');
});
});
}); });

View File

@@ -2,6 +2,12 @@
* Feature Flag Configuration Types * Feature Flag Configuration Types
* *
* Provides type-safe configuration for feature flags across different environments * Provides type-safe configuration for feature flags across different environments
*
* FEATURE STATES:
* - 'enabled': Feature is fully available to users
* - 'disabled': Feature is turned off (not visible/functional)
* - 'coming_soon': Feature is visible but not yet available (shows "coming soon")
* - 'hidden': Feature is completely invisible (internal/experimental only)
*/ */
export type FeatureState = 'enabled' | 'disabled' | 'coming_soon' | 'hidden'; export type FeatureState = 'enabled' | 'disabled' | 'coming_soon' | 'hidden';

View File

@@ -2,33 +2,121 @@ import { FeatureFlagConfig } from './feature-types';
/** /**
* Feature flag configuration for all environments * Feature flag configuration for all environments
* This provides type safety, IntelliSense, and environment-specific settings *
* ARCHITECTURE: API-Driven Features
* - All feature control comes from the API /features endpoint
* - FeatureFlagService fetches and caches features
* - Components use FeatureFlagService or ModeGuard for conditional rendering
*
* FEATURE STATES - DETAILED EXPLANATION:
*
* 'enabled' = Feature is fully available to users
* - Visible in UI
* - Fully functional
* - No restrictions
* - Example: "Users can create leagues"
*
* 'disabled' = Feature is turned off for everyone
* - Not visible in UI (or shown as unavailable)
* - Non-functional
* - Users cannot access it
* - Use when: Feature is broken, not ready, or intentionally removed
* - Example: "Sponsor management disabled due to bug"
*
* 'coming_soon' = Feature is visible but not yet available
* - Visible in UI with "Coming Soon" badge
* - Shows users what's coming
* - Builds anticipation
* - Use when: Feature is in development, users should know about it
* - Example: "New UI coming soon" - users see it but can't use it
*
* 'hidden' = Feature is completely invisible
* - Not shown in UI at all
* - No user knows it exists
* - Use when: Feature is experimental, internal-only, or not ready for ANY visibility
* - Example: "Experimental AI feature" - only devs know it exists
*
* DECISION TREE:
* - Should users see this feature exists?
* - NO → 'hidden' or 'disabled'
* - YES → Should they be able to use it?
* - NO → 'coming_soon'
* - YES → 'enabled'
*
* REAL-WORLD EXAMPLES:
* - 'enabled': Dashboard, leagues, teams (core features working)
* - 'disabled': Sponsor management (broken, don't show anything)
* - 'coming_soon': New UI redesign (users see "coming soon" banner)
* - 'hidden': Experimental chat feature (internal testing only)
*/ */
export const featureConfig: FeatureFlagConfig = { export const featureConfig: FeatureFlagConfig = {
// Development environment - features for local development // Development environment - all features enabled for testing
development: { development: {
// Core platform features
platform: {
dashboard: 'enabled',
leagues: 'enabled',
teams: 'enabled',
drivers: 'enabled',
races: 'enabled',
leaderboards: 'enabled',
},
// Authentication & onboarding
auth: {
signup: 'enabled',
login: 'enabled',
forgotPassword: 'enabled',
resetPassword: 'enabled',
},
onboarding: {
wizard: 'enabled',
},
// Sponsor features
sponsors: { sponsors: {
portal: 'enabled', portal: 'enabled',
dashboard: 'enabled', dashboard: 'enabled',
management: 'enabled', management: 'enabled',
campaigns: 'enabled',
billing: 'enabled',
}, },
// Admin features
admin: { admin: {
dashboard: 'enabled', dashboard: 'enabled',
userManagement: 'enabled', userManagement: 'enabled',
analytics: 'enabled', analytics: 'enabled',
}, },
// Beta features for testing
beta: { beta: {
newUI: 'enabled', // Enable new UI for testing newUI: 'enabled',
experimental: 'coming_soon', experimental: 'coming_soon',
}, },
}, },
// Test environment - features for automated tests // Test environment - all features enabled for automated tests
test: { test: {
platform: {
dashboard: 'enabled',
leagues: 'enabled',
teams: 'enabled',
drivers: 'enabled',
races: 'enabled',
leaderboards: 'enabled',
},
auth: {
signup: 'enabled',
login: 'enabled',
forgotPassword: 'enabled',
resetPassword: 'enabled',
},
onboarding: {
wizard: 'enabled',
},
sponsors: { sponsors: {
portal: 'enabled', portal: 'enabled',
dashboard: 'enabled', dashboard: 'enabled',
management: 'enabled', management: 'enabled',
campaigns: 'enabled',
billing: 'enabled',
}, },
admin: { admin: {
dashboard: 'enabled', dashboard: 'enabled',
@@ -41,18 +129,42 @@ export const featureConfig: FeatureFlagConfig = {
}, },
}, },
// Staging environment - features for pre-production testing // Staging environment - controlled feature rollout
staging: { staging: {
// Core platform features
platform: {
dashboard: 'enabled',
leagues: 'enabled',
teams: 'enabled',
drivers: 'enabled',
races: 'enabled',
leaderboards: 'enabled',
},
// Authentication & onboarding
auth: {
signup: 'enabled',
login: 'enabled',
forgotPassword: 'enabled',
resetPassword: 'enabled',
},
onboarding: {
wizard: 'enabled',
},
// Sponsor features (gradual rollout)
sponsors: { sponsors: {
portal: 'enabled', portal: 'enabled',
dashboard: 'enabled', dashboard: 'enabled',
management: 'enabled', management: 'coming_soon', // Ready for testing but not fully rolled out
campaigns: 'enabled',
billing: 'enabled',
}, },
// Admin features
admin: { admin: {
dashboard: 'enabled', dashboard: 'enabled',
userManagement: 'enabled', userManagement: 'enabled',
analytics: 'enabled', analytics: 'enabled',
}, },
// Beta features (controlled rollout)
beta: { beta: {
newUI: 'coming_soon', // Ready for testing but not fully rolled out newUI: 'coming_soon', // Ready for testing but not fully rolled out
experimental: 'hidden', experimental: 'hidden',
@@ -61,18 +173,42 @@ export const featureConfig: FeatureFlagConfig = {
// Production environment - stable features only // Production environment - stable features only
production: { production: {
// Core platform features (stable)
platform: {
dashboard: 'enabled',
leagues: 'enabled',
teams: 'enabled',
drivers: 'enabled',
races: 'enabled',
leaderboards: 'enabled',
},
// Authentication & onboarding (stable)
auth: {
signup: 'enabled',
login: 'enabled',
forgotPassword: 'enabled',
resetPassword: 'enabled',
},
onboarding: {
wizard: 'enabled',
},
// Sponsor features (gradual rollout)
sponsors: { sponsors: {
portal: 'enabled', portal: 'enabled',
dashboard: 'enabled', dashboard: 'enabled',
management: 'disabled', // Feature not ready yet management: 'disabled', // Feature not ready yet
campaigns: 'enabled',
billing: 'enabled',
}, },
// Admin features (stable)
admin: { admin: {
dashboard: 'enabled', dashboard: 'enabled',
userManagement: 'enabled', userManagement: 'enabled',
analytics: 'disabled', // Feature not ready yet analytics: 'disabled', // Feature not ready yet
}, },
// Beta features (controlled rollout)
beta: { beta: {
newUI: 'disabled', newUI: 'disabled', // Not ready for production
experimental: 'hidden', experimental: 'hidden',
}, },
}, },

View File

@@ -16,28 +16,87 @@ describe('Feature Flag Integration Test', () => {
expect(result.loadedFrom).toBe('config-file'); expect(result.loadedFrom).toBe('config-file');
expect(result.configPath).toBe('apps/api/src/config/features.config.ts'); expect(result.configPath).toBe('apps/api/src/config/features.config.ts');
// Verify specific features from our config // Verify core platform features (new structure)
expect(result.features['platform.dashboard']).toBe('enabled');
expect(result.features['platform.leagues']).toBe('enabled');
expect(result.features['platform.teams']).toBe('enabled');
// Verify auth features
expect(result.features['auth.signup']).toBe('enabled');
expect(result.features['auth.login']).toBe('enabled');
// Verify sponsor features (expanded)
expect(result.features['sponsors.portal']).toBe('enabled'); expect(result.features['sponsors.portal']).toBe('enabled');
expect(result.features['sponsors.dashboard']).toBe('enabled'); expect(result.features['sponsors.dashboard']).toBe('enabled');
expect(result.features['sponsors.campaigns']).toBe('enabled');
// Verify admin features
expect(result.features['admin.dashboard']).toBe('enabled'); expect(result.features['admin.dashboard']).toBe('enabled');
expect(result.features['admin.userManagement']).toBe('enabled');
// Verify beta features (controlled in test)
expect(result.features['beta.newUI']).toBe('disabled'); expect(result.features['beta.newUI']).toBe('disabled');
expect(result.features['beta.experimental']).toBe('disabled');
// Verify utility functions work // Verify utility functions work
expect(isFeatureEnabled(result.features, 'sponsors.portal')).toBe(true); expect(isFeatureEnabled(result.features, 'platform.dashboard')).toBe(true);
expect(isFeatureEnabled(result.features, 'beta.newUI')).toBe(false); expect(isFeatureEnabled(result.features, 'beta.newUI')).toBe(false);
expect(getFeatureState(result.features, 'sponsors.portal')).toBe('enabled'); expect(getFeatureState(result.features, 'platform.dashboard')).toBe('enabled');
expect(getFeatureState(result.features, 'nonexistent')).toBe('disabled'); expect(getFeatureState(result.features, 'nonexistent')).toBe('disabled');
}); });
it('should work with different environments', async () => { it('should work with different environments', async () => {
// Test development environment // Test development environment - alpha mode (all enabled)
process.env.NODE_ENV = 'development'; process.env.NODE_ENV = 'development';
const devResult = await loadFeatureConfig(); const devResult = await loadFeatureConfig();
expect(devResult.features['beta.newUI']).toBe('enabled'); // dev has beta enabled expect(devResult.features['beta.newUI']).toBe('enabled');
expect(devResult.features['platform.dashboard']).toBe('enabled');
expect(devResult.features['sponsors.management']).toBe('enabled');
// Test production environment // Test production environment - beta mode (controlled rollout)
process.env.NODE_ENV = 'production'; process.env.NODE_ENV = 'production';
const prodResult = await loadFeatureConfig(); const prodResult = await loadFeatureConfig();
expect(prodResult.features['beta.newUI']).toBe('disabled'); // prod has beta disabled expect(prodResult.features['beta.newUI']).toBe('disabled');
expect(prodResult.features['platform.dashboard']).toBe('enabled');
expect(prodResult.features['sponsors.management']).toBe('disabled'); // Not ready yet
expect(prodResult.features['admin.analytics']).toBe('disabled'); // Not ready yet
});
it('should handle the new two-tier architecture correctly', async () => {
// Development should have all platform features enabled (alpha mode)
process.env.NODE_ENV = 'development';
const devResult = await loadFeatureConfig();
// All core platform features should be enabled
expect(devResult.features['platform.dashboard']).toBe('enabled');
expect(devResult.features['platform.leagues']).toBe('enabled');
expect(devResult.features['platform.teams']).toBe('enabled');
expect(devResult.features['platform.drivers']).toBe('enabled');
expect(devResult.features['platform.races']).toBe('enabled');
expect(devResult.features['platform.leaderboards']).toBe('enabled');
// All auth features should be enabled
expect(devResult.features['auth.signup']).toBe('enabled');
expect(devResult.features['auth.login']).toBe('enabled');
expect(devResult.features['auth.forgotPassword']).toBe('enabled');
expect(devResult.features['auth.resetPassword']).toBe('enabled');
// Onboarding should be enabled
expect(devResult.features['onboarding.wizard']).toBe('enabled');
});
it('should return flattened features with expected structure', async () => {
process.env.NODE_ENV = 'test';
const result = await loadFeatureConfig();
// Verify the result has the expected shape for API response
expect(result).toHaveProperty('features');
expect(result).toHaveProperty('loadedFrom');
expect(result).toHaveProperty('configPath');
// Verify features is a flat object with dot-notation keys
expect(typeof result.features).toBe('object');
expect(result.features['platform.dashboard']).toBeDefined();
expect(result.features['sponsors.portal']).toBeDefined();
}); });
}); });

View File

@@ -40,7 +40,7 @@ export class RaceDetailPresenter implements UseCaseOutputPort<GetRaceDetailResul
track: output.race.track, track: output.race.track,
car: output.race.car, car: output.race.car,
scheduledAt: output.race.scheduledAt.toISOString(), scheduledAt: output.race.scheduledAt.toISOString(),
sessionType: output.race.sessionType.toString(), sessionType: output.race.sessionType.props,
status: output.race.status.toString(), status: output.race.status.toString(),
strengthOfField: output.race.strengthOfField?.toNumber() ?? null, strengthOfField: output.race.strengthOfField?.toNumber() ?? null,
...(output.race.registeredCount !== undefined && { ...(output.race.registeredCount !== undefined && {

View File

@@ -0,0 +1,79 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { FeaturesController } from './features.controller';
import type { FlattenedFeatures, ConfigLoadResult } from '../config/feature-types';
// Mock the feature-loader module
vi.mock('../config/feature-loader', () => ({
loadFeatureConfig: vi.fn(),
}));
import { loadFeatureConfig } from '../config/feature-loader';
describe('FeaturesController', () => {
let controller: FeaturesController;
beforeEach(() => {
controller = new FeaturesController();
vi.clearAllMocks();
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
it('should return features with correct shape', async () => {
const mockFeatures: FlattenedFeatures = {
'platform.dashboard': 'enabled',
'platform.leagues': 'enabled',
'sponsors.portal': 'enabled',
};
const mockResult: ConfigLoadResult = {
features: mockFeatures,
loadedFrom: 'config-file',
configPath: 'apps/api/src/config/features.config.ts',
};
vi.mocked(loadFeatureConfig).mockResolvedValue(mockResult);
const result = await controller.getFeatures();
expect(loadFeatureConfig).toHaveBeenCalledTimes(1);
expect(result).toHaveProperty('features');
expect(result).toHaveProperty('loadedFrom');
expect(result).toHaveProperty('timestamp');
expect(result.features).toEqual(mockFeatures);
expect(result.loadedFrom).toBe('config-file');
expect(result.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
});
it('should handle empty features', async () => {
const mockResult: ConfigLoadResult = {
features: {},
loadedFrom: 'defaults',
};
vi.mocked(loadFeatureConfig).mockResolvedValue(mockResult);
const result = await controller.getFeatures();
expect(result.features).toEqual({});
expect(result.loadedFrom).toBe('defaults');
expect(typeof result.timestamp).toBe('string');
});
it('should include timestamp as ISO string', async () => {
const mockResult: ConfigLoadResult = {
features: { 'test.feature': 'enabled' as const },
loadedFrom: 'config-file',
};
vi.mocked(loadFeatureConfig).mockResolvedValue(mockResult);
const result = await controller.getFeatures();
const timestamp = new Date(result.timestamp);
expect(timestamp instanceof Date).toBe(true);
expect(timestamp.toISOString()).toBe(result.timestamp);
});
});

View File

@@ -0,0 +1,18 @@
import { Controller, Get } from '@nestjs/common';
import { Public } from '../domain/auth/Public';
import { loadFeatureConfig } from '../config/feature-loader';
@Controller('features')
export class FeaturesController {
@Public()
@Get()
async getFeatures() {
const result = await loadFeatureConfig();
return {
features: result.features,
loadedFrom: result.loadedFrom,
timestamp: new Date().toISOString(),
};
}
}

View File

@@ -0,0 +1,102 @@
import 'reflect-metadata';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { FeaturesModule } from './features.module';
// Mock the feature-loader to control test behavior
vi.mock('../config/feature-loader', () => ({
loadFeatureConfig: vi.fn(),
}));
import { loadFeatureConfig } from '../config/feature-loader';
describe('Features HTTP Endpoint', () => {
let app: any;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [FeaturesModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
afterEach(async () => {
await app?.close();
vi.clearAllMocks();
});
it('GET /features returns 200 with correct shape', async () => {
vi.mocked(loadFeatureConfig).mockResolvedValue({
features: {
'platform.dashboard': 'enabled',
'platform.leagues': 'enabled',
'sponsors.portal': 'enabled',
},
loadedFrom: 'config-file',
configPath: 'apps/api/src/config/features.config.ts',
});
const response = await request(app.getHttpServer()).get('/features').expect(200);
expect(response.body).toHaveProperty('features');
expect(response.body).toHaveProperty('loadedFrom');
expect(response.body).toHaveProperty('timestamp');
expect(response.body.features).toEqual({
'platform.dashboard': 'enabled',
'platform.leagues': 'enabled',
'sponsors.portal': 'enabled',
});
expect(response.body.loadedFrom).toBe('config-file');
expect(response.body.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
});
it('GET /features returns 200 with empty features', async () => {
vi.mocked(loadFeatureConfig).mockResolvedValue({
features: {},
loadedFrom: 'defaults',
});
const response = await request(app.getHttpServer()).get('/features').expect(200);
expect(response.body.features).toEqual({});
expect(response.body.loadedFrom).toBe('defaults');
expect(typeof response.body.timestamp).toBe('string');
});
it('GET /features works without authentication', async () => {
vi.mocked(loadFeatureConfig).mockResolvedValue({
features: { 'test.feature': 'enabled' as const },
loadedFrom: 'config-file',
});
// Should work without any auth headers
const response = await request(app.getHttpServer()).get('/features').expect(200);
expect(response.body.features).toHaveProperty('test.feature');
});
it('GET /features includes timestamp as ISO string', async () => {
const beforeRequest = new Date();
vi.mocked(loadFeatureConfig).mockResolvedValue({
features: { 'platform.dashboard': 'enabled' as const },
loadedFrom: 'config-file',
});
const response = await request(app.getHttpServer()).get('/features').expect(200);
const afterRequest = new Date();
const timestamp = new Date(response.body.timestamp);
expect(timestamp instanceof Date).toBe(true);
expect(timestamp.toISOString()).toBe(response.body.timestamp);
// Timestamp should be within the request window
expect(timestamp >= beforeRequest).toBe(true);
expect(timestamp <= afterRequest).toBe(true);
});
});

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { FeaturesController } from './features.controller';
@Module({
controllers: [FeaturesController],
exports: [],
})
export class FeaturesModule {}

View File

@@ -1,14 +1,16 @@
# GridPilot Website Environment Variables # GridPilot Website Environment Variables
# Application Mode # Site URL (for metadata and OG tags)
# pre-launch = landing page only, no features NEXT_PUBLIC_SITE_URL=https://gridpilot.com
# alpha = full platform with all features enabled automatically
NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
# Feature Flags (Optional - only needed for custom feature selection) # API base URL (browser → API)
# When in alpha mode, all features are enabled automatically NEXT_PUBLIC_API_BASE_URL=https://gridpilot.com/api
# Use this to override or select specific features
# FEATURE_FLAGS=driver_profiles,team_profiles,wallets,sponsors,team_feature # Discord Community
# Discord invite URL for the community CTA
# Get this from: Discord Server Settings -> Invites -> Create Invite
# Example: https://discord.gg/your-invite-code
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
# Vercel KV (for email signups and rate limiting) # Vercel KV (for email signups and rate limiting)
# Get these from: https://vercel.com/dashboard -> Storage -> KV # Get these from: https://vercel.com/dashboard -> Storage -> KV
@@ -17,23 +19,15 @@ NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
# KV_REST_API_URL=your_kv_rest_api_url_here # KV_REST_API_URL=your_kv_rest_api_url_here
# KV_REST_API_TOKEN=your_kv_rest_api_token_here # KV_REST_API_TOKEN=your_kv_rest_api_token_here
# Site URL (for metadata and OG tags) # Feature Flags (Optional - for fine-grained feature control)
NEXT_PUBLIC_SITE_URL=https://gridpilot.com # Use this to enable/disable specific features
# FEATURE_FLAGS=driver_profiles,team_profiles,wallets,sponsors,team_feature
# Discord Community # Optional site metadata (defaults used when unset)
# Discord invite URL for the community CTA # NEXT_PUBLIC_SITE_NAME=GridPilot
# Get this from: Discord Server Settings -> Invites -> Create Invite # NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com
# Example: https://discord.gg/your-invite-code # NEXT_PUBLIC_SPONSOR_EMAIL=sponsors@example.com
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code # NEXT_PUBLIC_LEGAL_COMPANY_NAME=
# NEXT_PUBLIC_LEGAL_VAT_ID=
# Example configurations: # NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY=
# NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS=
# Pre-launch (default)
# NEXT_PUBLIC_GRIDPILOT_MODE=pre-launch
# Alpha with all features (recommended)
# NEXT_PUBLIC_GRIDPILOT_MODE=alpha
# Alpha with specific features only
# NEXT_PUBLIC_GRIDPILOT_MODE=alpha
# FEATURE_FLAGS=driver_profiles,wallets

View File

@@ -3,6 +3,10 @@
FROM node:20-alpine FROM node:20-alpine
# Accept build arguments
ARG NODE_ENV=production
ARG NEXT_PUBLIC_API_BASE_URL
# Install build dependencies required for SWC and sharp # Install build dependencies required for SWC and sharp
RUN apk add --no-cache \ RUN apk add --no-cache \
python3 \ python3 \
@@ -36,6 +40,11 @@ RUN npm install --include-workspace-root --no-audit --fund=false
# Copy source code # Copy source code
COPY . . COPY . .
# Set environment variables for build
ENV NODE_ENV=${NODE_ENV}
ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
ENV NEXT_TELEMETRY_DISABLED=1
# Build the website # Build the website
WORKDIR /app/apps/website WORKDIR /app/apps/website
RUN npm run build RUN npm run build

View File

@@ -1,14 +1,28 @@
import { PageWrapper } from '@/components/shared/state/PageWrapper'; import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { DriversTemplate } from '@/templates/DriversTemplate'; import { DriversTemplate } from '@/templates/DriversTemplate';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher'; import { DriverService } from '@/lib/services/drivers/DriverService';
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens'; import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import type { DriverService } from '@/lib/services/drivers/DriverService'; import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
export default async function Page() { export default async function Page() {
const data = await PageDataFetcher.fetch<DriverService, 'getDriverLeaderboard'>( // Manual dependency creation (consistent with /races and /teams)
DRIVER_SERVICE_TOKEN, const baseUrl = getWebsiteApiBaseUrl();
'getDriverLeaderboard' const logger = new ConsoleLogger();
); const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
// Create API client
const driversApiClient = new DriversApiClient(baseUrl, errorReporter, logger);
// Create service
const service = new DriverService(driversApiClient);
const data = await service.getDriverLeaderboard();
return <PageWrapper data={data} Template={DriversTemplate} />; return <PageWrapper data={data} Template={DriversTemplate} />;
} }

View File

@@ -71,8 +71,8 @@ export default async function RootLayout({
} }
} }
// Initialize feature flag service // Initialize feature flag service from API
const featureService = FeatureFlagService.fromEnv(); const featureService = await FeatureFlagService.fromAPI();
const enabledFlags = featureService.getEnabledFlags(); const enabledFlags = featureService.getEnabledFlags();
return ( return (

View File

@@ -152,7 +152,6 @@ export function DebugModeToggle({ show }: DebugModeToggleProps) {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
environment: { environment: {
mode: process.env.NODE_ENV, mode: process.env.NODE_ENV,
appMode: process.env.NEXT_PUBLIC_GRIDPILOT_MODE,
version: process.env.NEXT_PUBLIC_APP_VERSION, version: process.env.NEXT_PUBLIC_APP_VERSION,
}, },
browser: { browser: {

View File

@@ -53,7 +53,6 @@ interface ErrorStats {
}; };
environment: { environment: {
mode: string; mode: string;
appMode: string;
version?: string; version?: string;
buildTime?: string; buildTime?: string;
}; };
@@ -141,7 +140,6 @@ export function ErrorAnalyticsDashboard({
}, },
environment: { environment: {
mode: process.env.NODE_ENV || 'unknown', mode: process.env.NODE_ENV || 'unknown',
appMode: process.env.NEXT_PUBLIC_GRIDPILOT_MODE || 'pre-launch',
version: process.env.NEXT_PUBLIC_APP_VERSION, version: process.env.NEXT_PUBLIC_APP_VERSION,
buildTime: process.env.NEXT_PUBLIC_BUILD_TIME, buildTime: process.env.NEXT_PUBLIC_BUILD_TIME,
}, },
@@ -432,10 +430,6 @@ export function ErrorAnalyticsDashboard({
stats.environment.mode === 'development' ? 'text-green-400' : 'text-yellow-400' stats.environment.mode === 'development' ? 'text-green-400' : 'text-yellow-400'
}`}>{stats.environment.mode}</span> }`}>{stats.environment.mode}</span>
</div> </div>
<div className="flex justify-between">
<span className="text-gray-500">App Mode</span>
<span className="text-blue-400 font-mono">{stats.environment.appMode}</span>
</div>
{stats.environment.version && ( {stats.environment.version && (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-500">Version</span> <span className="text-gray-500">Version</span>

View File

@@ -1,47 +1,57 @@
'use client'; 'use client';
import { useState, useEffect, ReactNode } from 'react'; import { useState, useEffect, ReactNode } from 'react';
import { FeatureFlagService } from '@/lib/feature/FeatureFlagService';
/** /**
* ModeGuard - Conditional rendering component based on application mode * ModeGuard - Conditional rendering component based on feature availability
* *
* Usage: * Usage:
* <ModeGuard mode="pre-launch"> * <ModeGuard feature="platform.dashboard">
* <PreLaunchContent /> * <DashboardContent />
* </ModeGuard> * </ModeGuard>
* *
* <ModeGuard mode="alpha"> * <ModeGuard feature="alpha_features">
* <FullPlatformContent /> * <AlphaContent />
* </ModeGuard> * </ModeGuard>
*/ */
export type GuardMode = 'pre-launch' | 'alpha';
interface ModeGuardProps { interface ModeGuardProps {
mode: GuardMode; feature: string;
children: ReactNode; children: ReactNode;
fallback?: ReactNode; fallback?: ReactNode;
} }
/** /**
* Client-side mode guard component * Client-side feature guard component
* Note: For initial page load, rely on middleware for route protection * Uses API-driven feature flags instead of environment mode
* This component is for conditional UI rendering within accessible pages
*/ */
export function ModeGuard({ mode, children, fallback = null }: ModeGuardProps) { export function ModeGuard({ feature, children, fallback = null }: ModeGuardProps) {
const [isMounted, setIsMounted] = useState(false); const [isMounted, setIsMounted] = useState(false);
const [currentMode, setCurrentMode] = useState<GuardMode>('pre-launch'); const [isEnabled, setIsEnabled] = useState(false);
useEffect(() => { useEffect(() => {
setIsMounted(true); setIsMounted(true);
setCurrentMode(getClientMode());
}, []); // Check feature availability using API-driven service
const checkFeature = async () => {
try {
const service = await FeatureFlagService.fromAPI();
setIsEnabled(service.isEnabled(feature));
} catch (error) {
console.error('Error checking feature:', error);
setIsEnabled(false);
}
};
checkFeature();
}, [feature]);
if (!isMounted) { if (!isMounted) {
return <>{fallback}</>; return <>{fallback}</>;
} }
if (currentMode !== mode) { if (!isEnabled) {
return <>{fallback}</>; return <>{fallback}</>;
} }
@@ -49,40 +59,57 @@ export function ModeGuard({ mode, children, fallback = null }: ModeGuardProps) {
} }
/** /**
* Get mode on client side from injected environment variable * Hook to check feature availability in client components
* Falls back to 'pre-launch' if not available
*/ */
function getClientMode(): GuardMode { export function useFeature(feature: string): boolean {
if (typeof window === 'undefined') { const [isEnabled, setIsEnabled] = useState(false);
return 'pre-launch';
} useEffect(() => {
const checkFeature = async () => {
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE; try {
const service = await FeatureFlagService.fromAPI();
if (mode === 'alpha') { setIsEnabled(service.isEnabled(feature));
return 'alpha'; } catch (error) {
} console.error('Error checking feature:', error);
setIsEnabled(false);
return 'pre-launch'; }
};
checkFeature();
}, [feature]);
return isEnabled;
} }
/** /**
* Hook to get current mode in client components * Hook to check multiple features
*/ */
export function useAppMode(): GuardMode { export function useFeatures(features: string[]): Record<string, boolean> {
return getClientMode(); const [featureStates, setFeatureStates] = useState<Record<string, boolean>>({});
}
/** useEffect(() => {
* Hook to check if in pre-launch mode const checkFeatures = async () => {
*/ try {
export function useIsPreLaunch(): boolean { const service = await FeatureFlagService.fromAPI();
return getClientMode() === 'pre-launch'; const states: Record<string, boolean> = {};
}
features.forEach(feature => {
states[feature] = service.isEnabled(feature);
});
setFeatureStates(states);
} catch (error) {
console.error('Error checking features:', error);
const states: Record<string, boolean> = {};
features.forEach(feature => {
states[feature] = false;
});
setFeatureStates(states);
}
};
checkFeatures();
}, [features]);
/** return featureStates;
* Hook to check if in alpha mode
*/
export function useIsAlpha(): boolean {
return getClientMode() === 'alpha';
} }

View File

@@ -50,7 +50,6 @@ declare global {
NODE_ENV?: 'development' | 'production' | 'test'; NODE_ENV?: 'development' | 'production' | 'test';
// Website (public, exposed to browser) // Website (public, exposed to browser)
NEXT_PUBLIC_GRIDPILOT_MODE?: 'pre-launch' | 'alpha';
NEXT_PUBLIC_SITE_URL?: string; NEXT_PUBLIC_SITE_URL?: string;
NEXT_PUBLIC_API_BASE_URL?: string; NEXT_PUBLIC_API_BASE_URL?: string;
NEXT_PUBLIC_SITE_NAME?: string; NEXT_PUBLIC_SITE_NAME?: string;

View File

@@ -14,12 +14,15 @@ export function getWebsiteApiBaseUrl(): string {
return normalizeBaseUrl(configured); return normalizeBaseUrl(configured);
} }
// In test-like environments, check if we have any configuration at all
const isTestLike = const isTestLike =
process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'test' ||
process.env.CI === 'true' || process.env.CI === 'true' ||
process.env.DOCKER === 'true'; process.env.DOCKER === 'true';
if (isTestLike) { // If we're in a test-like environment and have NO configuration, that's an error
// But if we have some configuration (even if empty), we should use the fallback
if (isTestLike && !process.env.API_BASE_URL && !process.env.NEXT_PUBLIC_API_BASE_URL) {
throw new Error( throw new Error(
isBrowser isBrowser
? 'Missing NEXT_PUBLIC_API_BASE_URL. In Docker/CI/test we do not allow falling back to localhost.' ? 'Missing NEXT_PUBLIC_API_BASE_URL. In Docker/CI/test we do not allow falling back to localhost.'

View File

@@ -4,7 +4,6 @@ const urlOptional = z.string().url().optional();
const stringOptional = z.string().optional(); const stringOptional = z.string().optional();
const publicEnvSchema = z.object({ const publicEnvSchema = z.object({
NEXT_PUBLIC_GRIDPILOT_MODE: z.enum(['pre-launch', 'alpha']).optional(),
NEXT_PUBLIC_SITE_URL: urlOptional, NEXT_PUBLIC_SITE_URL: urlOptional,
NEXT_PUBLIC_API_BASE_URL: urlOptional, NEXT_PUBLIC_API_BASE_URL: urlOptional,

View File

@@ -42,7 +42,8 @@ export const ApiModule = new ContainerModule((options) => {
ClientClass: any, ClientClass: any,
context: any context: any
) => { ) => {
const baseUrl = context.get(CONFIG_TOKEN); const getConfig = context.get(CONFIG_TOKEN);
const baseUrl = getConfig(); // Call function to get fresh config
const errorReporter = context.get(ERROR_REPORTER_TOKEN); const errorReporter = context.get(ERROR_REPORTER_TOKEN);
const logger = context.get(LOGGER_TOKEN); const logger = context.get(LOGGER_TOKEN);

View File

@@ -28,7 +28,7 @@ export const CoreModule = new ContainerModule((options) => {
}) })
.inSingletonScope(); .inSingletonScope();
// Config // Config - bind as function to read env at runtime
bind<string>(CONFIG_TOKEN) bind<() => string>(CONFIG_TOKEN)
.toConstantValue(getWebsiteApiBaseUrl()); .toDynamicValue(() => () => getWebsiteApiBaseUrl());
}); });

View File

@@ -1,95 +1,143 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { FeatureFlagService, MockFeatureFlagService, mockFeatureFlags } from './FeatureFlagService'; import { FeatureFlagService, MockFeatureFlagService, mockFeatureFlags } from './FeatureFlagService';
describe('FeatureFlagService', () => { describe('FeatureFlagService', () => {
describe('fromEnv() with alpha mode integration', () => { describe('fromAPI()', () => {
let originalMode: string | undefined; let originalBaseUrl: string | undefined;
let originalFlags: string | undefined; let fetchMock: any;
beforeEach(() => { beforeEach(() => {
originalMode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE; originalBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
originalFlags = process.env.FEATURE_FLAGS; // Mock fetch globally
fetchMock = vi.fn();
global.fetch = fetchMock;
}); });
afterEach(() => { afterEach(() => {
if (originalMode !== undefined) { if (originalBaseUrl !== undefined) {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = originalMode; process.env.NEXT_PUBLIC_API_BASE_URL = originalBaseUrl;
} else { } else {
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE; delete process.env.NEXT_PUBLIC_API_BASE_URL;
}
if (originalFlags !== undefined) {
process.env.FEATURE_FLAGS = originalFlags;
} else {
delete process.env.FEATURE_FLAGS;
} }
vi.restoreAllMocks();
}); });
it('should enable all features when NEXT_PUBLIC_GRIDPILOT_MODE is alpha', () => { it('should fetch from API and enable flags with value "enabled"', async () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha'; process.env.NEXT_PUBLIC_API_BASE_URL = 'http://api.example.com';
const service = FeatureFlagService.fromEnv();
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
features: {
driver_profiles: 'enabled',
team_profiles: 'enabled',
wallets: 'disabled',
sponsors: 'enabled',
team_feature: 'disabled',
alpha_features: 'enabled'
}
})
});
const service = await FeatureFlagService.fromAPI();
expect(fetchMock).toHaveBeenCalledWith(
'http://api.example.com/features',
{ next: { revalidate: 0 } }
);
expect(service.isEnabled('driver_profiles')).toBe(true); expect(service.isEnabled('driver_profiles')).toBe(true);
expect(service.isEnabled('team_profiles')).toBe(true); expect(service.isEnabled('team_profiles')).toBe(true);
expect(service.isEnabled('wallets')).toBe(true);
expect(service.isEnabled('sponsors')).toBe(true); expect(service.isEnabled('sponsors')).toBe(true);
expect(service.isEnabled('team_feature')).toBe(true);
expect(service.isEnabled('alpha_features')).toBe(true); expect(service.isEnabled('alpha_features')).toBe(true);
});
it('should enable no features when NEXT_PUBLIC_GRIDPILOT_MODE is pre-launch', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'pre-launch';
const service = FeatureFlagService.fromEnv();
expect(service.isEnabled('driver_profiles')).toBe(false);
expect(service.isEnabled('team_profiles')).toBe(false);
expect(service.isEnabled('wallets')).toBe(false); expect(service.isEnabled('wallets')).toBe(false);
expect(service.isEnabled('sponsors')).toBe(false);
expect(service.isEnabled('team_feature')).toBe(false);
expect(service.isEnabled('alpha_features')).toBe(false);
});
it('should enable no features when NEXT_PUBLIC_GRIDPILOT_MODE is not set', () => {
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
const service = FeatureFlagService.fromEnv();
expect(service.isEnabled('driver_profiles')).toBe(false);
expect(service.isEnabled('team_profiles')).toBe(false);
expect(service.isEnabled('wallets')).toBe(false);
expect(service.isEnabled('sponsors')).toBe(false);
expect(service.isEnabled('team_feature')).toBe(false);
expect(service.isEnabled('alpha_features')).toBe(false);
});
it('should allow FEATURE_FLAGS to override alpha mode', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
process.env.FEATURE_FLAGS = 'driver_profiles,wallets';
const service = FeatureFlagService.fromEnv();
expect(service.isEnabled('driver_profiles')).toBe(true);
expect(service.isEnabled('wallets')).toBe(true);
expect(service.isEnabled('team_profiles')).toBe(false);
expect(service.isEnabled('sponsors')).toBe(false);
expect(service.isEnabled('team_feature')).toBe(false); expect(service.isEnabled('team_feature')).toBe(false);
}); });
it('should return correct list of enabled flags in alpha mode', () => { it('should use default localhost URL when NEXT_PUBLIC_API_BASE_URL is not set', async () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha'; delete process.env.NEXT_PUBLIC_API_BASE_URL;
const service = FeatureFlagService.fromEnv(); fetchMock.mockResolvedValueOnce({
const enabledFlags = service.getEnabledFlags(); ok: true,
json: async () => ({
expect(enabledFlags).toContain('driver_profiles'); features: {
expect(enabledFlags).toContain('team_profiles'); alpha_features: 'enabled'
expect(enabledFlags).toContain('wallets'); }
expect(enabledFlags).toContain('sponsors'); })
expect(enabledFlags).toContain('team_feature'); });
expect(enabledFlags).toContain('alpha_features');
expect(enabledFlags.length).toBe(6); await FeatureFlagService.fromAPI();
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:3001/features',
{ next: { revalidate: 0 } }
);
});
it('should return empty flags on HTTP error', async () => {
fetchMock.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error'
});
const service = await FeatureFlagService.fromAPI();
expect(service.isEnabled('any_flag')).toBe(false);
expect(service.getEnabledFlags()).toEqual([]);
});
it('should return empty flags on network error', async () => {
fetchMock.mockRejectedValueOnce(new Error('Network error'));
const service = await FeatureFlagService.fromAPI();
expect(service.isEnabled('any_flag')).toBe(false);
expect(service.getEnabledFlags()).toEqual([]);
});
it('should handle empty features object', async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({ features: {} })
});
const service = await FeatureFlagService.fromAPI();
expect(service.getEnabledFlags()).toEqual([]);
});
it('should handle malformed response', async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({})
});
const service = await FeatureFlagService.fromAPI();
expect(service.getEnabledFlags()).toEqual([]);
});
it('should ignore non-"enabled" values', async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => ({
features: {
flag1: 'enabled',
flag2: 'disabled',
flag3: 'pending',
flag4: 'ENABLED', // case sensitive
flag5: ''
}
})
});
const service = await FeatureFlagService.fromAPI();
expect(service.isEnabled('flag1')).toBe(true);
expect(service.isEnabled('flag2')).toBe(false);
expect(service.isEnabled('flag3')).toBe(false);
expect(service.isEnabled('flag4')).toBe(false);
expect(service.isEnabled('flag5')).toBe(false);
}); });
}); });

View File

@@ -1,12 +1,11 @@
/** /**
* FeatureFlagService - Manages feature flags for both server and client * FeatureFlagService - Manages feature flags for both server and client
* *
* Automatic Alpha Mode Integration: * API-Driven Integration:
* When NEXT_PUBLIC_GRIDPILOT_MODE=alpha, all features are automatically enabled. * Fetches feature flags from the API endpoint GET /features
* This eliminates the need to manually set FEATURE_FLAGS for alpha deployments. * Returns empty flags on error (secure by default)
* *
* Server: Reads from process.env.FEATURE_FLAGS (comma-separated) * Server: Fetches from API at ${NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'}/features
* OR auto-enables all features if in alpha mode
* Client: Reads from session context or provides mock implementation * Client: Reads from session context or provides mock implementation
*/ */
@@ -18,7 +17,7 @@ export class FeatureFlagService {
if (flags) { if (flags) {
this.flags = new Set(flags); this.flags = new Set(flags);
} else { } else {
// Parse from environment variable // Parse from environment variable (fallback for backward compatibility)
const flagsEnv = process.env.FEATURE_FLAGS; const flagsEnv = process.env.FEATURE_FLAGS;
this.flags = flagsEnv this.flags = flagsEnv
? new Set(flagsEnv.split(',').map(f => f.trim())) ? new Set(flagsEnv.split(',').map(f => f.trim()))
@@ -41,33 +40,44 @@ export class FeatureFlagService {
} }
/** /**
* Factory method to create service with environment flags * Factory method to create service by fetching from API
* Automatically enables all features if in alpha mode * Fetches from ${NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'}/features
* FEATURE_FLAGS can override alpha mode defaults * On error, returns empty flags (secure by default)
*/ */
static fromEnv(): FeatureFlagService { static async fromAPI(): Promise<FeatureFlagService> {
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE; const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
const flagsEnv = process.env.FEATURE_FLAGS; const url = `${baseUrl}/features`;
// If FEATURE_FLAGS is explicitly set, use it (overrides alpha mode) try {
if (flagsEnv) { // Use next: { revalidate: 0 } for Next.js server runtime
return new FeatureFlagService(); // This is equivalent to cache: 'no-store' but is the preferred Next.js convention
const response = await fetch(url, {
next: { revalidate: 0 },
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Parse JSON { features: Record<string, string> }
// Enable flags whose value is 'enabled'
const enabledFlags: string[] = [];
if (data.features && typeof data.features === 'object') {
Object.entries(data.features).forEach(([flag, value]) => {
if (value === 'enabled') {
enabledFlags.push(flag);
}
});
}
return new FeatureFlagService(enabledFlags);
} catch (error) {
// Log error but return empty flags (secure by default)
console.error('Failed to fetch feature flags from API:', error);
return new FeatureFlagService([]);
} }
// If in alpha mode, automatically enable all features
if (mode === 'alpha') {
return new FeatureFlagService([
'driver_profiles',
'team_profiles',
'wallets',
'sponsors',
'team_feature',
'alpha_features'
]);
}
// Otherwise, use FEATURE_FLAGS environment variable (empty if not set)
return new FeatureFlagService();
} }
} }

View File

@@ -35,7 +35,6 @@ export interface ReplayContext {
}>; }>;
metadata: { metadata: {
mode: string; mode: string;
appMode: string;
timestamp: string; timestamp: string;
replayId: string; replayId: string;
}; };
@@ -85,7 +84,6 @@ export class ErrorReplaySystem {
})) || [], })) || [],
metadata: { metadata: {
mode: process.env.NODE_ENV || 'unknown', mode: process.env.NODE_ENV || 'unknown',
appMode: process.env.NEXT_PUBLIC_GRIDPILOT_MODE || 'pre-launch',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
replayId, replayId,
}, },
@@ -272,7 +270,6 @@ REACT ERRORS (${replay.reactErrors.length})
METADATA METADATA
-------- --------
Mode: ${replay.metadata.mode} Mode: ${replay.metadata.mode}
App Mode: ${replay.metadata.appMode}
Original Timestamp: ${replay.metadata.timestamp} Original Timestamp: ${replay.metadata.timestamp}
`; `;
} }

View File

@@ -1,165 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { getAppMode, isPreLaunch, isAlpha, getPublicRoutes, isPublicRoute } from './mode';
describe('mode', () => {
const originalEnv = process.env;
beforeEach(() => {
vi.resetModules();
process.env = { ...originalEnv };
});
afterEach(() => {
process.env = originalEnv;
});
describe('getAppMode', () => {
it('should return "pre-launch" when NEXT_PUBLIC_GRIDPILOT_MODE is not set', () => {
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
expect(getAppMode()).toBe('pre-launch');
});
it('should return "pre-launch" when NEXT_PUBLIC_GRIDPILOT_MODE is empty', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = '';
expect(getAppMode()).toBe('pre-launch');
});
it('should return "pre-launch" when NEXT_PUBLIC_GRIDPILOT_MODE is "pre-launch"', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'pre-launch';
expect(getAppMode()).toBe('pre-launch');
});
it('should return "alpha" when NEXT_PUBLIC_GRIDPILOT_MODE is "alpha"', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
expect(getAppMode()).toBe('alpha');
});
it('should throw error in development for invalid mode', () => {
process.env.NODE_ENV = 'development';
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'invalid';
expect(() => getAppMode()).toThrow('Invalid NEXT_PUBLIC_GRIDPILOT_MODE: "invalid". Must be one of: pre-launch, alpha');
});
it('should log error and return "pre-launch" in production for invalid mode', () => {
process.env.NODE_ENV = 'production';
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'invalid';
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const result = getAppMode();
expect(consoleSpy).toHaveBeenCalledWith(
'Invalid NEXT_PUBLIC_GRIDPILOT_MODE: "invalid". Must be one of: pre-launch, alpha'
);
expect(result).toBe('pre-launch');
consoleSpy.mockRestore();
});
});
describe('isPreLaunch', () => {
it('should return true when mode is "pre-launch"', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'pre-launch';
expect(isPreLaunch()).toBe(true);
});
it('should return false when mode is "alpha"', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
expect(isPreLaunch()).toBe(false);
});
it('should return true when mode is not set', () => {
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
expect(isPreLaunch()).toBe(true);
});
});
describe('isAlpha', () => {
it('should return true when mode is "alpha"', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'alpha';
expect(isAlpha()).toBe(true);
});
it('should return false when mode is "pre-launch"', () => {
process.env.NEXT_PUBLIC_GRIDPILOT_MODE = 'pre-launch';
expect(isAlpha()).toBe(false);
});
it('should return false when mode is not set', () => {
delete process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
expect(isAlpha()).toBe(false);
});
});
describe('getPublicRoutes', () => {
it('should return an array of public routes', () => {
const routes = getPublicRoutes();
expect(Array.isArray(routes)).toBe(true);
expect(routes.length).toBeGreaterThan(0);
});
it('should include core public pages', () => {
const routes = getPublicRoutes();
expect(routes).toContain('/');
expect(routes).toContain('/leagues');
expect(routes).toContain('/drivers');
});
it('should include auth routes', () => {
const routes = getPublicRoutes();
expect(routes).toContain('/auth/login');
expect(routes).toContain('/auth/signup');
expect(routes).toContain('/api/auth/login');
});
it('should return consistent results', () => {
const routes1 = getPublicRoutes();
const routes2 = getPublicRoutes();
expect(routes1).toEqual(routes2); // Same content
expect(routes1).not.toBe(routes2); // Different references (immutable)
});
});
describe('isPublicRoute', () => {
it('should return true for exact matches', () => {
expect(isPublicRoute('/')).toBe(true);
expect(isPublicRoute('/leagues')).toBe(true);
expect(isPublicRoute('/auth/login')).toBe(true);
});
it('should return true for nested routes under public prefixes', () => {
expect(isPublicRoute('/leagues/123')).toBe(true);
expect(isPublicRoute('/leagues/create')).toBe(true);
expect(isPublicRoute('/drivers/456')).toBe(true);
expect(isPublicRoute('/teams/789')).toBe(true);
expect(isPublicRoute('/races/abc')).toBe(true);
});
it('should return false for private routes', () => {
expect(isPublicRoute('/dashboard')).toBe(false);
expect(isPublicRoute('/admin')).toBe(false);
// Note: /leagues/123/admin is actually public because it starts with /leagues/
// This is the intended behavior - all nested routes under public prefixes are public
});
it('should return true for nested routes under public prefixes', () => {
// These are all public because they start with public prefixes
expect(isPublicRoute('/leagues/123')).toBe(true);
expect(isPublicRoute('/leagues/123/admin')).toBe(true);
expect(isPublicRoute('/drivers/456')).toBe(true);
expect(isPublicRoute('/teams/789')).toBe(true);
expect(isPublicRoute('/races/abc')).toBe(true);
});
it('should return false for routes that only start with public prefix but are different', () => {
// This tests that '/leaguex' doesn't match '/leagues'
expect(isPublicRoute('/leaguex')).toBe(false);
});
it('should handle trailing slashes correctly', () => {
expect(isPublicRoute('/leagues/')).toBe(true);
expect(isPublicRoute('/drivers/')).toBe(true);
});
});
});

View File

@@ -1,120 +0,0 @@
/**
* Mode detection system for GridPilot website
*
* Controls whether the site shows pre-launch content or alpha platform
* Based on NEXT_PUBLIC_GRIDPILOT_MODE environment variable
*/
export type AppMode = 'pre-launch' | 'alpha';
const VALID_MODES: readonly AppMode[] = ['pre-launch', 'alpha'] as const;
/**
* Get the current application mode from environment variable
* Defaults to 'pre-launch' if not set or invalid
*
* @throws {Error} If mode is set but invalid (development only)
* @returns {AppMode} The current application mode
*/
export function getAppMode(): AppMode {
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
if (!mode) {
return 'pre-launch';
}
if (!isValidMode(mode)) {
const validModes = VALID_MODES.join(', ');
const error = `Invalid NEXT_PUBLIC_GRIDPILOT_MODE: "${mode}". Must be one of: ${validModes}`;
if (process.env.NODE_ENV === 'development') {
throw new Error(error);
}
console.error(error);
return 'pre-launch';
}
return mode;
}
/**
* Type guard to check if a string is a valid AppMode
*/
function isValidMode(mode: string): mode is AppMode {
return VALID_MODES.includes(mode as AppMode);
}
/**
* Check if currently in pre-launch mode
*/
export function isPreLaunch(): boolean {
return getAppMode() === 'pre-launch';
}
/**
* Check if currently in alpha mode
*/
export function isAlpha(): boolean {
return getAppMode() === 'alpha';
}
/**
* Get list of public routes that are always accessible
*/
export function getPublicRoutes(): readonly string[] {
return [
// Core public pages
'/',
// Public content routes (leagues, drivers, teams, leaderboards, races)
'/leagues',
'/drivers',
'/teams',
'/leaderboards',
'/races',
// Sponsor signup (publicly accessible)
'/sponsor/signup',
// Auth routes
'/api/signup',
'/api/auth/signup',
'/api/auth/login',
'/api/auth/forgot-password',
'/api/auth/reset-password',
'/api/auth/session',
'/api/auth/logout',
'/auth/login',
'/auth/signup',
'/auth/forgot-password',
'/auth/reset-password',
] as const;
}
/**
* Check if a route is public (accessible in all modes)
* Supports both exact matches and prefix matches for nested routes
*/
export function isPublicRoute(pathname: string): boolean {
const publicRoutes = getPublicRoutes();
// Check exact match first
if (publicRoutes.includes(pathname)) {
return true;
}
// Check prefix matches for nested routes
// e.g., '/leagues' should match '/leagues/123', '/leagues/create', etc.
const publicPrefixes = [
'/leagues',
'/drivers',
'/teams',
'/leaderboards',
'/races',
];
return publicPrefixes.some(prefix =>
pathname === prefix || pathname.startsWith(prefix + '/')
);
}

View File

@@ -15,7 +15,7 @@ export async function getHomeData() {
redirect('/dashboard'); redirect('/dashboard');
} }
const featureService = FeatureFlagService.fromEnv(); const featureService = await FeatureFlagService.fromAPI();
const isAlpha = featureService.isEnabled('alpha_features'); const isAlpha = featureService.isEnabled('alpha_features');
const discovery = await landingService.getHomeDiscovery(); const discovery = await landingService.getHomeDiscovery();

View File

@@ -1,190 +0,0 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { RaceService } from './RaceService';
import { RacesApiClient } from '../../api/races/RacesApiClient';
import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel';
import { RacesPageViewModel } from '../../view-models/RacesPageViewModel';
import { RaceStatsViewModel } from '../../view-models/RaceStatsViewModel';
import type { RaceDetailsViewModel } from '../../view-models/RaceDetailsViewModel';
describe('RaceService', () => {
let mockApiClient: Mocked<RacesApiClient>;
let service: RaceService;
beforeEach(() => {
mockApiClient = {
getDetail: vi.fn(),
getPageData: vi.fn(),
getTotal: vi.fn(),
register: vi.fn(),
withdraw: vi.fn(),
cancel: vi.fn(),
complete: vi.fn(),
reopen: vi.fn(),
} as unknown as Mocked<RacesApiClient>;
service = new RaceService(mockApiClient);
});
describe('getRaceDetail', () => {
it('should call apiClient.getDetail and return RaceDetailViewModel', async () => {
const raceId = 'race-123';
const driverId = 'driver-456';
const mockDto = {
race: { id: raceId, track: 'Test Track' },
league: { id: 'league-1', name: 'Test League' },
entryList: [],
registration: { isRegistered: true, canRegister: false },
userResult: null,
};
mockApiClient.getDetail.mockResolvedValue(mockDto as any);
const result = await service.getRaceDetail(raceId, driverId);
expect(mockApiClient.getDetail).toHaveBeenCalledWith(raceId, driverId);
expect(result).toBeInstanceOf(RaceDetailViewModel);
expect(result.race?.id).toBe(raceId);
});
it('should throw error when apiClient.getDetail fails', async () => {
const raceId = 'race-123';
const driverId = 'driver-456';
const error = new Error('API call failed');
mockApiClient.getDetail.mockRejectedValue(error);
await expect(service.getRaceDetail(raceId, driverId)).rejects.toThrow('API call failed');
});
});
describe('getRaceDetails', () => {
it('should call apiClient.getDetail and return a ViewModel-shaped object (no DTOs)', async () => {
const raceId = 'race-123';
const driverId = 'driver-456';
const mockDto = {
race: {
id: raceId,
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2023-12-31T20:00:00Z',
status: 'completed',
sessionType: 'race',
},
league: { id: 'league-1', name: 'Test League', description: 'Desc', settings: { maxDrivers: 32 } },
entryList: [],
registration: { isUserRegistered: true, canRegister: false },
userResult: null,
};
mockApiClient.getDetail.mockResolvedValue(mockDto as any);
const result: RaceDetailsViewModel = await service.getRaceDetails(raceId, driverId);
expect(mockApiClient.getDetail).toHaveBeenCalledWith(raceId, driverId);
expect(result.race?.id).toBe(raceId);
expect(result.league?.id).toBe('league-1');
expect(result.registration.isUserRegistered).toBe(true);
expect(result.canReopenRace).toBe(true);
});
});
describe('getRacesPageData', () => {
it('should call apiClient.getPageData and return RacesPageViewModel with transformed data', async () => {
const mockDto = {
races: [
{
id: 'race-1',
track: 'Monza',
car: 'Ferrari',
scheduledAt: '2023-10-01T10:00:00Z',
status: 'upcoming',
leagueId: 'league-1',
leagueName: 'Test League',
},
{
id: 'race-2',
track: 'Silverstone',
car: 'Mercedes',
scheduledAt: '2023-09-15T10:00:00Z',
status: 'completed',
leagueId: 'league-1',
leagueName: 'Test League',
},
],
};
mockApiClient.getPageData.mockResolvedValue(mockDto as any);
const result = await service.getRacesPageData();
expect(mockApiClient.getPageData).toHaveBeenCalled();
expect(result).toBeInstanceOf(RacesPageViewModel);
expect(result.upcomingRaces).toHaveLength(1);
expect(result.completedRaces).toHaveLength(1);
expect(result.totalCount).toBe(2);
expect(result.upcomingRaces[0].title).toBe('Monza - Ferrari');
expect(result.completedRaces[0].title).toBe('Silverstone - Mercedes');
});
it('should handle empty races array', async () => {
const mockDto = { races: [] };
mockApiClient.getPageData.mockResolvedValue(mockDto as any);
const result = await service.getRacesPageData();
expect(result.upcomingRaces).toHaveLength(0);
expect(result.completedRaces).toHaveLength(0);
expect(result.totalCount).toBe(0);
});
it('should throw error when apiClient.getPageData fails', async () => {
const error = new Error('API call failed');
mockApiClient.getPageData.mockRejectedValue(error);
await expect(service.getRacesPageData()).rejects.toThrow('API call failed');
});
});
describe('getRacesTotal', () => {
it('should call apiClient.getTotal and return RaceStatsViewModel', async () => {
const mockDto = { totalRaces: 42 };
mockApiClient.getTotal.mockResolvedValue(mockDto as any);
const result = await service.getRacesTotal();
expect(mockApiClient.getTotal).toHaveBeenCalled();
expect(result).toBeInstanceOf(RaceStatsViewModel);
expect(result.totalRaces).toBe(42);
expect(result.formattedTotalRaces).toBe('42');
});
it('should throw error when apiClient.getTotal fails', async () => {
const error = new Error('API call failed');
mockApiClient.getTotal.mockRejectedValue(error);
await expect(service.getRacesTotal()).rejects.toThrow('API call failed');
});
});
describe('reopenRace', () => {
it('should call apiClient.reopen with raceId', async () => {
const raceId = 'race-123';
await service.reopenRace(raceId);
expect(mockApiClient.reopen).toHaveBeenCalledWith(raceId);
});
it('should propagate errors from apiClient.reopen', async () => {
const raceId = 'race-123';
const error = new Error('API call failed');
mockApiClient.reopen.mockRejectedValue(error);
await expect(service.reopenRace(raceId)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AcceptSponsorshipRequestInputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/ActivityItemDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AllLeaguesWithCapacityAndScoringDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AllLeaguesWithCapacityDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AllRacesFilterOptionsDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AllRacesLeagueFilterDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AllRacesListItemDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AllRacesPageDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AllRacesStatusFilterDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/ApplyPenaltyCommandDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/ApproveJoinRequestInputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/ApproveJoinRequestOutputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AuthSessionDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AuthenticatedUserDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849 * Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * Do not edit manually - regenerate using: npm run api:generate-types
*/ */
@@ -11,5 +11,6 @@ export interface AuthenticatedUserDTO {
displayName: string; displayName: string;
primaryDriverId?: string; primaryDriverId?: string;
avatarUrl?: string; avatarUrl?: string;
companyId?: string;
role?: string; role?: string;
} }

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AvailableLeagueDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AvatarDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/AwardPrizeResultDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/BillingStatsDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CompleteOnboardingInputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CompleteOnboardingOutputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CreateLeagueInputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CreateLeagueOutputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CreateLeagueScheduleRaceInputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CreateLeagueScheduleRaceOutputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CreatePaymentInputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CreatePaymentOutputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CreatePrizeResultDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CreateSponsorInputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CreateSponsorOutputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CreateTeamInputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/CreateTeamOutputDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/DashboardDriverSummaryDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

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

View File

@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('types/generated/DashboardFeedItemSummaryDTO', () => {
it('should be defined', () => {
expect(true).toBe(true);
});
});

View File

@@ -1,6 +1,6 @@
/** /**
* Auto-generated DTO from OpenAPI spec * Auto-generated DTO from OpenAPI spec
* Spec SHA256: 486d4cc42e94a6bbc53e2ed3b770a221d7e7a6e41f684929fd050ca0f62b5849 * Spec SHA256: 9648d364e9e3e1dc9f9db311c176f61f57633e1d7ca8cbefee9e3fdd2f824669
* This file is generated by scripts/generate-api-types.ts * This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types * 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