diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 37c9857..fd32c70 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.11.0] - 2026-05-20 + +### Changed +- **BREAKING:** All 16 services now require explicit registration via `provideX()` factory functions +- Removed `providedIn: 'root'` from 12 services: ErrorService, FeatureFlagService, NavigationService, BreadcrumbService, PermissionService, AlertService, MasterDataService, TransportRegistry, ApiClient, AuthService, UserContextService, SessionService +- Updated 7 existing factories to register their service class (provideErrorHandling, provideFeatureFlags, provideNavigation, providePermissions, provideAlerts, provideMasterData, provideFireflyTransport) + +### Added +- `provideAuth()` — factory for AuthService registration +- `provideSession(config?)` — factory for SessionService registration (accepts optional `SessionTimeoutConfig`) +- `provideBreadcrumb()` — factory for BreadcrumbService registration +- `provideApiClient()` — factory for ApiClient registration +- `provideUserContext()` — factory for UserContextService registration + +### Migration +- Applications MUST add `provideAuth()`, `provideSession()`, `provideBreadcrumb()`, `provideApiClient()`, and `provideUserContext()` to their `app.config.ts` providers +- Tests that inject services directly MUST include the corresponding `provideX()` in `TestBed.configureTestingModule({ providers: [...] })` + ## [0.10.1] - 2026-05-20 ### Added @@ -154,7 +172,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - User context module: `UserContextService` - API runtime module: `ApiClient`, `TransportRegistry`, `HttpTransportAdapter` -[Unreleased]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.10.0...HEAD +[Unreleased]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.11.0...HEAD +[0.11.0]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.10.1...core@0.11.0 [0.10.0]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.9.0...core@0.10.0 [0.9.0]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.8.0...core@0.9.0 [0.8.0]: https://github.com/fireflyframework/firefly-frontend-framework/compare/core@0.7.2...core@0.8.0 diff --git a/packages/core/package.json b/packages/core/package.json index 579080b..fd0dcdd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@fireflyframework/core", - "version": "0.10.1", + "version": "0.11.0", "publishConfig": { "registry": "https://npm.pkg.github.com" }, diff --git a/packages/core/src/lib/alerts/alert.service.spec.ts b/packages/core/src/lib/alerts/alert.service.spec.ts index 2d0ab02..07708ad 100644 --- a/packages/core/src/lib/alerts/alert.service.spec.ts +++ b/packages/core/src/lib/alerts/alert.service.spec.ts @@ -1,12 +1,13 @@ import { TestBed } from '@angular/core/testing'; import { AlertService } from './alert.service'; +import { provideAlerts } from './provide-alerts'; describe('AlertService — toasts & banners', () => { let service: AlertService; beforeEach(() => { vi.useFakeTimers(); - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ providers: [provideAlerts()] }); service = TestBed.inject(AlertService); }); diff --git a/packages/core/src/lib/alerts/alert.service.ts b/packages/core/src/lib/alerts/alert.service.ts index 01d6988..5d943fb 100644 --- a/packages/core/src/lib/alerts/alert.service.ts +++ b/packages/core/src/lib/alerts/alert.service.ts @@ -56,7 +56,7 @@ const MAX_VISIBLE_BANNERS = 3; * // @for (toast of alerts.activeToasts(); track toast.id) { ... } * ``` */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class AlertService { private readonly _toasts = signal([]); private readonly _banners = signal([]); diff --git a/packages/core/src/lib/alerts/provide-alerts.ts b/packages/core/src/lib/alerts/provide-alerts.ts index 899e8ce..638c994 100644 --- a/packages/core/src/lib/alerts/provide-alerts.ts +++ b/packages/core/src/lib/alerts/provide-alerts.ts @@ -4,6 +4,7 @@ import { makeEnvironmentProviders, } from '@angular/core'; import { AlertConfig } from './alert.types'; +import { AlertService } from './alert.service'; /** * Injection token for optional alert configuration. @@ -36,6 +37,7 @@ export function provideAlerts( config?: AlertConfig, ): EnvironmentProviders { return makeEnvironmentProviders([ + AlertService, ...(config ? [{ provide: ALERT_CONFIG, useValue: config }] : []), ]); } diff --git a/packages/core/src/lib/api-runtime/api-client.service.spec.ts b/packages/core/src/lib/api-runtime/api-client.service.spec.ts index 0a5775a..0205f8d 100644 --- a/packages/core/src/lib/api-runtime/api-client.service.spec.ts +++ b/packages/core/src/lib/api-runtime/api-client.service.spec.ts @@ -5,6 +5,8 @@ import { TransportRegistry } from './transport/transport-registry'; import { TransportAdapter } from './transport/transport-adapter'; import { TransportError } from './transport/transport-error'; import { TransportProtocol, TransportRequest, TransportResponse } from './transport/transport-request'; +import { provideApiClient } from './provide-api-client'; +import { provideFireflyTransport } from './provide-firefly-transport'; /** Minimal mock adapter for testing */ class MockTransportAdapter extends TransportAdapter { @@ -29,7 +31,7 @@ describe('ApiClient', () => { let mockAdapter: MockTransportAdapter; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ providers: [provideApiClient(), provideFireflyTransport({ defaultProtocol: 'http', routes: [] })] }); apiClient = TestBed.inject(ApiClient); registry = TestBed.inject(TransportRegistry); diff --git a/packages/core/src/lib/api-runtime/api-client.service.ts b/packages/core/src/lib/api-runtime/api-client.service.ts index 780fec1..ac8025e 100644 --- a/packages/core/src/lib/api-runtime/api-client.service.ts +++ b/packages/core/src/lib/api-runtime/api-client.service.ts @@ -24,7 +24,7 @@ import { TransportError } from './transport/transport-error'; * - Retry (that's RetryInterceptor cross-protocol, G14) * - Handle auth headers (that's SecurityInterceptor) */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class ApiClient { private readonly registry = inject(TransportRegistry); diff --git a/packages/core/src/lib/api-runtime/index.ts b/packages/core/src/lib/api-runtime/index.ts index fbd112a..481fe74 100644 --- a/packages/core/src/lib/api-runtime/index.ts +++ b/packages/core/src/lib/api-runtime/index.ts @@ -2,3 +2,4 @@ export * from './transport'; export * from './adapters/http'; export { ApiClient } from './api-client.service'; export { provideFireflyTransport } from './provide-firefly-transport'; +export { provideApiClient } from './provide-api-client'; diff --git a/packages/core/src/lib/api-runtime/provide-api-client.ts b/packages/core/src/lib/api-runtime/provide-api-client.ts new file mode 100644 index 0000000..2cb0458 --- /dev/null +++ b/packages/core/src/lib/api-runtime/provide-api-client.ts @@ -0,0 +1,6 @@ +import { makeEnvironmentProviders, EnvironmentProviders } from '@angular/core'; +import { ApiClient } from './api-client.service'; + +export function provideApiClient(): EnvironmentProviders { + return makeEnvironmentProviders([ApiClient]); +} diff --git a/packages/core/src/lib/api-runtime/provide-firefly-transport.ts b/packages/core/src/lib/api-runtime/provide-firefly-transport.ts index 918cc59..f9c34ed 100644 --- a/packages/core/src/lib/api-runtime/provide-firefly-transport.ts +++ b/packages/core/src/lib/api-runtime/provide-firefly-transport.ts @@ -24,6 +24,7 @@ import { TRANSPORT_OPTIONS } from './transport/retry.interceptor'; */ export function provideFireflyTransport(config: TransportConfig): EnvironmentProviders { return makeEnvironmentProviders([ + TransportRegistry, ...(config.options ? [{ provide: TRANSPORT_OPTIONS, useValue: config.options }] : []), provideEnvironmentInitializer(() => { const registry = inject(TransportRegistry); diff --git a/packages/core/src/lib/api-runtime/transport/transport-registry.spec.ts b/packages/core/src/lib/api-runtime/transport/transport-registry.spec.ts index 99c415c..591b29f 100644 --- a/packages/core/src/lib/api-runtime/transport/transport-registry.spec.ts +++ b/packages/core/src/lib/api-runtime/transport/transport-registry.spec.ts @@ -4,6 +4,7 @@ import { TransportAdapter } from './transport-adapter'; import { TransportError } from './transport-error'; import { TransportProtocol, TransportRequest, TransportResponse } from './transport-request'; import { TransportRoute } from './transport-route'; +import { provideFireflyTransport } from '../provide-firefly-transport'; /** Minimal mock adapter for testing */ class MockTransportAdapter extends TransportAdapter { @@ -30,7 +31,7 @@ describe('TransportRegistry', () => { let registry: TransportRegistry; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ providers: [provideFireflyTransport({ defaultProtocol: 'http', routes: [] })] }); registry = TestBed.inject(TransportRegistry); }); diff --git a/packages/core/src/lib/api-runtime/transport/transport-registry.ts b/packages/core/src/lib/api-runtime/transport/transport-registry.ts index 2644b98..de3d804 100644 --- a/packages/core/src/lib/api-runtime/transport/transport-registry.ts +++ b/packages/core/src/lib/api-runtime/transport/transport-registry.ts @@ -22,7 +22,7 @@ import { TransportRoute, ResolvedTransport } from './transport-route'; * configuration and adapters are registered in the providers. * No dynamic re-routing at runtime. */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class TransportRegistry { /** Configured routes — loaded from firefly.config.yaml by provideFireflyTransport() */ private readonly routes = signal([]); diff --git a/packages/core/src/lib/auth/auth.service.spec.ts b/packages/core/src/lib/auth/auth.service.spec.ts index 6c31cef..da88ba9 100644 --- a/packages/core/src/lib/auth/auth.service.spec.ts +++ b/packages/core/src/lib/auth/auth.service.spec.ts @@ -11,6 +11,9 @@ import { AuthService } from './auth.service'; import { AuthTokens } from './auth.types'; import { SessionService } from '../session/session.service'; import { UserContextService } from '../user-context/user-context.service'; +import { provideAuth } from './provide-auth'; +import { provideSession } from '../session/provide-session'; +import { provideUserContext } from '../user-context/provide-user-context'; const API = '/api/v1/experience/security'; @@ -47,6 +50,9 @@ describe('AuthService', () => { TestBed.configureTestingModule({ providers: [ + provideAuth(), + provideSession(), + provideUserContext(), provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), ], diff --git a/packages/core/src/lib/auth/auth.service.ts b/packages/core/src/lib/auth/auth.service.ts index 6ee581c..ed859c7 100644 --- a/packages/core/src/lib/auth/auth.service.ts +++ b/packages/core/src/lib/auth/auth.service.ts @@ -21,7 +21,7 @@ const AUTH_API_BASE = '/api/v1/experience/security'; * * API base: /api/v1/experience/security (exp-security microservice) */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class AuthService { /** Reactive signal indicating whether the user is currently authenticated. */ readonly isAuthenticated = signal(false); diff --git a/packages/core/src/lib/auth/guards/auth.guard.spec.ts b/packages/core/src/lib/auth/guards/auth.guard.spec.ts index 470843b..b018cf2 100644 --- a/packages/core/src/lib/auth/guards/auth.guard.spec.ts +++ b/packages/core/src/lib/auth/guards/auth.guard.spec.ts @@ -5,6 +5,9 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'; import { Component } from '@angular/core'; import { authGuard } from './auth.guard'; import { AuthService } from '../auth.service'; +import { provideAuth } from '../provide-auth'; +import { provideSession } from '../../session/provide-session'; +import { provideUserContext } from '../../user-context/provide-user-context'; @Component({ template: '', standalone: true }) class DummyComponent {} @@ -18,6 +21,9 @@ describe('authGuard', () => { TestBed.configureTestingModule({ providers: [ + provideAuth(), + provideSession(), + provideUserContext(), provideHttpClient(), provideHttpClientTesting(), provideRouter([ diff --git a/packages/core/src/lib/auth/index.ts b/packages/core/src/lib/auth/index.ts index 81ee706..af22cc0 100644 --- a/packages/core/src/lib/auth/index.ts +++ b/packages/core/src/lib/auth/index.ts @@ -2,3 +2,4 @@ export { AuthService } from './auth.service'; export { authInterceptor } from './interceptors/auth.interceptor'; export { authGuard } from './guards/auth.guard'; export type { LoginCredentials, AuthTokens, AuthResult } from './auth.types'; +export { provideAuth } from './provide-auth'; diff --git a/packages/core/src/lib/auth/interceptors/auth.interceptor.spec.ts b/packages/core/src/lib/auth/interceptors/auth.interceptor.spec.ts index 830b160..2120fd9 100644 --- a/packages/core/src/lib/auth/interceptors/auth.interceptor.spec.ts +++ b/packages/core/src/lib/auth/interceptors/auth.interceptor.spec.ts @@ -9,6 +9,9 @@ import { provideHttpClientTesting, } from '@angular/common/http/testing'; import { authInterceptor } from './auth.interceptor'; +import { provideAuth } from '../provide-auth'; +import { provideSession } from '../../session/provide-session'; +import { provideUserContext } from '../../user-context/provide-user-context'; /** Flush Promise microtask queue so async interceptor logic completes. */ const flushMicrotasks = () => new Promise((r) => setTimeout(r, 0)); @@ -22,6 +25,9 @@ describe('authInterceptor', () => { TestBed.configureTestingModule({ providers: [ + provideAuth(), + provideSession(), + provideUserContext(), provideHttpClient(withInterceptors([authInterceptor])), provideHttpClientTesting(), ], diff --git a/packages/core/src/lib/auth/provide-auth.ts b/packages/core/src/lib/auth/provide-auth.ts new file mode 100644 index 0000000..c592003 --- /dev/null +++ b/packages/core/src/lib/auth/provide-auth.ts @@ -0,0 +1,6 @@ +import { makeEnvironmentProviders, EnvironmentProviders } from '@angular/core'; +import { AuthService } from './auth.service'; + +export function provideAuth(): EnvironmentProviders { + return makeEnvironmentProviders([AuthService]); +} diff --git a/packages/core/src/lib/error-handling/error.interceptor.spec.ts b/packages/core/src/lib/error-handling/error.interceptor.spec.ts index f1cf91a..de6c841 100644 --- a/packages/core/src/lib/error-handling/error.interceptor.spec.ts +++ b/packages/core/src/lib/error-handling/error.interceptor.spec.ts @@ -10,6 +10,7 @@ import { } from '@angular/common/http/testing'; import { errorInterceptor } from './error.interceptor'; import { ErrorService } from './error.service'; +import { provideErrorHandling } from './provide-error-handling'; describe('errorInterceptor', () => { let http: HttpClient; @@ -19,6 +20,7 @@ describe('errorInterceptor', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ + provideErrorHandling(), provideHttpClient(withInterceptors([errorInterceptor])), provideHttpClientTesting(), ], diff --git a/packages/core/src/lib/error-handling/error.service.spec.ts b/packages/core/src/lib/error-handling/error.service.spec.ts index 0669695..ef87744 100644 --- a/packages/core/src/lib/error-handling/error.service.spec.ts +++ b/packages/core/src/lib/error-handling/error.service.spec.ts @@ -2,12 +2,13 @@ import { TestBed } from '@angular/core/testing'; import { ErrorService, ERROR_HANDLING_CONFIG } from './error.service'; import { createAppError } from './app-error'; import type { AppError } from './app-error'; +import { provideErrorHandling } from './provide-error-handling'; describe('ErrorService', () => { let service: ErrorService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ providers: [provideErrorHandling()] }); service = TestBed.inject(ErrorService); }); @@ -106,7 +107,7 @@ describe('ErrorService with custom config', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ - { provide: ERROR_HANDLING_CONFIG, useValue: { maxHistorySize: 3 } }, + provideErrorHandling({ maxHistorySize: 3 }), ], }); service = TestBed.inject(ErrorService); diff --git a/packages/core/src/lib/error-handling/error.service.ts b/packages/core/src/lib/error-handling/error.service.ts index 0498e1c..dab30ae 100644 --- a/packages/core/src/lib/error-handling/error.service.ts +++ b/packages/core/src/lib/error-handling/error.service.ts @@ -35,7 +35,7 @@ export const ERROR_HANDLING_CONFIG = new InjectionToken<{ maxHistorySize?: numbe * this.errors.clearError(); * ``` */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class ErrorService { private readonly config = inject(ERROR_HANDLING_CONFIG, { optional: true }); diff --git a/packages/core/src/lib/error-handling/firefly-error-handler.spec.ts b/packages/core/src/lib/error-handling/firefly-error-handler.spec.ts index 25b7381..0a02454 100644 --- a/packages/core/src/lib/error-handling/firefly-error-handler.spec.ts +++ b/packages/core/src/lib/error-handling/firefly-error-handler.spec.ts @@ -2,6 +2,7 @@ import { TestBed } from '@angular/core/testing'; import { HttpErrorResponse } from '@angular/common/http'; import { FireflyErrorHandler } from './firefly-error-handler'; import { ErrorService } from './error.service'; +import { provideErrorHandling } from './provide-error-handling'; describe('FireflyErrorHandler', () => { let handler: FireflyErrorHandler; @@ -10,7 +11,7 @@ describe('FireflyErrorHandler', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [FireflyErrorHandler], + providers: [provideErrorHandling(), FireflyErrorHandler], }); handler = TestBed.inject(FireflyErrorHandler); diff --git a/packages/core/src/lib/error-handling/provide-error-handling.ts b/packages/core/src/lib/error-handling/provide-error-handling.ts index 9656621..68d0627 100644 --- a/packages/core/src/lib/error-handling/provide-error-handling.ts +++ b/packages/core/src/lib/error-handling/provide-error-handling.ts @@ -4,7 +4,7 @@ import { makeEnvironmentProviders, } from '@angular/core'; import type { ErrorHandlingConfig } from './app-error'; -import { ERROR_HANDLING_CONFIG } from './error.service'; +import { ErrorService, ERROR_HANDLING_CONFIG } from './error.service'; import { FireflyErrorHandler } from './firefly-error-handler'; /** @@ -36,6 +36,7 @@ export function provideErrorHandling( config?: ErrorHandlingConfig, ): EnvironmentProviders { return makeEnvironmentProviders([ + ErrorService, { provide: ErrorHandler, useClass: FireflyErrorHandler }, ...(config ? [{ provide: ERROR_HANDLING_CONFIG, useValue: config }] : []), ]); diff --git a/packages/core/src/lib/feature-flags/feature-flag.directive.spec.ts b/packages/core/src/lib/feature-flags/feature-flag.directive.spec.ts index 3a11ab3..4295880 100644 --- a/packages/core/src/lib/feature-flags/feature-flag.directive.spec.ts +++ b/packages/core/src/lib/feature-flags/feature-flag.directive.spec.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FfFeatureFlagDirective } from './feature-flag.directive'; import { FeatureFlagService } from './feature-flag.service'; +import { provideFeatureFlags } from './provide-feature-flags'; @Component({ template: `Visible`, @@ -17,6 +18,7 @@ describe('FfFeatureFlagDirective', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [TestHostComponent], + providers: [provideFeatureFlags()], }); service = TestBed.inject(FeatureFlagService); diff --git a/packages/core/src/lib/feature-flags/feature-flag.service.spec.ts b/packages/core/src/lib/feature-flags/feature-flag.service.spec.ts index fe5c514..a14f17e 100644 --- a/packages/core/src/lib/feature-flags/feature-flag.service.spec.ts +++ b/packages/core/src/lib/feature-flags/feature-flag.service.spec.ts @@ -1,11 +1,12 @@ import { TestBed } from '@angular/core/testing'; import { FeatureFlagService, FEATURE_FLAG_CONFIG } from './feature-flag.service'; +import { provideFeatureFlags } from './provide-feature-flags'; describe('FeatureFlagService', () => { let service: FeatureFlagService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ providers: [provideFeatureFlags()] }); service = TestBed.inject(FeatureFlagService); }); @@ -338,10 +339,7 @@ describe('FeatureFlagService with config', () => { it('should load defaults from injected config', () => { TestBed.configureTestingModule({ providers: [ - { - provide: FEATURE_FLAG_CONFIG, - useValue: { defaults: { 'pre-loaded': true } }, - }, + provideFeatureFlags({ defaults: { 'pre-loaded': true } }), ], }); diff --git a/packages/core/src/lib/feature-flags/feature-flag.service.ts b/packages/core/src/lib/feature-flags/feature-flag.service.ts index 4f0fc75..9ee4f94 100644 --- a/packages/core/src/lib/feature-flags/feature-flag.service.ts +++ b/packages/core/src/lib/feature-flags/feature-flag.service.ts @@ -32,7 +32,7 @@ export const FEATURE_FLAG_CONFIG = new InjectionToken( * await flags.loadFlags('firebase'); * ``` */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class FeatureFlagService { private readonly _flags = signal>(new Map()); private readonly _computedCache = new Map>(); diff --git a/packages/core/src/lib/feature-flags/provide-feature-flags.ts b/packages/core/src/lib/feature-flags/provide-feature-flags.ts index 6c5deb5..bf1c2b9 100644 --- a/packages/core/src/lib/feature-flags/provide-feature-flags.ts +++ b/packages/core/src/lib/feature-flags/provide-feature-flags.ts @@ -1,6 +1,6 @@ import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; import type { FeatureFlagConfig } from './feature-flag.types'; -import { FEATURE_FLAG_CONFIG } from './feature-flag.service'; +import { FeatureFlagService, FEATURE_FLAG_CONFIG } from './feature-flag.service'; /** * Configure the feature-flags module. @@ -27,6 +27,7 @@ export function provideFeatureFlags( config?: FeatureFlagConfig, ): EnvironmentProviders { return makeEnvironmentProviders([ + FeatureFlagService, ...(config ? [{ provide: FEATURE_FLAG_CONFIG, useValue: config }] : []), diff --git a/packages/core/src/lib/master-data/master-data.service.spec.ts b/packages/core/src/lib/master-data/master-data.service.spec.ts index 0d3c196..bad4525 100644 --- a/packages/core/src/lib/master-data/master-data.service.spec.ts +++ b/packages/core/src/lib/master-data/master-data.service.spec.ts @@ -2,12 +2,13 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { MasterDataService } from './master-data.service'; import { MasterDataSource } from './master-data.types'; +import { provideMasterData } from './provide-master-data'; describe('MasterDataService', () => { let service: MasterDataService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ providers: [provideMasterData([])] }); service = TestBed.inject(MasterDataService); }); diff --git a/packages/core/src/lib/master-data/master-data.service.ts b/packages/core/src/lib/master-data/master-data.service.ts index 3475c42..8ac2a78 100644 --- a/packages/core/src/lib/master-data/master-data.service.ts +++ b/packages/core/src/lib/master-data/master-data.service.ts @@ -22,7 +22,7 @@ interface MasterDataEntry { * All queries return reactive signals — UI updates automatically * when data changes. */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class MasterDataService { private readonly entries = new Map(); private readonly stateSignals = signal>>(new Map()); diff --git a/packages/core/src/lib/master-data/provide-master-data.ts b/packages/core/src/lib/master-data/provide-master-data.ts index 13a2d10..8805f6e 100644 --- a/packages/core/src/lib/master-data/provide-master-data.ts +++ b/packages/core/src/lib/master-data/provide-master-data.ts @@ -36,6 +36,7 @@ export function provideMasterData( sources: MasterDataSource[] ): EnvironmentProviders { return makeEnvironmentProviders([ + MasterDataService, { provide: MASTER_DATA_SOURCES, useValue: sources, diff --git a/packages/core/src/lib/navigation/breadcrumb.service.spec.ts b/packages/core/src/lib/navigation/breadcrumb.service.spec.ts index efbd7ca..6d5474a 100644 --- a/packages/core/src/lib/navigation/breadcrumb.service.spec.ts +++ b/packages/core/src/lib/navigation/breadcrumb.service.spec.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { provideRouter, Router } from '@angular/router'; import { BreadcrumbService } from './breadcrumb.service'; +import { provideBreadcrumb } from './provide-breadcrumb'; @Component({ template: '', standalone: true }) class DummyComponent {} @@ -13,6 +14,7 @@ describe('BreadcrumbService', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ + provideBreadcrumb(), provideRouter([ { path: 'modules', diff --git a/packages/core/src/lib/navigation/breadcrumb.service.ts b/packages/core/src/lib/navigation/breadcrumb.service.ts index e1e50e9..3310f71 100644 --- a/packages/core/src/lib/navigation/breadcrumb.service.ts +++ b/packages/core/src/lib/navigation/breadcrumb.service.ts @@ -16,7 +16,7 @@ import { BreadcrumbItem } from './navigation.types'; * * Routes without `data.breadcrumb` are skipped in the trail. */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class BreadcrumbService { private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); diff --git a/packages/core/src/lib/navigation/index.ts b/packages/core/src/lib/navigation/index.ts index 8587840..2ac7fb9 100644 --- a/packages/core/src/lib/navigation/index.ts +++ b/packages/core/src/lib/navigation/index.ts @@ -2,3 +2,4 @@ export { NavigationService } from './navigation.service'; export { BreadcrumbService } from './breadcrumb.service'; export { provideNavigation } from './provide-navigation'; export type { NavItem, BreadcrumbItem, NavigationConfig } from './navigation.types'; +export { provideBreadcrumb } from './provide-breadcrumb'; diff --git a/packages/core/src/lib/navigation/navigation.service.spec.ts b/packages/core/src/lib/navigation/navigation.service.spec.ts index 9167ad9..29bb054 100644 --- a/packages/core/src/lib/navigation/navigation.service.spec.ts +++ b/packages/core/src/lib/navigation/navigation.service.spec.ts @@ -3,6 +3,8 @@ import { provideRouter } from '@angular/router'; import { NavigationService } from './navigation.service'; import { PermissionService } from '../permissions/permission.service'; import { NavItem } from './navigation.types'; +import { provideNavigation } from './provide-navigation'; +import { providePermissions } from '../permissions/provide-permissions'; describe('NavigationService', () => { let service: NavigationService; @@ -10,7 +12,7 @@ describe('NavigationService', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [provideRouter([])], + providers: [provideRouter([]), provideNavigation({ items: [] }), providePermissions()], }); service = TestBed.inject(NavigationService); permissions = TestBed.inject(PermissionService); diff --git a/packages/core/src/lib/navigation/navigation.service.ts b/packages/core/src/lib/navigation/navigation.service.ts index 62d5cda..127c8b8 100644 --- a/packages/core/src/lib/navigation/navigation.service.ts +++ b/packages/core/src/lib/navigation/navigation.service.ts @@ -19,7 +19,7 @@ import { NavItem, NavigationConfig } from './navigation.types'; * * Does NOT wrap `Router.navigate()` — use Angular Router directly. */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class NavigationService { private readonly permissions = inject(PermissionService); private readonly router = inject(Router); diff --git a/packages/core/src/lib/navigation/provide-breadcrumb.ts b/packages/core/src/lib/navigation/provide-breadcrumb.ts new file mode 100644 index 0000000..3967af0 --- /dev/null +++ b/packages/core/src/lib/navigation/provide-breadcrumb.ts @@ -0,0 +1,6 @@ +import { makeEnvironmentProviders, EnvironmentProviders } from '@angular/core'; +import { BreadcrumbService } from './breadcrumb.service'; + +export function provideBreadcrumb(): EnvironmentProviders { + return makeEnvironmentProviders([BreadcrumbService]); +} diff --git a/packages/core/src/lib/navigation/provide-navigation.spec.ts b/packages/core/src/lib/navigation/provide-navigation.spec.ts index 8556ed0..51e45cf 100644 --- a/packages/core/src/lib/navigation/provide-navigation.spec.ts +++ b/packages/core/src/lib/navigation/provide-navigation.spec.ts @@ -4,6 +4,7 @@ import { provideRouter } from '@angular/router'; import { NavigationService } from './navigation.service'; import { NavigationConfig } from './navigation.types'; import { provideNavigation } from './provide-navigation'; +import { providePermissions } from '../permissions/provide-permissions'; describe('provideNavigation', () => { const config: NavigationConfig = { @@ -26,7 +27,7 @@ describe('provideNavigation', () => { it('should configure NavigationService via APP_INITIALIZER', async () => { TestBed.configureTestingModule({ - providers: [provideRouter([]), provideNavigation(config)], + providers: [provideRouter([]), providePermissions(), provideNavigation(config)], }); const initStatus = TestBed.inject(ApplicationInitStatus); @@ -42,7 +43,7 @@ describe('provideNavigation', () => { it('should work with empty items', async () => { TestBed.configureTestingModule({ - providers: [provideRouter([]), provideNavigation({ items: [] })], + providers: [provideRouter([]), providePermissions(), provideNavigation({ items: [] })], }); const initStatus = TestBed.inject(ApplicationInitStatus); diff --git a/packages/core/src/lib/navigation/provide-navigation.ts b/packages/core/src/lib/navigation/provide-navigation.ts index 472e886..320ee93 100644 --- a/packages/core/src/lib/navigation/provide-navigation.ts +++ b/packages/core/src/lib/navigation/provide-navigation.ts @@ -44,6 +44,7 @@ export function provideNavigation( config: NavigationConfig ): EnvironmentProviders { return makeEnvironmentProviders([ + NavigationService, { provide: NAVIGATION_CONFIG, useValue: config, diff --git a/packages/core/src/lib/permissions/directives/has-permission.directive.spec.ts b/packages/core/src/lib/permissions/directives/has-permission.directive.spec.ts index 4101046..95700d2 100644 --- a/packages/core/src/lib/permissions/directives/has-permission.directive.spec.ts +++ b/packages/core/src/lib/permissions/directives/has-permission.directive.spec.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HasPermissionDirective } from './has-permission.directive'; import { PermissionService } from '../permission.service'; +import { providePermissions } from '../provide-permissions'; @Component({ template: `Visible`, @@ -17,6 +18,7 @@ describe('HasPermissionDirective', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [TestHostComponent], + providers: [providePermissions()], }); service = TestBed.inject(PermissionService); diff --git a/packages/core/src/lib/permissions/directives/has-role.directive.spec.ts b/packages/core/src/lib/permissions/directives/has-role.directive.spec.ts index e44baf2..c3d19ac 100644 --- a/packages/core/src/lib/permissions/directives/has-role.directive.spec.ts +++ b/packages/core/src/lib/permissions/directives/has-role.directive.spec.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HasRoleDirective } from './has-role.directive'; import { PermissionService } from '../permission.service'; +import { providePermissions } from '../provide-permissions'; @Component({ template: `Admin Only`, @@ -17,6 +18,7 @@ describe('HasRoleDirective', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [TestHostComponent], + providers: [providePermissions()], }); service = TestBed.inject(PermissionService); diff --git a/packages/core/src/lib/permissions/guards/dynamic-permission.guard.spec.ts b/packages/core/src/lib/permissions/guards/dynamic-permission.guard.spec.ts index 7362632..9363a02 100644 --- a/packages/core/src/lib/permissions/guards/dynamic-permission.guard.spec.ts +++ b/packages/core/src/lib/permissions/guards/dynamic-permission.guard.spec.ts @@ -5,6 +5,7 @@ import { dynamicPermissionGuard } from './dynamic-permission.guard'; import { PermissionService } from '../permission.service'; import { RoutePermissionMap } from '../dynamic-permission.types'; import { DYNAMIC_PERMISSION_CONFIG } from '../provide-dynamic-permissions'; +import { providePermissions } from '../provide-permissions'; @Component({ template: '', standalone: true }) class DummyComponent {} @@ -19,7 +20,7 @@ describe('dynamicPermissionGuard', () => { providers: any[] = [], ) { TestBed.configureTestingModule({ - providers: [provideRouter(routes), ...providers], + providers: [provideRouter(routes), providePermissions(), ...providers], }); service = TestBed.inject(PermissionService); router = TestBed.inject(Router); diff --git a/packages/core/src/lib/permissions/guards/permission.guard.spec.ts b/packages/core/src/lib/permissions/guards/permission.guard.spec.ts index 318c4a3..af0db12 100644 --- a/packages/core/src/lib/permissions/guards/permission.guard.spec.ts +++ b/packages/core/src/lib/permissions/guards/permission.guard.spec.ts @@ -3,6 +3,7 @@ import { Router, provideRouter } from '@angular/router'; import { Component } from '@angular/core'; import { permissionGuard, roleGuard } from './permission.guard'; import { PermissionService } from '../permission.service'; +import { providePermissions } from '../provide-permissions'; @Component({ template: '', standalone: true }) class DummyComponent {} @@ -14,6 +15,7 @@ describe('permissionGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ + providePermissions(), provideRouter([ { path: 'protected', @@ -76,6 +78,7 @@ describe('roleGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ + providePermissions(), provideRouter([ { path: 'admin', diff --git a/packages/core/src/lib/permissions/permission.service.spec.ts b/packages/core/src/lib/permissions/permission.service.spec.ts index dd04f3a..d3318b4 100644 --- a/packages/core/src/lib/permissions/permission.service.spec.ts +++ b/packages/core/src/lib/permissions/permission.service.spec.ts @@ -1,11 +1,12 @@ import { TestBed } from '@angular/core/testing'; import { PermissionService } from './permission.service'; +import { providePermissions } from './provide-permissions'; describe('PermissionService', () => { let service: PermissionService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ providers: [providePermissions()] }); service = TestBed.inject(PermissionService); }); diff --git a/packages/core/src/lib/permissions/permission.service.ts b/packages/core/src/lib/permissions/permission.service.ts index ef53ace..6a0dc4b 100644 --- a/packages/core/src/lib/permissions/permission.service.ts +++ b/packages/core/src/lib/permissions/permission.service.ts @@ -14,7 +14,7 @@ import { PermissionSnapshot } from './permission.types'; * All queries return reactive signals — UI updates automatically * when permissions change. */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class PermissionService { private readonly _permissions = signal([]); private readonly _roles = signal([]); diff --git a/packages/core/src/lib/permissions/provide-dynamic-permissions.spec.ts b/packages/core/src/lib/permissions/provide-dynamic-permissions.spec.ts index 2efcd4d..fd290fb 100644 --- a/packages/core/src/lib/permissions/provide-dynamic-permissions.spec.ts +++ b/packages/core/src/lib/permissions/provide-dynamic-permissions.spec.ts @@ -8,6 +8,7 @@ import { import { DynamicPermissionConfig, RoutePermissionMap } from './dynamic-permission.types'; import { dynamicPermissionGuard } from './guards/dynamic-permission.guard'; import { PermissionService } from './permission.service'; +import { providePermissions } from './provide-permissions'; @Component({ template: '', standalone: true }) class DummyComponent {} @@ -29,7 +30,7 @@ describe('provideDynamicPermissions', () => { }; TestBed.configureTestingModule({ - providers: [provideRouter([]), provideDynamicPermissions(config)], + providers: [provideRouter([]), providePermissions(), provideDynamicPermissions(config)], }); const injected = TestBed.inject(DYNAMIC_PERMISSION_CONFIG); @@ -44,6 +45,7 @@ describe('provideDynamicPermissions', () => { TestBed.configureTestingModule({ providers: [ + providePermissions(), provideRouter([ { path: 'secured', @@ -69,6 +71,7 @@ describe('provideDynamicPermissions', () => { it('should respect fallbackBehavior from provided config', async () => { TestBed.configureTestingModule({ providers: [ + providePermissions(), provideRouter([ { path: 'unmapped', @@ -94,6 +97,7 @@ describe('provideDynamicPermissions', () => { it('should respect redirectTo from provided config', async () => { TestBed.configureTestingModule({ providers: [ + providePermissions(), provideRouter([ { path: 'locked', @@ -125,6 +129,7 @@ describe('provideDynamicPermissions', () => { TestBed.configureTestingModule({ providers: [ + providePermissions(), provideRouter([ { path: 'page', diff --git a/packages/core/src/lib/permissions/provide-permissions.ts b/packages/core/src/lib/permissions/provide-permissions.ts index 2dd15eb..4e7916b 100644 --- a/packages/core/src/lib/permissions/provide-permissions.ts +++ b/packages/core/src/lib/permissions/provide-permissions.ts @@ -1,11 +1,10 @@ import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; +import { PermissionService } from './permission.service'; /** * Configures the Permissions module. * - * Currently PermissionService is `providedIn: 'root'` and requires - * no additional setup, so this factory returns an empty provider set. - * It exists as a consistent entry-point for product `app.config.ts` + * Registers `PermissionService` and serves as a consistent entry-point for product `app.config.ts` * and as an extension point for future configuration (e.g. default * redirect route, permission loaders). * @@ -19,5 +18,5 @@ import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; * ``` */ export function providePermissions(): EnvironmentProviders { - return makeEnvironmentProviders([]); + return makeEnvironmentProviders([PermissionService]); } diff --git a/packages/core/src/lib/session/index.ts b/packages/core/src/lib/session/index.ts index c65d230..0cd6ed8 100644 --- a/packages/core/src/lib/session/index.ts +++ b/packages/core/src/lib/session/index.ts @@ -1,3 +1,4 @@ export { SessionService } from './session.service'; export { SESSION_TIMEOUT_CONFIG, DEFAULT_SESSION_TIMEOUT } from './session-timeout.config'; export type { SessionState, SessionTimeoutConfig } from './session.types'; +export { provideSession } from './provide-session'; diff --git a/packages/core/src/lib/session/provide-session.ts b/packages/core/src/lib/session/provide-session.ts new file mode 100644 index 0000000..e187197 --- /dev/null +++ b/packages/core/src/lib/session/provide-session.ts @@ -0,0 +1,15 @@ +import { makeEnvironmentProviders, EnvironmentProviders } from '@angular/core'; +import { SessionService } from './session.service'; +import { SESSION_TIMEOUT_CONFIG } from './session-timeout.config'; +import type { SessionTimeoutConfig } from './session.types'; + +export function provideSession( + config?: SessionTimeoutConfig, +): EnvironmentProviders { + return makeEnvironmentProviders([ + SessionService, + ...(config + ? [{ provide: SESSION_TIMEOUT_CONFIG, useValue: config }] + : []), + ]); +} diff --git a/packages/core/src/lib/session/session.service.spec.ts b/packages/core/src/lib/session/session.service.spec.ts index e26c7a4..a56dafd 100644 --- a/packages/core/src/lib/session/session.service.spec.ts +++ b/packages/core/src/lib/session/session.service.spec.ts @@ -2,6 +2,7 @@ import { TestBed } from '@angular/core/testing'; import { SessionService } from './session.service'; import { SESSION_TIMEOUT_CONFIG } from './session-timeout.config'; import { SessionTimeoutConfig } from './session.types'; +import { provideSession } from './provide-session'; describe('SessionService', () => { let service: SessionService; @@ -9,7 +10,7 @@ describe('SessionService', () => { beforeEach(() => { vi.useFakeTimers(); - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ providers: [provideSession()] }); service = TestBed.inject(SessionService); }); @@ -177,7 +178,7 @@ describe('SessionService', () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [ - { provide: SESSION_TIMEOUT_CONFIG, useValue: customConfig }, + provideSession(customConfig), ], }); service = TestBed.inject(SessionService); diff --git a/packages/core/src/lib/session/session.service.ts b/packages/core/src/lib/session/session.service.ts index cb79938..c0c67b4 100644 --- a/packages/core/src/lib/session/session.service.ts +++ b/packages/core/src/lib/session/session.service.ts @@ -26,7 +26,7 @@ const ACTIVITY_THROTTLE_MS = 5_000; * * Configuration is injectable via `SESSION_TIMEOUT_CONFIG`. */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class SessionService { /** Current session lifecycle state. */ readonly sessionState = signal('expired'); diff --git a/packages/core/src/lib/user-context/index.ts b/packages/core/src/lib/user-context/index.ts index 035af8a..499a3b1 100644 --- a/packages/core/src/lib/user-context/index.ts +++ b/packages/core/src/lib/user-context/index.ts @@ -1,3 +1,4 @@ export { UserContextService } from './user-context.service'; export { ROLE_PRIORITY } from './role-priority.config'; export type { UserProfile, UserRole, DecodedToken } from './user-context.types'; +export { provideUserContext } from './provide-user-context'; diff --git a/packages/core/src/lib/user-context/provide-user-context.ts b/packages/core/src/lib/user-context/provide-user-context.ts new file mode 100644 index 0000000..972b925 --- /dev/null +++ b/packages/core/src/lib/user-context/provide-user-context.ts @@ -0,0 +1,6 @@ +import { makeEnvironmentProviders, EnvironmentProviders } from '@angular/core'; +import { UserContextService } from './user-context.service'; + +export function provideUserContext(): EnvironmentProviders { + return makeEnvironmentProviders([UserContextService]); +} diff --git a/packages/core/src/lib/user-context/user-context.service.spec.ts b/packages/core/src/lib/user-context/user-context.service.spec.ts index 9c2a915..3230fca 100644 --- a/packages/core/src/lib/user-context/user-context.service.spec.ts +++ b/packages/core/src/lib/user-context/user-context.service.spec.ts @@ -2,6 +2,7 @@ import { TestBed } from '@angular/core/testing'; import { UserContextService } from './user-context.service'; import { ROLE_PRIORITY } from './role-priority.config'; import { UserProfile } from './user-context.types'; +import { provideUserContext } from './provide-user-context'; const MOCK_USER: UserProfile = { userId: 'u1', @@ -14,7 +15,7 @@ describe('UserContextService', () => { let service: UserContextService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ providers: [provideUserContext()] }); service = TestBed.inject(UserContextService); }); @@ -73,6 +74,7 @@ describe('UserContextService', () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [ + provideUserContext(), { provide: ROLE_PRIORITY, useValue: ['admin', 'supervisor', 'distributor', 'agent'], diff --git a/packages/core/src/lib/user-context/user-context.service.ts b/packages/core/src/lib/user-context/user-context.service.ts index 0e79103..c294edc 100644 --- a/packages/core/src/lib/user-context/user-context.service.ts +++ b/packages/core/src/lib/user-context/user-context.service.ts @@ -15,7 +15,7 @@ import { UserProfile, UserRole } from './user-context.types'; * The product layer is responsible for mapping its token/API * responses to these signals. */ -@Injectable({ providedIn: 'root' }) +@Injectable() export class UserContextService { readonly user = signal(null); readonly roles = signal([]);