feature flags
This commit is contained in:
110
apps/api/src/config/feature-loader.test.ts
Normal file
110
apps/api/src/config/feature-loader.test.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Test file for the new feature flag configuration system
|
||||||
|
*
|
||||||
|
* Run with: npm test -- feature-loader.test.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||||
|
import { loadFeatureConfig, isFeatureEnabled, getFeatureState, getAllFeatures } from './feature-loader';
|
||||||
|
import { FlattenedFeatures, FeatureState } from './feature-types';
|
||||||
|
|
||||||
|
describe('Feature Flag Configuration', () => {
|
||||||
|
const originalEnv = process.env.NODE_ENV;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset environment if needed
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// Restore original environment
|
||||||
|
process.env.NODE_ENV = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadFeatureConfig', () => {
|
||||||
|
it('should load development config when NODE_ENV=development', async () => {
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
const result = await loadFeatureConfig();
|
||||||
|
|
||||||
|
expect(result.loadedFrom).toBe('config-file');
|
||||||
|
expect(result.features).toBeDefined();
|
||||||
|
expect(result.features['sponsors.portal']).toBe('enabled');
|
||||||
|
expect(result.features['admin.dashboard']).toBe('enabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load test config when NODE_ENV=test', async () => {
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
const result = await loadFeatureConfig();
|
||||||
|
|
||||||
|
expect(result.loadedFrom).toBe('config-file');
|
||||||
|
expect(result.features['sponsors.portal']).toBe('enabled');
|
||||||
|
expect(result.features['admin.dashboard']).toBe('enabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load production config when NODE_ENV=production', async () => {
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
const result = await loadFeatureConfig();
|
||||||
|
|
||||||
|
expect(result.loadedFrom).toBe('config-file');
|
||||||
|
expect(result.features['sponsors.portal']).toBe('enabled');
|
||||||
|
expect(result.features['sponsors.management']).toBe('disabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should handle invalid environment', async () => {
|
||||||
|
process.env.NODE_ENV = 'invalid-env';
|
||||||
|
|
||||||
|
await expect(loadFeatureConfig()).rejects.toThrow('Invalid environment');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isFeatureEnabled', () => {
|
||||||
|
it('should return true for enabled features', () => {
|
||||||
|
const features: FlattenedFeatures = { 'sponsors.portal': 'enabled' };
|
||||||
|
expect(isFeatureEnabled(features, 'sponsors.portal')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for disabled features', () => {
|
||||||
|
const features: FlattenedFeatures = { 'sponsors.portal': 'disabled' };
|
||||||
|
expect(isFeatureEnabled(features, 'sponsors.portal')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-existent features', () => {
|
||||||
|
const features: FlattenedFeatures = { 'sponsors.portal': 'enabled' };
|
||||||
|
expect(isFeatureEnabled(features, 'nonexistent.feature')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFeatureState', () => {
|
||||||
|
it('should return correct state for existing features', () => {
|
||||||
|
const features: FlattenedFeatures = { 'sponsors.portal': 'coming_soon' };
|
||||||
|
expect(getFeatureState(features, 'sponsors.portal')).toBe('coming_soon');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return disabled for non-existent features', () => {
|
||||||
|
const features: FlattenedFeatures = { 'sponsors.portal': 'enabled' };
|
||||||
|
expect(getFeatureState(features, 'nonexistent')).toBe('disabled');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllFeatures', () => {
|
||||||
|
it('should return all features for current environment', () => {
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
const features = getAllFeatures();
|
||||||
|
|
||||||
|
expect(features).toBeDefined();
|
||||||
|
expect(Object.keys(features).length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Flattening behavior', () => {
|
||||||
|
it('should flatten nested feature configurations', async () => {
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
const result = await loadFeatureConfig();
|
||||||
|
|
||||||
|
// Should flatten nested objects
|
||||||
|
expect(result.features['sponsors.portal']).toBeDefined();
|
||||||
|
expect(result.features['sponsors.dashboard']).toBeDefined();
|
||||||
|
expect(result.features['admin.dashboard']).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
129
apps/api/src/config/feature-loader.ts
Normal file
129
apps/api/src/config/feature-loader.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { featureConfig } from './features.config';
|
||||||
|
import {
|
||||||
|
FeatureFlagConfig,
|
||||||
|
FlattenedFeatures,
|
||||||
|
ConfigLoadResult,
|
||||||
|
FeatureState
|
||||||
|
} from './feature-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default configuration path relative to project root
|
||||||
|
*/
|
||||||
|
const DEFAULT_CONFIG_PATH = 'apps/api/src/config/features.config.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flattens nested feature configuration into a simple key-value map
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* Input: { sponsors: { portal: 'enabled' } }
|
||||||
|
* Output: { 'sponsors.portal': 'enabled' }
|
||||||
|
*/
|
||||||
|
function flattenFeatures(
|
||||||
|
config: Record<string, any>,
|
||||||
|
prefix: string = ''
|
||||||
|
): FlattenedFeatures {
|
||||||
|
const flattened: FlattenedFeatures = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(config)) {
|
||||||
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||||
|
|
||||||
|
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||||
|
// Recursively flatten nested objects
|
||||||
|
Object.assign(flattened, flattenFeatures(value, fullKey));
|
||||||
|
} else if (isFeatureState(value)) {
|
||||||
|
// Assign feature state
|
||||||
|
flattened[fullKey] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for FeatureState
|
||||||
|
*/
|
||||||
|
function isFeatureState(value: unknown): value is FeatureState {
|
||||||
|
return (
|
||||||
|
value === 'enabled' ||
|
||||||
|
value === 'disabled' ||
|
||||||
|
value === 'coming_soon' ||
|
||||||
|
value === 'hidden'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current environment
|
||||||
|
*/
|
||||||
|
function getEnvironment(): string {
|
||||||
|
return process.env.NODE_ENV || 'development';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that the environment exists in the config
|
||||||
|
*/
|
||||||
|
function validateEnvironment(
|
||||||
|
env: string
|
||||||
|
): env is keyof FeatureFlagConfig {
|
||||||
|
const validEnvs = ['development', 'test', 'staging', 'production'];
|
||||||
|
if (!validEnvs.includes(env)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid environment: "${env}". Valid environments: ${validEnvs.join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads feature configuration from the TypeScript config file
|
||||||
|
*/
|
||||||
|
export async function loadFeatureConfig(): Promise<ConfigLoadResult> {
|
||||||
|
const env = getEnvironment();
|
||||||
|
|
||||||
|
if (validateEnvironment(env)) {
|
||||||
|
const envConfig = featureConfig[env];
|
||||||
|
const flattened = flattenFeatures(envConfig);
|
||||||
|
|
||||||
|
return {
|
||||||
|
features: flattened,
|
||||||
|
loadedFrom: 'config-file',
|
||||||
|
configPath: DEFAULT_CONFIG_PATH,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to defaults
|
||||||
|
return {
|
||||||
|
features: {},
|
||||||
|
loadedFrom: 'defaults',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific feature state
|
||||||
|
*/
|
||||||
|
export function getFeatureState(
|
||||||
|
features: FlattenedFeatures,
|
||||||
|
featurePath: string
|
||||||
|
): FeatureState {
|
||||||
|
return features[featurePath] || 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a feature is enabled
|
||||||
|
*/
|
||||||
|
export function isFeatureEnabled(
|
||||||
|
features: FlattenedFeatures,
|
||||||
|
featurePath: string
|
||||||
|
): boolean {
|
||||||
|
return getFeatureState(features, featurePath) === 'enabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all features for debugging
|
||||||
|
*/
|
||||||
|
export function getAllFeatures(): FlattenedFeatures {
|
||||||
|
const env = getEnvironment();
|
||||||
|
if (validateEnvironment(env)) {
|
||||||
|
return flattenFeatures(featureConfig[env]);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
53
apps/api/src/config/feature-types.ts
Normal file
53
apps/api/src/config/feature-types.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Feature Flag Configuration Types
|
||||||
|
*
|
||||||
|
* Provides type-safe configuration for feature flags across different environments
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type FeatureState = 'enabled' | 'disabled' | 'coming_soon' | 'hidden';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Individual feature configuration
|
||||||
|
*/
|
||||||
|
export interface FeatureConfig {
|
||||||
|
[featureName: string]: FeatureState | FeatureConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment-specific feature configurations
|
||||||
|
*/
|
||||||
|
export interface EnvironmentConfig {
|
||||||
|
[featureGroup: string]: FeatureConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete feature flag configuration
|
||||||
|
*/
|
||||||
|
export interface FeatureFlagConfig {
|
||||||
|
development: EnvironmentConfig;
|
||||||
|
test: EnvironmentConfig;
|
||||||
|
staging: EnvironmentConfig;
|
||||||
|
production: EnvironmentConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flattened feature map for runtime lookup
|
||||||
|
* Example: { 'sponsors.portal': 'enabled', 'admin.dashboard': 'enabled' }
|
||||||
|
*/
|
||||||
|
export type FlattenedFeatures = Record<string, FeatureState>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration loader result
|
||||||
|
*/
|
||||||
|
export interface ConfigLoadResult {
|
||||||
|
features: FlattenedFeatures;
|
||||||
|
loadedFrom: 'config-file' | 'env-var' | 'defaults';
|
||||||
|
configPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy JSON format for backward compatibility
|
||||||
|
*/
|
||||||
|
export interface LegacyFeatureConfig {
|
||||||
|
[key: string]: FeatureState | LegacyFeatureConfig;
|
||||||
|
}
|
||||||
82
apps/api/src/config/features.config.example.ts
Normal file
82
apps/api/src/config/features.config.example.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { FeatureFlagConfig } from './feature-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example feature flag configuration
|
||||||
|
*
|
||||||
|
* Copy this file to features.config.ts and customize for your needs
|
||||||
|
*
|
||||||
|
* Feature states:
|
||||||
|
* - 'enabled': Feature is fully available
|
||||||
|
* - 'disabled': Feature is turned off
|
||||||
|
* - 'coming_soon': Feature is in development, shown as "coming soon"
|
||||||
|
* - 'hidden': Feature is hidden from users
|
||||||
|
*/
|
||||||
|
export const featureConfig: FeatureFlagConfig = {
|
||||||
|
development: {
|
||||||
|
sponsors: {
|
||||||
|
portal: 'enabled',
|
||||||
|
dashboard: 'enabled',
|
||||||
|
management: 'enabled',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
dashboard: 'enabled',
|
||||||
|
userManagement: 'enabled',
|
||||||
|
analytics: 'enabled',
|
||||||
|
},
|
||||||
|
beta: {
|
||||||
|
newUI: 'enabled',
|
||||||
|
experimental: 'coming_soon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
test: {
|
||||||
|
sponsors: {
|
||||||
|
portal: 'enabled',
|
||||||
|
dashboard: 'enabled',
|
||||||
|
management: 'enabled',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
dashboard: 'enabled',
|
||||||
|
userManagement: 'enabled',
|
||||||
|
analytics: 'enabled',
|
||||||
|
},
|
||||||
|
beta: {
|
||||||
|
newUI: 'disabled',
|
||||||
|
experimental: 'disabled',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
staging: {
|
||||||
|
sponsors: {
|
||||||
|
portal: 'enabled',
|
||||||
|
dashboard: 'enabled',
|
||||||
|
management: 'enabled',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
dashboard: 'enabled',
|
||||||
|
userManagement: 'enabled',
|
||||||
|
analytics: 'enabled',
|
||||||
|
},
|
||||||
|
beta: {
|
||||||
|
newUI: 'coming_soon',
|
||||||
|
experimental: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
production: {
|
||||||
|
sponsors: {
|
||||||
|
portal: 'enabled',
|
||||||
|
dashboard: 'enabled',
|
||||||
|
management: 'disabled',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
dashboard: 'enabled',
|
||||||
|
userManagement: 'enabled',
|
||||||
|
analytics: 'disabled',
|
||||||
|
},
|
||||||
|
beta: {
|
||||||
|
newUI: 'disabled',
|
||||||
|
experimental: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
79
apps/api/src/config/features.config.ts
Normal file
79
apps/api/src/config/features.config.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { FeatureFlagConfig } from './feature-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature flag configuration for all environments
|
||||||
|
* This provides type safety, IntelliSense, and environment-specific settings
|
||||||
|
*/
|
||||||
|
export const featureConfig: FeatureFlagConfig = {
|
||||||
|
// Development environment - features for local development
|
||||||
|
development: {
|
||||||
|
sponsors: {
|
||||||
|
portal: 'enabled',
|
||||||
|
dashboard: 'enabled',
|
||||||
|
management: 'enabled',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
dashboard: 'enabled',
|
||||||
|
userManagement: 'enabled',
|
||||||
|
analytics: 'enabled',
|
||||||
|
},
|
||||||
|
beta: {
|
||||||
|
newUI: 'enabled', // Enable new UI for testing
|
||||||
|
experimental: 'coming_soon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Test environment - features for automated tests
|
||||||
|
test: {
|
||||||
|
sponsors: {
|
||||||
|
portal: 'enabled',
|
||||||
|
dashboard: 'enabled',
|
||||||
|
management: 'enabled',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
dashboard: 'enabled',
|
||||||
|
userManagement: 'enabled',
|
||||||
|
analytics: 'enabled',
|
||||||
|
},
|
||||||
|
beta: {
|
||||||
|
newUI: 'disabled',
|
||||||
|
experimental: 'disabled',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Staging environment - features for pre-production testing
|
||||||
|
staging: {
|
||||||
|
sponsors: {
|
||||||
|
portal: 'enabled',
|
||||||
|
dashboard: 'enabled',
|
||||||
|
management: 'enabled',
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
dashboard: 'enabled',
|
||||||
|
userManagement: 'enabled',
|
||||||
|
analytics: 'enabled',
|
||||||
|
},
|
||||||
|
beta: {
|
||||||
|
newUI: 'coming_soon', // Ready for testing but not fully rolled out
|
||||||
|
experimental: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Production environment - stable features only
|
||||||
|
production: {
|
||||||
|
sponsors: {
|
||||||
|
portal: 'enabled',
|
||||||
|
dashboard: 'enabled',
|
||||||
|
management: 'disabled', // Feature not ready yet
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
dashboard: 'enabled',
|
||||||
|
userManagement: 'enabled',
|
||||||
|
analytics: 'disabled', // Feature not ready yet
|
||||||
|
},
|
||||||
|
beta: {
|
||||||
|
newUI: 'disabled',
|
||||||
|
experimental: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
43
apps/api/src/config/integration.test.ts
Normal file
43
apps/api/src/config/integration.test.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Integration test to verify the feature flag system works end-to-end
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeAll } from 'vitest';
|
||||||
|
import { loadFeatureConfig, isFeatureEnabled, getFeatureState } from './feature-loader';
|
||||||
|
|
||||||
|
describe('Feature Flag Integration Test', () => {
|
||||||
|
it('should load config and provide correct feature states', async () => {
|
||||||
|
// Set test environment
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
|
const result = await loadFeatureConfig();
|
||||||
|
|
||||||
|
// Verify config loaded from file
|
||||||
|
expect(result.loadedFrom).toBe('config-file');
|
||||||
|
expect(result.configPath).toBe('apps/api/src/config/features.config.ts');
|
||||||
|
|
||||||
|
// Verify specific features from our config
|
||||||
|
expect(result.features['sponsors.portal']).toBe('enabled');
|
||||||
|
expect(result.features['sponsors.dashboard']).toBe('enabled');
|
||||||
|
expect(result.features['admin.dashboard']).toBe('enabled');
|
||||||
|
expect(result.features['beta.newUI']).toBe('disabled');
|
||||||
|
|
||||||
|
// Verify utility functions work
|
||||||
|
expect(isFeatureEnabled(result.features, 'sponsors.portal')).toBe(true);
|
||||||
|
expect(isFeatureEnabled(result.features, 'beta.newUI')).toBe(false);
|
||||||
|
expect(getFeatureState(result.features, 'sponsors.portal')).toBe('enabled');
|
||||||
|
expect(getFeatureState(result.features, 'nonexistent')).toBe('disabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with different environments', async () => {
|
||||||
|
// Test development environment
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
const devResult = await loadFeatureConfig();
|
||||||
|
expect(devResult.features['beta.newUI']).toBe('enabled'); // dev has beta enabled
|
||||||
|
|
||||||
|
// Test production environment
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
const prodResult = await loadFeatureConfig();
|
||||||
|
expect(prodResult.features['beta.newUI']).toBe('disabled'); // prod has beta disabled
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { loadFeatureConfig } from '../../config/feature-loader';
|
||||||
|
import { FlattenedFeatures } from '../../config/feature-types';
|
||||||
|
|
||||||
export type OperationalMode = 'normal' | 'maintenance' | 'test';
|
export type OperationalMode = 'normal' | 'maintenance' | 'test';
|
||||||
export type FeatureState = 'enabled' | 'disabled' | 'coming_soon' | 'hidden';
|
export type FeatureState = 'enabled' | 'disabled' | 'coming_soon' | 'hidden';
|
||||||
@@ -72,24 +74,18 @@ export class PolicyService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const anyEnvConfigured =
|
// Load from TypeScript config file
|
||||||
Boolean(process.env.GRIDPILOT_OPERATIONAL_MODE) ||
|
const configResult = await loadFeatureConfig();
|
||||||
Boolean(process.env.GRIDPILOT_FEATURES_JSON) ||
|
const raw: RawPolicySnapshot = {
|
||||||
Boolean(process.env.GRIDPILOT_MAINTENANCE_ALLOW_VIEW) ||
|
capabilities: convertFlattenedToCapabilities(configResult.features),
|
||||||
Boolean(process.env.GRIDPILOT_MAINTENANCE_ALLOW_MUTATE);
|
};
|
||||||
|
|
||||||
const raw: RawPolicySnapshot = {};
|
|
||||||
|
|
||||||
|
// Include other env vars if set
|
||||||
const operationalMode = process.env.GRIDPILOT_OPERATIONAL_MODE;
|
const operationalMode = process.env.GRIDPILOT_OPERATIONAL_MODE;
|
||||||
if (operationalMode) {
|
if (operationalMode) {
|
||||||
raw.operationalMode = operationalMode;
|
raw.operationalMode = operationalMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const capabilities = parseFeaturesJson(process.env.GRIDPILOT_FEATURES_JSON);
|
|
||||||
if (capabilities) {
|
|
||||||
raw.capabilities = capabilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
const maintenanceAllowView = parseCsvList(process.env.GRIDPILOT_MAINTENANCE_ALLOW_VIEW);
|
const maintenanceAllowView = parseCsvList(process.env.GRIDPILOT_MAINTENANCE_ALLOW_VIEW);
|
||||||
const maintenanceAllowMutate = parseCsvList(process.env.GRIDPILOT_MAINTENANCE_ALLOW_MUTATE);
|
const maintenanceAllowMutate = parseCsvList(process.env.GRIDPILOT_MAINTENANCE_ALLOW_MUTATE);
|
||||||
|
|
||||||
@@ -102,7 +98,7 @@ export class PolicyService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
raw,
|
raw,
|
||||||
loadedFrom: anyEnvConfigured ? 'env' : 'defaults',
|
loadedFrom: configResult.loadedFrom === 'config-file' ? 'file' : 'defaults',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,16 +187,6 @@ function parseFeatureState(value: unknown): FeatureState | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFeaturesJson(raw: string | undefined): unknown {
|
|
||||||
if (!raw) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw) as unknown;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCsvList(raw: string | undefined): readonly string[] | undefined {
|
function parseCsvList(raw: string | undefined): readonly string[] | undefined {
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
@@ -226,4 +212,11 @@ function normalizeStringArray(value: unknown): readonly string[] {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Helper function to convert flattened features to capabilities
|
||||||
|
function convertFlattenedToCapabilities(
|
||||||
|
flattened: FlattenedFeatures
|
||||||
|
): Record<string, FeatureState> {
|
||||||
|
return flattened as Record<string, FeatureState>;
|
||||||
}
|
}
|
||||||
1
apps/api/src/env.d.ts
vendored
1
apps/api/src/env.d.ts
vendored
@@ -20,7 +20,6 @@ declare global {
|
|||||||
GRIDPILOT_POLICY_CACHE_MS?: string;
|
GRIDPILOT_POLICY_CACHE_MS?: string;
|
||||||
GRIDPILOT_POLICY_PATH?: string;
|
GRIDPILOT_POLICY_PATH?: string;
|
||||||
GRIDPILOT_OPERATIONAL_MODE?: string;
|
GRIDPILOT_OPERATIONAL_MODE?: string;
|
||||||
GRIDPILOT_FEATURES_JSON?: string;
|
|
||||||
GRIDPILOT_MAINTENANCE_ALLOW_VIEW?: string;
|
GRIDPILOT_MAINTENANCE_ALLOW_VIEW?: string;
|
||||||
GRIDPILOT_MAINTENANCE_ALLOW_MUTATE?: string;
|
GRIDPILOT_MAINTENANCE_ALLOW_MUTATE?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ services:
|
|||||||
- GRIDPILOT_API_PERSISTENCE=postgres
|
- GRIDPILOT_API_PERSISTENCE=postgres
|
||||||
- GRIDPILOT_API_BOOTSTRAP=true
|
- GRIDPILOT_API_BOOTSTRAP=true
|
||||||
- GRIDPILOT_API_FORCE_RESEED=true
|
- GRIDPILOT_API_FORCE_RESEED=true
|
||||||
- GRIDPILOT_FEATURES_JSON={"sponsors.portal":"enabled","admin.dashboard":"enabled"}
|
|
||||||
- DATABASE_URL=postgres://gridpilot_e2e_user:gridpilot_e2e_pass@db:5432/gridpilot_e2e
|
- DATABASE_URL=postgres://gridpilot_e2e_user:gridpilot_e2e_pass@db:5432/gridpilot_e2e
|
||||||
- POSTGRES_DB=gridpilot_e2e
|
- POSTGRES_DB=gridpilot_e2e
|
||||||
- POSTGRES_USER=gridpilot_e2e_user
|
- POSTGRES_USER=gridpilot_e2e_user
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ services:
|
|||||||
- GRIDPILOT_API_PERSISTENCE=postgres
|
- GRIDPILOT_API_PERSISTENCE=postgres
|
||||||
- GRIDPILOT_API_BOOTSTRAP=true
|
- GRIDPILOT_API_BOOTSTRAP=true
|
||||||
- GRIDPILOT_API_FORCE_RESEED=true
|
- GRIDPILOT_API_FORCE_RESEED=true
|
||||||
- GRIDPILOT_FEATURES_JSON={"sponsors.portal":"enabled","admin.dashboard":"enabled"}
|
|
||||||
- DATABASE_URL=postgres://gridpilot_test_user:gridpilot_test_pass@db:5432/gridpilot_test
|
- DATABASE_URL=postgres://gridpilot_test_user:gridpilot_test_pass@db:5432/gridpilot_test
|
||||||
- POSTGRES_DB=gridpilot_test
|
- POSTGRES_DB=gridpilot_test
|
||||||
- POSTGRES_USER=gridpilot_test_user
|
- POSTGRES_USER=gridpilot_test_user
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
|
|
||||||
import { WebsiteAuthManager } from '../../shared/website/WebsiteAuthManager';
|
|
||||||
import { ConsoleErrorCapture } from '../../shared/website/ConsoleErrorCapture';
|
import { ConsoleErrorCapture } from '../../shared/website/ConsoleErrorCapture';
|
||||||
|
import { WebsiteAuthManager } from '../../shared/website/WebsiteAuthManager';
|
||||||
|
import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
|
||||||
|
|
||||||
const WEBSITE_BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000';
|
const WEBSITE_BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
@@ -538,4 +538,5 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
|||||||
expect(detailBodyText).toBeTruthy();
|
expect(detailBodyText).toBeTruthy();
|
||||||
expect(detailBodyText?.length).toBeGreaterThan(50);
|
expect(detailBodyText?.length).toBeGreaterThan(50);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user