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';
|
2023-03-03 16:36:23 +01:00
|
|
|
import { ClientMetricsSchema, ProxyFeatureSchema } from '../openapi';
|
2023-11-03 17:36:50 +01:00
|
|
|
import ApiUser, { IApiUser } from '../types/api-user';
|
2022-08-16 15:33:33 +02:00
|
|
|
import {
|
|
|
|
Context,
|
|
|
|
InMemStorageProvider,
|
|
|
|
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';
|
2023-02-13 08:40:04 +01:00
|
|
|
import { BadDataError, InvalidTokenError } from '../error';
|
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'
|
2023-05-12 19:52:11 +02:00
|
|
|
| 'configurationRevisionService'
|
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;
|
|
|
|
|
2023-04-04 09:32:35 +02:00
|
|
|
/**
|
|
|
|
* This is intentionally a Promise becasue we want to be able to await
|
|
|
|
* until the client (which might be being created by a different request) is ready
|
|
|
|
* Check this test that fails if we don't use a Promise: src/test/e2e/api/proxy/proxy.concurrency.e2e.test.ts
|
|
|
|
*/
|
|
|
|
private readonly clients: Map<ApiUser['secret'], Promise<Unleash>> =
|
|
|
|
new Map();
|
2022-08-16 15:33:33 +02:00
|
|
|
|
2022-12-14 17:35:22 +01:00
|
|
|
private cachedFrontendSettings?: FrontendSettings;
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getProxyFeatures(
|
2023-11-03 17:36:50 +01:00
|
|
|
token: IApiUser,
|
2022-08-16 15:33:33 +02:00
|
|
|
context: Context,
|
|
|
|
): Promise<ProxyFeatureSchema[]> {
|
|
|
|
const client = await this.clientForProxyToken(token);
|
|
|
|
const definitions = client.getFeatureToggleDefinitions() || [];
|
|
|
|
|
2023-10-18 16:19:03 +02:00
|
|
|
const sessionId = context.sessionId || String(Math.random());
|
|
|
|
|
2022-08-16 15:33:33 +02:00
|
|
|
return definitions
|
2023-10-18 16:19:03 +02:00
|
|
|
.filter((feature) =>
|
|
|
|
client.isEnabled(feature.name, { ...context, sessionId }),
|
|
|
|
)
|
2022-08-16 15:33:33 +02:00
|
|
|
.map((feature) => ({
|
|
|
|
name: feature.name,
|
|
|
|
enabled: Boolean(feature.enabled),
|
2023-10-18 16:19:03 +02:00
|
|
|
variant: client.getVariant(feature.name, {
|
|
|
|
...context,
|
|
|
|
sessionId,
|
|
|
|
}),
|
2022-08-16 15:33:33 +02:00
|
|
|
impressionData: Boolean(feature.impressionData),
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
async registerProxyMetrics(
|
2023-11-03 17:36:50 +01:00
|
|
|
token: IApiUser,
|
2023-03-03 16:36:23 +01:00
|
|
|
metrics: ClientMetricsSchema,
|
2022-08-16 15:33:33 +02:00
|
|
|
ip: string,
|
|
|
|
): Promise<void> {
|
|
|
|
ProxyService.assertExpectedTokenType(token);
|
|
|
|
|
|
|
|
const environment =
|
|
|
|
this.services.clientMetricsServiceV2.resolveMetricsEnvironment(
|
2023-11-03 17:36:50 +01:00
|
|
|
token as ApiUser,
|
2022-08-16 15:33:33 +02:00
|
|
|
metrics,
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.services.clientMetricsServiceV2.registerClientMetrics(
|
|
|
|
{ ...metrics, environment },
|
|
|
|
ip,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-11-03 17:36:50 +01:00
|
|
|
private async clientForProxyToken(token: IApiUser): Promise<Unleash> {
|
2022-08-16 15:33:33 +02:00
|
|
|
ProxyService.assertExpectedTokenType(token);
|
|
|
|
|
2023-04-04 09:32:35 +02:00
|
|
|
let client = this.clients.get(token.secret);
|
|
|
|
if (!client) {
|
|
|
|
client = this.createClientForProxyToken(token);
|
|
|
|
this.clients.set(token.secret, client);
|
2022-08-16 15:33:33 +02:00
|
|
|
}
|
|
|
|
|
2023-04-04 09:32:35 +02:00
|
|
|
return client;
|
2022-08-16 15:33:33 +02:00
|
|
|
}
|
|
|
|
|
2023-11-03 17:36:50 +01:00
|
|
|
private async createClientForProxyToken(token: IApiUser): Promise<Unleash> {
|
2022-08-16 15:33:33 +02:00
|
|
|
const repository = new ProxyRepository(
|
|
|
|
this.config,
|
|
|
|
this.stores,
|
|
|
|
this.services,
|
|
|
|
token,
|
|
|
|
);
|
|
|
|
|
2023-02-10 10:51:53 +01:00
|
|
|
const client = new Unleash({
|
2022-08-16 15:33:33 +02:00
|
|
|
appName: 'proxy',
|
|
|
|
url: 'unused',
|
|
|
|
storageProvider: new InMemStorageProvider(),
|
|
|
|
disableMetrics: true,
|
|
|
|
repository,
|
|
|
|
});
|
|
|
|
|
|
|
|
client.on(UnleashEvents.Error, (error) => {
|
|
|
|
this.logger.error(error);
|
|
|
|
});
|
|
|
|
|
2023-02-10 10:51:53 +01:00
|
|
|
await client.start();
|
|
|
|
|
2022-08-16 15:33:33 +02:00
|
|
|
return client;
|
|
|
|
}
|
|
|
|
|
2023-04-04 09:32:35 +02:00
|
|
|
async deleteClientForProxyToken(secret: string): Promise<void> {
|
2023-09-29 14:18:21 +02:00
|
|
|
const clientPromise = this.clients.get(secret);
|
2023-04-04 09:32:35 +02:00
|
|
|
if (clientPromise) {
|
|
|
|
const client = await clientPromise;
|
|
|
|
client.destroy();
|
|
|
|
this.clients.delete(secret);
|
|
|
|
}
|
2022-08-22 15:02:39 +02:00
|
|
|
}
|
|
|
|
|
2022-09-28 14:23:41 +02:00
|
|
|
stopAll(): void {
|
2023-04-04 09:32:35 +02:00
|
|
|
this.clients.forEach((promise) => promise.then((c) => c.destroy()));
|
2022-09-28 14:23:41 +02:00
|
|
|
}
|
|
|
|
|
2023-11-03 17:36:50 +01:00
|
|
|
private static assertExpectedTokenType({ type }: IApiUser) {
|
2023-02-13 08:40:04 +01:00
|
|
|
if (!(type === ApiTokenType.FRONTEND || type === ApiTokenType.ADMIN)) {
|
|
|
|
throw new InvalidTokenError();
|
|
|
|
}
|
2022-08-16 15:33:33 +02:00
|
|
|
}
|
2022-12-14 17:35:22 +01:00
|
|
|
|
|
|
|
async setFrontendSettings(
|
|
|
|
value: FrontendSettings,
|
|
|
|
createdBy: string,
|
2023-12-14 13:45:25 +01:00
|
|
|
createdByUserId: number,
|
2022-12-14 17:35:22 +01:00
|
|
|
): Promise<void> {
|
|
|
|
const error = validateOrigins(value.frontendApiOrigins);
|
|
|
|
if (error) {
|
|
|
|
throw new BadDataError(error);
|
|
|
|
}
|
|
|
|
await this.services.settingService.insert(
|
|
|
|
frontendSettingsKey,
|
|
|
|
value,
|
|
|
|
createdBy,
|
2023-12-14 13:45:25 +01:00
|
|
|
createdByUserId,
|
2023-11-28 13:47:51 +01:00
|
|
|
false,
|
2022-12-14 17:35:22 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-10-23 15:11:38 +02:00
|
|
|
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();
|
|
|
|
}
|
2022-08-16 15:33:33 +02:00
|
|
|
}
|