diff --git a/.gitignore b/.gitignore index f493b540d..8cd7708f9 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ npm-debug.log # Build directories dist build +build-cli .angular/cache # Testing diff --git a/jslib/angular/src/services/jslib-services.module.ts b/jslib/angular/src/services/jslib-services.module.ts index d6bc0c602..098e82e16 100644 --- a/jslib/angular/src/services/jslib-services.module.ts +++ b/jslib/angular/src/services/jslib-services.module.ts @@ -38,6 +38,9 @@ import { StateMigrationService } from "@/jslib/common/src/services/stateMigratio import { TokenService } from "@/jslib/common/src/services/token.service"; import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service"; +import { SafeInjectionToken, SECURE_STORAGE, WINDOW } from '../../../../src/app/services/injection-tokens'; +import { SafeProvider, safeProvider } from '../../../../src/app/services/safe-provider'; + import { BroadcasterService } from "./broadcaster.service"; import { ModalService } from "./modal.service"; import { ValidationService } from "./validation.service"; @@ -45,20 +48,20 @@ import { ValidationService } from "./validation.service"; @NgModule({ declarations: [], providers: [ - { provide: "WINDOW", useValue: window }, - { - provide: LOCALE_ID, + safeProvider({ provide: WINDOW, useValue: window }), + safeProvider({ + provide: LOCALE_ID as SafeInjectionToken, useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale, deps: [I18nServiceAbstraction], - }, - ValidationService, - ModalService, - { + }), + safeProvider(ValidationService), + safeProvider(ModalService), + safeProvider({ provide: AppIdServiceAbstraction, useClass: AppIdService, deps: [StorageServiceAbstraction], - }, - { + }), + safeProvider({ provide: AuthServiceAbstraction, useClass: AuthService, deps: [ @@ -75,15 +78,15 @@ import { ValidationService } from "./validation.service"; TwoFactorServiceAbstraction, I18nServiceAbstraction, ], - }, - { provide: LogService, useFactory: () => new ConsoleLogService(false) }, - { + }), + safeProvider({ provide: LogService, useFactory: () => new ConsoleLogService(false), deps: [] }), + safeProvider({ provide: EnvironmentServiceAbstraction, useClass: EnvironmentService, deps: [StateServiceAbstraction], - }, - { provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] }, - { + }), + safeProvider({ provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] }), + safeProvider({ provide: CryptoServiceAbstraction, useClass: CryptoService, deps: [ @@ -92,13 +95,13 @@ import { ValidationService } from "./validation.service"; LogService, StateServiceAbstraction, ], - }, - { + }), + safeProvider({ provide: PasswordGenerationServiceAbstraction, useClass: PasswordGenerationService, deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction], - }, - { + }), + safeProvider({ provide: ApiServiceAbstraction, useFactory: ( tokenService: TokenServiceAbstraction, @@ -121,9 +124,9 @@ import { ValidationService } from "./validation.service"; MessagingServiceAbstraction, AppIdServiceAbstraction, ], - }, - { provide: BroadcasterServiceAbstraction, useClass: BroadcasterService }, - { + }), + safeProvider({ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService, useAngularDecorators: true }), + safeProvider({ provide: StateServiceAbstraction, useFactory: ( storageService: StorageServiceAbstraction, @@ -140,12 +143,12 @@ import { ValidationService } from "./validation.service"; ), deps: [ StorageServiceAbstraction, - "SECURE_STORAGE", + SECURE_STORAGE, LogService, StateMigrationServiceAbstraction, ], - }, - { + }), + safeProvider({ provide: StateMigrationServiceAbstraction, useFactory: ( storageService: StorageServiceAbstraction, @@ -156,14 +159,14 @@ import { ValidationService } from "./validation.service"; secureStorageService, new StateFactory(GlobalState, Account), ), - deps: [StorageServiceAbstraction, "SECURE_STORAGE"], - }, - { + deps: [StorageServiceAbstraction, SECURE_STORAGE], + }), + safeProvider({ provide: PolicyServiceAbstraction, useClass: PolicyService, deps: [StateServiceAbstraction, OrganizationServiceAbstraction, ApiServiceAbstraction], - }, - { + }), + safeProvider({ provide: KeyConnectorServiceAbstraction, useClass: KeyConnectorService, deps: [ @@ -175,17 +178,17 @@ import { ValidationService } from "./validation.service"; OrganizationServiceAbstraction, CryptoFunctionServiceAbstraction, ], - }, - { + }), + safeProvider({ provide: OrganizationServiceAbstraction, useClass: OrganizationService, deps: [StateServiceAbstraction], - }, - { + }), + safeProvider({ provide: TwoFactorServiceAbstraction, useClass: TwoFactorService, deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], - }, - ], + }), + ] satisfies SafeProvider[], }) export class JslibServicesModule {} diff --git a/src/app/services/injection-tokens.ts b/src/app/services/injection-tokens.ts new file mode 100644 index 000000000..97628338c --- /dev/null +++ b/src/app/services/injection-tokens.ts @@ -0,0 +1,17 @@ +import { InjectionToken } from "@angular/core"; + +import { StorageService } from "../../../jslib/common/src/abstractions/storage.service"; + +declare const tag: unique symbol; +/** + * A (more) typesafe version of InjectionToken which will more strictly enforce the generic type parameter. + * @remarks The default angular implementation does not use the generic type to define the structure of the object, + * so the structural type system will not complain about a mismatch in the type parameter. + * This is solved by assigning T to an arbitrary private property. + */ +export class SafeInjectionToken extends InjectionToken { + private readonly [tag]: T; +} + +export const SECURE_STORAGE = new SafeInjectionToken("SECURE_STORAGE"); +export const WINDOW = new SafeInjectionToken("WINDOW"); diff --git a/src/app/services/safe-provider.ts b/src/app/services/safe-provider.ts new file mode 100644 index 000000000..ccb8220f9 --- /dev/null +++ b/src/app/services/safe-provider.ts @@ -0,0 +1,144 @@ +import { Provider } from "@angular/core"; +import { Constructor, Opaque } from "type-fest"; + +import { SafeInjectionToken } from "./injection-tokens"; + +// ****** +// NOTE: this is a copy/paste of safe-provider.ts from the clients repository. +// The clients repository remains the primary version of this code. +// Make any changes there and copy it back to this repository. +// ****** + +/** + * The return type of the {@link safeProvider} helper function. + * Used to distinguish a type safe provider definition from a non-type safe provider definition. + */ +export type SafeProvider = Opaque; + +// TODO: type-fest also provides a type like this when we upgrade >= 3.7.0 +type AbstractConstructor = abstract new (...args: any) => T; + +type MapParametersToDeps = { + [K in keyof T]: AbstractConstructor | SafeInjectionToken; +}; + +type SafeInjectionTokenType = T extends SafeInjectionToken ? J : never; + +/** + * Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken + */ +type ProviderInstanceType = + T extends SafeInjectionToken + ? InstanceType> + : T extends Constructor | AbstractConstructor + ? InstanceType + : never; + +/** + * Represents a dependency provided with the useClass option. + */ +type SafeClassProvider< + A extends AbstractConstructor | SafeInjectionToken, + I extends Constructor>, + D extends MapParametersToDeps>, +> = { + provide: A; + useClass: I; + deps: D; +}; + +/** + * Represents a dependency provided with the useValue option. + */ +type SafeValueProvider, V extends SafeInjectionTokenType> = { + provide: A; + useValue: V; +}; + +/** + * Represents a dependency provided with the useFactory option. + */ +type SafeFactoryProvider< + A extends AbstractConstructor | SafeInjectionToken, + I extends (...args: any) => ProviderInstanceType, + D extends MapParametersToDeps>, +> = { + provide: A; + useFactory: I; + deps: D; + multi?: boolean; +}; + +/** + * Represents a dependency provided with the useExisting option. + */ +type SafeExistingProvider< + A extends Constructor | AbstractConstructor | SafeInjectionToken, + I extends Constructor> | AbstractConstructor>, +> = { + provide: A; + useExisting: I; +}; + +/** + * Represents a dependency where there is no abstract token, the token is the implementation + */ +type SafeConcreteProvider< + I extends Constructor, + D extends MapParametersToDeps>, +> = { + provide: I; + deps: D; +}; + +/** + * If useAngularDecorators: true is specified, do not require a deps array. + * This is a manual override for where @Injectable decorators are used + */ +type UseAngularDecorators = Omit & { + useAngularDecorators: true; +}; + +/** + * Represents a type with a deps array that may optionally be overridden with useAngularDecorators + */ +type AllowAngularDecorators = T | UseAngularDecorators; + +/** + * A factory function that creates a provider for the ngModule providers array. + * This (almost) guarantees type safety for your provider definition. It does nothing at runtime. + * Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator, + * however this cannot be enforced by the type system and will not cause an error if the decorator is not used. + * @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] }) + * @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.) + * @returns The exact same object without modification (pass-through). + */ +export const safeProvider = < + // types for useClass + AClass extends AbstractConstructor | SafeInjectionToken, + IClass extends Constructor>, + DClass extends MapParametersToDeps>, + // types for useValue + AValue extends SafeInjectionToken, + VValue extends SafeInjectionTokenType, + // types for useFactory + AFactory extends AbstractConstructor | SafeInjectionToken, + IFactory extends (...args: any) => ProviderInstanceType, + DFactory extends MapParametersToDeps>, + // types for useExisting + AExisting extends Constructor | AbstractConstructor | SafeInjectionToken, + IExisting extends + | Constructor> + | AbstractConstructor>, + // types for no token + IConcrete extends Constructor, + DConcrete extends MapParametersToDeps>, +>( + provider: + | AllowAngularDecorators> + | SafeValueProvider + | AllowAngularDecorators> + | SafeExistingProvider + | AllowAngularDecorators> + | Constructor, +): SafeProvider => provider as SafeProvider; diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts index d79f1caa7..e45fecc8b 100644 --- a/src/app/services/services.module.ts +++ b/src/app/services/services.module.ts @@ -38,11 +38,13 @@ import { StateMigrationService } from "../../services/stateMigration.service"; import { SyncService } from "../../services/sync.service"; import { AuthGuardService } from "./auth-guard.service"; +import { SafeInjectionToken, SECURE_STORAGE, WINDOW } from "./injection-tokens"; import { LaunchGuardService } from "./launch-guard.service"; +import { SafeProvider, safeProvider } from "./safe-provider"; export function initFactory( environmentService: EnvironmentServiceAbstraction, - i18nService: I18nService, + i18nService: I18nServiceAbstraction, platformUtilsService: PlatformUtilsServiceAbstraction, stateService: StateServiceAbstraction, cryptoService: CryptoServiceAbstraction, @@ -50,7 +52,7 @@ export function initFactory( return async () => { await stateService.init(); await environmentService.setUrlsFromStorage(); - await i18nService.init(); + await (i18nService as I18nService).init(); const htmlEl = window.document.documentElement; htmlEl.classList.add("os_" + platformUtilsService.getDeviceString()); htmlEl.classList.add("locale_" + i18nService.translationLocale); @@ -78,8 +80,8 @@ export function initFactory( imports: [JslibServicesModule], declarations: [], providers: [ - { - provide: APP_INITIALIZER, + safeProvider({ + provide: APP_INITIALIZER as SafeInjectionToken<() => void>, useFactory: initFactory, deps: [ EnvironmentServiceAbstraction, @@ -89,21 +91,29 @@ export function initFactory( CryptoServiceAbstraction, ], multi: true, - }, - { provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }, - { + }), + safeProvider({ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }), + safeProvider({ provide: I18nServiceAbstraction, useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"), - deps: ["WINDOW"], - }, - { + deps: [WINDOW], + }), + safeProvider({ provide: MessagingServiceAbstraction, useClass: ElectronRendererMessagingService, deps: [BroadcasterServiceAbstraction], - }, - { provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService }, - { provide: "SECURE_STORAGE", useClass: ElectronRendererSecureStorageService }, - { + }), + safeProvider({ + provide: StorageServiceAbstraction, + useClass: ElectronRendererStorageService, + deps: [], + }), + safeProvider({ + provide: SECURE_STORAGE, + useClass: ElectronRendererSecureStorageService, + deps: [], + }), + safeProvider({ provide: PlatformUtilsServiceAbstraction, useFactory: ( i18nService: I18nServiceAbstraction, @@ -111,9 +121,13 @@ export function initFactory( stateService: StateServiceAbstraction, ) => new ElectronPlatformUtilsService(i18nService, messagingService, false, stateService), deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StateServiceAbstraction], - }, - { provide: CryptoFunctionServiceAbstraction, useClass: NodeCryptoFunctionService, deps: [] }, - { + }), + safeProvider({ + provide: CryptoFunctionServiceAbstraction, + useClass: NodeCryptoFunctionService, + deps: [], + }), + safeProvider({ provide: ApiServiceAbstraction, useFactory: ( tokenService: TokenServiceAbstraction, @@ -141,8 +155,8 @@ export function initFactory( MessagingServiceAbstraction, AppIdServiceAbstraction, ], - }, - { + }), + safeProvider({ provide: AuthServiceAbstraction, useClass: AuthService, deps: [ @@ -159,8 +173,8 @@ export function initFactory( TwoFactorServiceAbstraction, I18nServiceAbstraction, ], - }, - { + }), + safeProvider({ provide: SyncService, useClass: SyncService, deps: [ @@ -172,10 +186,10 @@ export function initFactory( EnvironmentServiceAbstraction, StateServiceAbstraction, ], - }, - AuthGuardService, - LaunchGuardService, - { + }), + safeProvider(AuthGuardService), + safeProvider(LaunchGuardService), + safeProvider({ provide: StateMigrationServiceAbstraction, useFactory: ( storageService: StorageServiceAbstraction, @@ -186,9 +200,9 @@ export function initFactory( secureStorageService, new StateFactory(GlobalState, Account), ), - deps: [StorageServiceAbstraction, "SECURE_STORAGE"], - }, - { + deps: [StorageServiceAbstraction, SECURE_STORAGE], + }), + safeProvider({ provide: StateServiceAbstraction, useFactory: ( storageService: StorageServiceAbstraction, @@ -206,15 +220,16 @@ export function initFactory( ), deps: [ StorageServiceAbstraction, - "SECURE_STORAGE", + SECURE_STORAGE, LogServiceAbstraction, StateMigrationServiceAbstraction, ], - }, - { + }), + safeProvider({ provide: TwoFactorServiceAbstraction, useClass: NoopTwoFactorService, - }, - ], + deps: [], + }), + ] satisfies SafeProvider[], }) export class ServicesModule {}