import { APPLICATION_CREATED, CLIENT_REGISTER } from '../../../types/events'; import { IApplication } from './models'; import { IUnleashStores } from '../../../types/stores'; import { IUnleashConfig } from '../../../types/option'; import { IEventStore } from '../../../types/stores/event-store'; import { IClientApplication, IClientApplicationsStore, } from '../../../types/stores/client-applications-store'; import { IFeatureToggleStore } from '../../feature-toggle/types/feature-toggle-store-type'; import { IStrategyStore } from '../../../types/stores/strategy-store'; import { IClientInstanceStore } from '../../../types/stores/client-instance-store'; import { IApplicationQuery } from '../../../types/query'; import { IClientApp } from '../../../types/model'; import { clientRegisterSchema } from '../shared/schema'; import { IClientMetricsStoreV2 } from '../client-metrics/client-metrics-store-v2-type'; import { clientMetricsSchema } from '../shared/schema'; import { PartialSome } from '../../../types/partial'; import { IPrivateProjectChecker } from '../../private-project/privateProjectCheckerType'; import { IFlagResolver, SYSTEM_USER } from '../../../types'; import { ALL_PROJECTS, parseStrictSemVer } from '../../../util'; import { Logger } from '../../../logger'; export default class ClientInstanceService { apps = {}; logger: Logger; seenClients: Record = {}; private clientMetricsStoreV2: IClientMetricsStoreV2; private strategyStore: IStrategyStore; private featureToggleStore: IFeatureToggleStore; private clientApplicationsStore: IClientApplicationsStore; private clientInstanceStore: IClientInstanceStore; private eventStore: IEventStore; private privateProjectChecker: IPrivateProjectChecker; private flagResolver: IFlagResolver; constructor( { clientMetricsStoreV2, strategyStore, featureToggleStore, clientInstanceStore, clientApplicationsStore, eventStore, }: Pick< IUnleashStores, | 'clientMetricsStoreV2' | 'strategyStore' | 'featureToggleStore' | 'clientApplicationsStore' | 'clientInstanceStore' | 'eventStore' >, { getLogger, flagResolver, }: Pick, privateProjectChecker: IPrivateProjectChecker, ) { this.clientMetricsStoreV2 = clientMetricsStoreV2; this.strategyStore = strategyStore; this.featureToggleStore = featureToggleStore; this.clientApplicationsStore = clientApplicationsStore; this.clientInstanceStore = clientInstanceStore; this.eventStore = eventStore; this.privateProjectChecker = privateProjectChecker; this.flagResolver = flagResolver; this.logger = getLogger( '/services/client-metrics/client-instance-service.ts', ); } public async registerInstance( data: PartialSome, clientIp: string, ): Promise { const value = await clientMetricsSchema.validateAsync(data); await this.clientInstanceStore.setLastSeen({ appName: value.appName, instanceId: value.instanceId, environment: value.environment, clientIp: clientIp, }); } public async registerClient( data: PartialSome, clientIp: string, ): Promise { const value = await clientRegisterSchema.validateAsync(data); value.clientIp = clientIp; value.createdBy = clientIp; this.seenClients[this.clientKey(value)] = value; this.eventStore.emit(CLIENT_REGISTER, value); } async announceUnannounced(): Promise { if (this.clientApplicationsStore) { const appsToAnnounce = await this.clientApplicationsStore.setUnannouncedToAnnounced(); if (appsToAnnounce.length > 0) { const events = appsToAnnounce.map((app) => ({ type: APPLICATION_CREATED, createdBy: app.createdBy || SYSTEM_USER.username, data: app, createdByUserId: app.createdByUserId || SYSTEM_USER.id, })); await this.eventStore.batchStore(events); } } } clientKey(client: IClientApp): string { return `${client.appName}_${client.instanceId}`; } async bulkAdd(): Promise { if ( this && this.seenClients && this.clientApplicationsStore && this.clientInstanceStore ) { const uniqueRegistrations = Object.values(this.seenClients); const uniqueApps: Partial[] = Object.values( uniqueRegistrations.reduce((soFar, reg) => { // eslint-disable-next-line no-param-reassign soFar[reg.appName] = reg; return soFar; }, {}), ); this.seenClients = {}; try { if (uniqueRegistrations.length > 0) { await this.clientApplicationsStore.bulkUpsert(uniqueApps); await this.clientInstanceStore.bulkUpsert( uniqueRegistrations, ); } } catch (err) { this.logger.warn('Failed to register clients', err); } } } async getApplications( query: IApplicationQuery, userId: number, ): Promise { const applications = await this.clientApplicationsStore.getAppsForStrategy(query); const accessibleProjects = await this.privateProjectChecker.getUserAccessibleProjects(userId); if (accessibleProjects.mode === 'all') { return applications; } else { return applications.map((application) => { return { ...application, usage: application.usage?.filter( (usageItem) => usageItem.project === ALL_PROJECTS || accessibleProjects.projects.includes( usageItem.project, ), ), }; }); } } async getApplication(appName: string): Promise { const [seenToggles, application, instances, strategies, features] = await Promise.all([ this.clientMetricsStoreV2.getSeenTogglesForApp(appName), this.clientApplicationsStore.get(appName), this.clientInstanceStore.getByAppName(appName), this.strategyStore.getAll(), this.featureToggleStore.getAll(), ]); return { appName: application.appName, createdAt: application.createdAt, description: application.description, url: application.url, color: application.color, icon: application.icon, strategies: application.strategies.map((name) => { const found = strategies.find((f) => f.name === name); return found || { name, notFound: true }; }), instances, seenToggles: seenToggles.map((name) => { const found = features.find((f) => f.name === name); return found || { name, notFound: true }; }), links: { self: `/api/applications/${application.appName}`, }, }; } async deleteApplication(appName: string): Promise { await this.clientInstanceStore.deleteForApplication(appName); await this.clientApplicationsStore.delete(appName); } async createApplication(input: IApplication): Promise { await this.clientApplicationsStore.upsert(input); } async removeInstancesOlderThanTwoDays(): Promise { return this.clientInstanceStore.removeInstancesOlderThanTwoDays(); } async usesSdkOlderThan( sdkName: string, sdkVersion: string, ): Promise { const semver = parseStrictSemVer(sdkVersion); const instancesOfSdk = await this.clientInstanceStore.getBySdkName(sdkName); return instancesOfSdk.some((instance) => { if (instance.sdkVersion) { const [_sdkName, sdkVersion] = instance.sdkVersion.split(':'); const instanceUsedSemver = parseStrictSemVer(sdkVersion); return ( instanceUsedSemver !== null && semver !== null && instanceUsedSemver < semver ); } }); } }