Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.10.1] - 2026-05-20

### Added
- `setServiceUrl(service, url)` — runtime override of individual service URLs
- `'services'` added to `ProtectedField` union type — blocks `setServiceUrl()` when listed in `protectedFields`
- `getApiUrl(service)` now checks runtime service overrides first (priority: serviceOverrides > config services > apiBaseUrlOverride > apiBaseUrl)
- Service URL overrides are cleared on `setEnvironment()`, `loadConfig()`, and `reset()`

## [0.10.0] - 2026-05-20

### Added
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fireflyframework/core",
"version": "0.10.0",
"version": "0.10.1",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
},
Expand Down
58 changes: 57 additions & 1 deletion packages/core/src/lib/environment/environment.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const MULTI_ENV_CONFIG: EnvironmentConfig = {

const PROTECTED_CONFIG: EnvironmentConfig = {
...MULTI_ENV_CONFIG,
protectedFields: ['currentEnv', 'apiBaseUrl', 'flags'],
protectedFields: ['currentEnv', 'apiBaseUrl', 'flags', 'services'],
};

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -248,6 +248,54 @@ describe('EnvironmentService (injected config)', () => {
});
});

// --- setServiceUrl ---

describe('setServiceUrl', () => {
it('should override a service URL', () => {
service.setServiceUrl('lending', 'http://custom-lending.com');
expect(service.getApiUrl('lending')).toBe('http://custom-lending.com');
});

it('should take priority over config service URLs', () => {
expect(service.getApiUrl('lending')).toBe('http://localhost:3001');
service.setServiceUrl('lending', 'http://override.com');
expect(service.getApiUrl('lending')).toBe('http://override.com');
});

it('should allow adding a new service not in config', () => {
service.setServiceUrl('payments', 'http://payments.local');
expect(service.getApiUrl('payments')).toBe('http://payments.local');
});

it('should not affect apiBaseUrl resolution', () => {
service.setServiceUrl('lending', 'http://custom.com');
expect(service.getApiUrl()).toBe('http://localhost:3000');
});

it('should not affect other services', () => {
service.setServiceUrl('lending', 'http://custom.com');
expect(service.getApiUrl('auth')).toBe('http://localhost:4000');
});

it('should be cleared on setEnvironment', () => {
service.setServiceUrl('lending', 'http://custom.com');
service.setEnvironment('staging');
expect(service.getApiUrl('lending')).toBe('https://lending.stg.firefly.com');
});

it('should be cleared on reset', () => {
service.setServiceUrl('lending', 'http://custom.com');
service.reset();
expect(service.getApiUrl('lending')).toBe('http://localhost:3001');
});

it('should be cleared on loadConfig', () => {
service.setServiceUrl('lending', 'http://custom.com');
service.loadConfig(MULTI_ENV_CONFIG);
expect(service.getApiUrl('lending')).toBe('http://localhost:3001');
});
});

// --- reset ---

describe('reset', () => {
Expand Down Expand Up @@ -331,6 +379,14 @@ describe('EnvironmentService (protectedFields)', () => {
expect.stringContaining('flags'),
);
});

it('setServiceUrl should be blocked and log warning', () => {
service.setServiceUrl('lending', 'http://hacked.com');
expect(service.getApiUrl('lending')).toBe('http://localhost:3001');
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('services'),
);
});
});

// ---------------------------------------------------------------------------
Expand Down
35 changes: 32 additions & 3 deletions packages/core/src/lib/environment/environment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export class EnvironmentService {
/** Runtime overrides for individual flags (set by `setFlag()`). */
private readonly _flagOverrides = new Map<string, unknown>();

/** Runtime overrides for individual service URLs (set by `setServiceUrl()`). */
private readonly _serviceOverrides = new Map<string, string>();

// ---------------------------------------------------------------------------
// Public signals (readonly)
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -113,6 +116,7 @@ export class EnvironmentService {
this._originalEnv = config.default;
this._apiBaseUrlOverride = null;
this._flagOverrides.clear();
this._serviceOverrides.clear();
}

/**
Expand Down Expand Up @@ -168,9 +172,16 @@ export class EnvironmentService {
getApiUrl(service?: string): string {
const entry = this._getCurrentEntry();

// Service-specific URL takes priority
if (service && entry.services?.[service]) {
return entry.services[service];
if (service) {
// Runtime service override takes highest priority
if (this._serviceOverrides.has(service)) {
return this._serviceOverrides.get(service)!;
}

// Config service-specific URL
if (entry.services?.[service]) {
return entry.services[service];
}
}

// apiBaseUrl override takes priority over config
Expand Down Expand Up @@ -239,6 +250,7 @@ export class EnvironmentService {
this._currentEnv.set(env);
this._apiBaseUrlOverride = null;
this._flagOverrides.clear();
this._serviceOverrides.clear();
}

/**
Expand Down Expand Up @@ -271,6 +283,22 @@ export class EnvironmentService {
this._flagOverrides.set(key, value);
}

/**
* Override a single service URL at runtime.
*
* Overrides take priority over the environment's `services` map
* and over `apiBaseUrl` for the specified service.
*
* Blocked when `'services'` is in `protectedFields`.
*
* @param service - Service name
* @param url - Service URL
*/
setServiceUrl(service: string, url: string): void {
if (this._isProtected('services')) return;
this._serviceOverrides.set(service, url);
}

/**
* Reset the service to the state after the last `loadConfig()` call.
*
Expand All @@ -284,6 +312,7 @@ export class EnvironmentService {
}
this._apiBaseUrlOverride = null;
this._flagOverrides.clear();
this._serviceOverrides.clear();
}

// ---------------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/lib/environment/environment.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ export interface EnvironmentEntry {
* - `'currentEnv'` — blocks `setEnvironment()`
* - `'apiBaseUrl'` — blocks `setApiBaseUrl()`
* - `'flags'` — blocks `setFlag()`
* - `'services'` — blocks `setServiceUrl()`
*/
export type ProtectedField = 'currentEnv' | 'apiBaseUrl' | 'flags';
export type ProtectedField = 'currentEnv' | 'apiBaseUrl' | 'flags' | 'services';

// ---------------------------------------------------------------------------
// Module configuration
Expand Down