UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

220 lines • 9.75 kB
import { APPLICATION_CREATED, CLIENT_REGISTER } from '../../../events/index.js'; import { clientRegisterSchema } from '../shared/schema.js'; import { SYSTEM_USER, } from '../../../types/index.js'; import { ALL_PROJECTS, parseStrictSemVer } from '../../../util/index.js'; import { findOutdatedSDKs, isOutdatedSdk } from './findOutdatedSdks.js'; import { CLIENT_REGISTERED } from '../../../metric-events.js'; import { NotFoundError } from '../../../error/index.js'; export default class ClientInstanceService { constructor({ clientMetricsStoreV2, strategyStore, featureToggleStore, clientInstanceStore, clientApplicationsStore, eventStore, }, { getLogger, flagResolver, eventBus, }, privateProjectChecker) { this.apps = {}; this.seenClients = {}; this.updateSeenClient = (data) => { const current = this.seenClients[this.clientKey(data)]; this.seenClients[this.clientKey(data)] = { ...current, ...data, }; }; this.clientMetricsStoreV2 = clientMetricsStoreV2; this.strategyStore = strategyStore; this.featureToggleStore = featureToggleStore; this.clientApplicationsStore = clientApplicationsStore; this.clientInstanceStore = clientInstanceStore; this.eventStore = eventStore; this.eventBus = eventBus; this.privateProjectChecker = privateProjectChecker; this.flagResolver = flagResolver; this.logger = getLogger('/services/client-metrics/client-instance-service.ts'); } async registerInstance(data, clientIp) { this.updateSeenClient({ appName: data.appName, instanceId: data.instanceId ?? 'default', environment: data.environment, clientIp: clientIp, }); } registerFrontendClient(data) { data.createdBy = SYSTEM_USER.username; this.updateSeenClient(data); } async registerBackendClient(data, clientIp) { const value = await clientRegisterSchema.validateAsync(data); value.clientIp = clientIp; value.createdBy = SYSTEM_USER.username; value.sdkType = 'backend'; this.updateSeenClient(value); this.eventBus.emit(CLIENT_REGISTERED, value); if (value.sdkVersion && value.sdkVersion.indexOf(':') > -1) { const [sdkName, sdkVersion] = value.sdkVersion.split(':'); const heartbeatEvent = { sdkName, sdkVersion, metadata: { platformName: data.platformName, platformVersion: data.platformVersion, yggdrasilVersion: data.yggdrasilVersion, specVersion: data.specVersion, }, }; this.eventStore.emit(CLIENT_REGISTER, heartbeatEvent); } } async announceUnannounced() { 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, ip: '', // TODO: fix this, how do we get the ip from the client? This comes from a row in the DB })); await this.eventStore.batchStore(events); } } } clientKey(client) { return `${client.appName}_${client.instanceId}`; } async bulkAdd() { if (this && this.seenClients && this.clientApplicationsStore && this.clientInstanceStore) { const uniqueRegistrations = Object.values(this.seenClients); const uniqueApps = Object.values(uniqueRegistrations.reduce((soFar, reg) => { let existingProjects = []; if (soFar[`${reg.appName} ${reg.environment}`]) { existingProjects = soFar[`${reg.appName} ${reg.environment}`] .projects || []; } soFar[`${reg.appName} ${reg.environment}`] = { ...reg, projects: [ ...existingProjects, ...(reg.projects || []), ], }; 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, userId) { const applications = await this.clientApplicationsStore.getApplications({ ...query, sortBy: query.sortBy || 'appName' }); const accessibleProjects = await this.privateProjectChecker.getUserAccessibleProjects(userId); if (accessibleProjects.mode === 'all') { return applications; } else { return { applications: applications.applications.map((application) => { return { ...application, usage: application.usage?.filter((usageItem) => usageItem.project === ALL_PROJECTS || accessibleProjects.projects.includes(usageItem.project)), }; }), total: applications.total, }; } } async getApplication(appName) { 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(), ]); if (application === undefined) { throw new NotFoundError(`Could not find application with appName ${appName}`); } 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 getApplicationOverview(appName, userId) { const result = await this.clientApplicationsStore.getApplicationOverview(appName); const accessibleProjects = await this.privateProjectChecker.filterUserAccessibleProjects(userId, result.projects); result.projects = accessibleProjects; result.environments.forEach((environment) => { environment.issues.outdatedSdks = findOutdatedSDKs(environment.sdks); }); return result; } async getRecentApplicationEnvironmentInstances(appName, environment) { const instances = await this.clientInstanceStore.getRecentByAppNameAndEnvironment(appName, environment); return instances.map((instance) => ({ instanceId: instance.instanceId, clientIp: instance.clientIp, sdkVersion: instance.sdkVersion, lastSeen: instance.lastSeen, })); } async deleteApplication(appName) { await this.clientInstanceStore.deleteForApplication(appName); await this.clientApplicationsStore.delete(appName); } async createApplication(input) { await this.clientApplicationsStore.upsert(input); } async removeOldInstances() { return this.clientInstanceStore.removeOldInstances(); } async removeInactiveApplications() { return this.clientApplicationsStore.removeInactiveApplications(); } async getOutdatedSdks() { const sdkApps = await this.clientInstanceStore.groupApplicationsBySdk(); return sdkApps.filter((sdkApp) => isOutdatedSdk(sdkApp.sdkVersion)); } async getOutdatedSdksByProject(projectId) { const sdkApps = await this.clientInstanceStore.groupApplicationsBySdkAndProject(projectId); return sdkApps.filter((sdkApp) => isOutdatedSdk(sdkApp.sdkVersion)); } async usesSdkOlderThan(sdkName, sdkVersion) { 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); } }); } } //# sourceMappingURL=instance-service.js.map