feature flags
This commit is contained in:
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,10 +67,21 @@ 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -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 && {
|
||||||
|
|||||||
79
apps/api/src/features/features.controller.test.ts
Normal file
79
apps/api/src/features/features.controller.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
18
apps/api/src/features/features.controller.ts
Normal file
18
apps/api/src/features/features.controller.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
102
apps/api/src/features/features.http.test.ts
Normal file
102
apps/api/src/features/features.http.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
8
apps/api/src/features/features.module.ts
Normal file
8
apps/api/src/features/features.module.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { FeaturesController } from './features.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [FeaturesController],
|
||||||
|
exports: [],
|
||||||
|
})
|
||||||
|
export class FeaturesModule {}
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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} />;
|
||||||
}
|
}
|
||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
|
||||||
}
|
|
||||||
|
|
||||||
const mode = process.env.NEXT_PUBLIC_GRIDPILOT_MODE;
|
useEffect(() => {
|
||||||
|
const checkFeature = async () => {
|
||||||
|
try {
|
||||||
|
const service = await FeatureFlagService.fromAPI();
|
||||||
|
setIsEnabled(service.isEnabled(feature));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking feature:', error);
|
||||||
|
setIsEnabled(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (mode === 'alpha') {
|
checkFeature();
|
||||||
return 'alpha';
|
}, [feature]);
|
||||||
}
|
|
||||||
|
|
||||||
return 'pre-launch';
|
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 => {
|
||||||
* Hook to check if in alpha mode
|
states[feature] = service.isEnabled(feature);
|
||||||
*/
|
});
|
||||||
export function useIsAlpha(): boolean {
|
|
||||||
return getClientMode() === 'alpha';
|
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;
|
||||||
}
|
}
|
||||||
1
apps/website/env.d.ts
vendored
1
apps/website/env.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
@@ -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.'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
});
|
});
|
||||||
@@ -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 () => ({
|
||||||
|
features: {
|
||||||
|
alpha_features: 'enabled'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
expect(enabledFlags).toContain('driver_profiles');
|
await FeatureFlagService.fromAPI();
|
||||||
expect(enabledFlags).toContain('team_profiles');
|
|
||||||
expect(enabledFlags).toContain('wallets');
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
expect(enabledFlags).toContain('sponsors');
|
'http://localhost:3001/features',
|
||||||
expect(enabledFlags).toContain('team_feature');
|
{ next: { revalidate: 0 } }
|
||||||
expect(enabledFlags).toContain('alpha_features');
|
);
|
||||||
expect(enabledFlags.length).toBe(6);
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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 + '/')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AcceptSponsorshipRequestInputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/ActivityItemDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AllLeaguesWithCapacityAndScoringDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AllLeaguesWithCapacityDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AllRacesFilterOptionsDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AllRacesLeagueFilterDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AllRacesListItemDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AllRacesPageDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AllRacesStatusFilterDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/ApplyPenaltyCommandDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/ApproveJoinRequestInputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/ApproveJoinRequestOutputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AuthSessionDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AuthenticatedUserDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AvailableLeagueDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AvatarDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/AwardPrizeResultDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/BillingStatsDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CompleteOnboardingInputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CompleteOnboardingOutputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CreateLeagueInputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CreateLeagueOutputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CreateLeagueScheduleRaceInputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CreateLeagueScheduleRaceOutputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CreatePaymentInputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CreatePaymentOutputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CreatePrizeResultDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CreateSponsorInputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CreateSponsorOutputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CreateTeamInputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/CreateTeamOutputDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/DashboardDriverSummaryDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('types/generated/DashboardFeedItemSummaryDTO', () => {
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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
Reference in New Issue
Block a user