2022-11-21 11:57:07 +01:00
|
|
|
import { IUnleashConfig, IUnleashServices, IUnleashStores } from '../types';
|
2022-08-16 15:33:33 +02:00
|
|
|
import { Logger } from '../logger';
|
2022-11-21 11:57:07 +01:00
|
|
|
import { ProxyFeatureSchema, ProxyMetricsSchema } from '../openapi';
|
2022-08-16 15:33:33 +02:00
|
|
|
import ApiUser from '../types/api-user';
|
|
|
|
import {
|
|
|
|
Context,
|
|
|
|
InMemStorageProvider,
|
|
|
|
startUnleash,
|
|
|
|
Unleash,
|
|
|
|
UnleashEvents,
|
|
|
|
} from 'unleash-client';
|
2022-11-21 11:57:07 +01:00
|
|
|
import { ProxyRepository } from '../proxy';
|
2022-08-16 15:33:33 +02:00
|
|
|
import { ApiTokenType } from '../types/models/api-token';
|
2022-12-14 17:35:22 +01:00
|
|
|
import {
|
|
|
|
FrontendSettings,
|
|
|
|
frontendSettingsKey,
|
|
|
|
} from '../types/settings/frontend-settings';
|
|
|
|
import { validateOrigins } from '../util';
|
|
|
|
import { BadDataError } from '../error';
|
|
|
|
import assert from 'assert';
|
|
|
|
import { minutesToMilliseconds } from 'date-fns';
|
2022-08-16 15:33:33 +02:00
|
|
|
|
2022-12-14 17:35:22 +01:00
|
|
|
type Config = Pick<
|
|
|
|
IUnleashConfig,
|
|
|
|
'getLogger' | 'frontendApi' | 'frontendApiOrigins'
|
|
|
|
>;
|
2022-08-16 15:33:33 +02:00
|
|
|
|
|
|
|
type Stores = Pick<IUnleashStores, 'projectStore' | 'eventStore'>;
|
|
|
|
|
|
|
|
type Services = Pick<
|
|
|
|
IUnleashServices,
|
2022-12-14 17:35:22 +01:00
|
|
|
| 'featureToggleServiceV2'
|
|
|
|
| 'segmentService'
|
|
|
|
| 'clientMetricsServiceV2'
|
|
|
|
| 'settingService'
|
2022-08-16 15:33:33 +02:00
|
|
|
>;
|
|
|
|
|
|
|
|
export class ProxyService {
|
|
|
|
private readonly config: Config;
|
|
|
|
|
|
|
|
private readonly logger: Logger;
|
|
|
|
|
|
|
|
private readonly stores: Stores;
|
|
|
|
|
|
|
|
private readonly services: Services;
|
|
|
|
|
|
|
|
private readonly clients: Map<ApiUser['secret'], Unleash> = new Map();
|
|
|
|
|
2022-12-14 17:35:22 +01:00
|
|
|
private cachedFrontendSettings?: FrontendSettings;
|
|
|
|
|
|
|
|
private timer: NodeJS.Timeout;
|
|
|
|
|
2022-08-16 15:33:33 +02:00
|
|
|
constructor(config: Config, stores: Stores, services: Services) {
|
|
|
|
this.config = config;
|
|
|
|
this.logger = config.getLogger('services/proxy-service.ts');
|
|
|
|
this.stores = stores;
|
|
|
|
this.services = services;
|
2022-12-14 17:35:22 +01:00
|
|
|
|
|
|
|
this.timer = setInterval(
|
|
|
|
() => this.fetchFrontendSettings(),
|
|
|
|
minutesToMilliseconds(2),
|
|
|
|
).unref();
|
2022-08-16 15:33:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async getProxyFeatures(
|
|
|
|
token: ApiUser,
|
|
|
|
context: Context,
|
|
|
|
): Promise<ProxyFeatureSchema[]> {
|
|
|
|
const client = await this.clientForProxyToken(token);
|
|
|
|
const definitions = client.getFeatureToggleDefinitions() || [];
|
|
|
|
|
|
|
|
return definitions
|
|
|
|
.filter((feature) => client.isEnabled(feature.name, context))
|
|
|
|
.map((feature) => ({
|
|
|
|
name: feature.name,
|
|
|
|
enabled: Boolean(feature.enabled),
|
|
|
|
variant: client.forceGetVariant(feature.name, context),
|
|
|
|
impressionData: Boolean(feature.impressionData),
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2022-11-21 11:57:07 +01:00
|
|
|
async getAllProxyFeatures(
|
|
|
|
token: ApiUser,
|
|
|
|
context: Context,
|
|
|
|
): Promise<ProxyFeatureSchema[]> {
|
|
|
|
const client = await this.clientForProxyToken(token);
|
|
|
|
const definitions = client.getFeatureToggleDefinitions() || [];
|
|
|
|
|
|
|
|
return definitions.map((feature) => ({
|
|
|
|
name: feature.name,
|
|
|
|
enabled: Boolean(feature.enabled),
|
|
|
|
variant: client.forceGetVariant(feature.name, context),
|
|
|
|
impressionData: Boolean(feature.impressionData),
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2022-08-16 15:33:33 +02:00
|
|
|
async registerProxyMetrics(
|
|
|
|
token: ApiUser,
|
|
|
|
metrics: ProxyMetricsSchema,
|
|
|
|
ip: string,
|
|
|
|
): Promise<void> {
|
|
|
|
ProxyService.assertExpectedTokenType(token);
|
|
|
|
|
|
|
|
const environment =
|
|
|
|
this.services.clientMetricsServiceV2.resolveMetricsEnvironment(
|
|
|
|
token,
|
|
|
|
metrics,
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.services.clientMetricsServiceV2.registerClientMetrics(
|
|
|
|
{ ...metrics, environment },
|
|
|
|
ip,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private async clientForProxyToken(token: ApiUser): Promise<Unleash> {
|
|
|
|
ProxyService.assertExpectedTokenType(token);
|
|
|
|
|
|
|
|
if (!this.clients.has(token.secret)) {
|
|
|
|
this.clients.set(
|
|
|
|
token.secret,
|
|
|
|
await this.createClientForProxyToken(token),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.clients.get(token.secret);
|
|
|
|
}
|
|
|
|
|
|
|
|
private async createClientForProxyToken(token: ApiUser): Promise<Unleash> {
|
|
|
|
const repository = new ProxyRepository(
|
|
|
|
this.config,
|
|
|
|
this.stores,
|
|
|
|
this.services,
|
|
|
|
token,
|
|
|
|
);
|
|
|
|
|
|
|
|
const client = await startUnleash({
|
|
|
|
appName: 'proxy',
|
|
|
|
url: 'unused',
|
|
|
|
storageProvider: new InMemStorageProvider(),
|
|
|
|
disableMetrics: true,
|
|
|
|
repository,
|
|
|
|
});
|
|
|
|
|
|
|
|
client.on(UnleashEvents.Error, (error) => {
|
|
|
|
this.logger.error(error);
|
|
|
|
});
|
|
|
|
|
|
|
|
return client;
|
|
|
|
}
|
|
|
|
|
2022-08-22 15:02:39 +02:00
|
|
|
deleteClientForProxyToken(secret: string): void {
|
|
|
|
this.clients.delete(secret);
|
|
|
|
}
|
|
|
|
|
2022-09-28 14:23:41 +02:00
|
|
|
stopAll(): void {
|
|
|
|
this.clients.forEach((client) => client.destroy());
|
|
|
|
}
|
|
|
|
|
2022-08-16 15:33:33 +02:00
|
|
|
private static assertExpectedTokenType({ type }: ApiUser) {
|
2022-08-18 10:20:51 +02:00
|
|
|
assert(type === ApiTokenType.FRONTEND || type === ApiTokenType.ADMIN);
|
2022-08-16 15:33:33 +02:00
|
|
|
}
|
2022-12-14 17:35:22 +01:00
|
|
|
|
|
|
|
async setFrontendSettings(
|
|
|
|
value: FrontendSettings,
|
|
|
|
createdBy: string,
|
|
|
|
): Promise<void> {
|
|
|
|
const error = validateOrigins(value.frontendApiOrigins);
|
|
|
|
if (error) {
|
|
|
|
throw new BadDataError(error);
|
|
|
|
}
|
|
|
|
await this.services.settingService.insert(
|
|
|
|
frontendSettingsKey,
|
|
|
|
value,
|
|
|
|
createdBy,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
private async fetchFrontendSettings(): Promise<FrontendSettings> {
|
2022-12-14 20:24:47 +01:00
|
|
|
try {
|
|
|
|
this.cachedFrontendSettings =
|
|
|
|
await this.services.settingService.get(frontendSettingsKey, {
|
|
|
|
frontendApiOrigins: this.config.frontendApiOrigins,
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
this.logger.debug('Unable to fetch frontend settings');
|
|
|
|
}
|
2022-12-14 17:35:22 +01:00
|
|
|
return this.cachedFrontendSettings;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getFrontendSettings(
|
|
|
|
useCache: boolean = true,
|
|
|
|
): Promise<FrontendSettings> {
|
|
|
|
if (useCache && this.cachedFrontendSettings) {
|
|
|
|
return this.cachedFrontendSettings;
|
|
|
|
}
|
|
|
|
return this.fetchFrontendSettings();
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy(): void {
|
|
|
|
clearInterval(this.timer);
|
|
|
|
this.timer = null;
|
|
|
|
}
|
2022-08-16 15:33:33 +02:00
|
|
|
}
|