1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-28 00:17:12 +01:00

feat: move delta controller to new path (#8981)

Feature delta is now at api//client/delta
This commit is contained in:
Jaanus Sellin 2024-12-16 08:46:15 +02:00 committed by GitHub
parent 3afcf690de
commit eb0699ca03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 248 additions and 59 deletions

View File

@ -0,0 +1,183 @@
import type { Response } from 'express';
import Controller from '../../../routes/controller';
import type {
IFlagResolver,
IUnleashConfig,
IUnleashServices,
} from '../../../types';
import type { Logger } from '../../../logger';
import { querySchema } from '../../../schema/feature-schema';
import type { IFeatureToggleQuery } from '../../../types/model';
import NotFoundError from '../../../error/notfound-error';
import type { IAuthRequest } from '../../../routes/unleash-types';
import ApiUser from '../../../types/api-user';
import { ALL, isAllProjects } from '../../../types/models/api-token';
import type { ClientSpecService } from '../../../services/client-spec-service';
import type { OpenApiService } from '../../../services/openapi-service';
import { NONE } from '../../../types/permissions';
import { createResponseSchema } from '../../../openapi/util/create-response-schema';
import type { ClientFeatureToggleService } from '../client-feature-toggle-service';
import type { RevisionCacheEntry } from './client-feature-toggle-cache';
import { clientFeaturesDeltaSchema } from '../../../openapi';
import type { QueryOverride } from '../client-feature-toggle.controller';
export default class ClientFeatureToggleDeltaController extends Controller {
private readonly logger: Logger;
private clientFeatureToggleService: ClientFeatureToggleService;
private clientSpecService: ClientSpecService;
private openApiService: OpenApiService;
private flagResolver: IFlagResolver;
constructor(
{
clientFeatureToggleService,
clientSpecService,
openApiService,
}: Pick<
IUnleashServices,
| 'clientFeatureToggleService'
| 'clientSpecService'
| 'openApiService'
| 'featureToggleService'
>,
config: IUnleashConfig,
) {
super(config);
this.clientFeatureToggleService = clientFeatureToggleService;
this.clientSpecService = clientSpecService;
this.openApiService = openApiService;
this.flagResolver = config.flagResolver;
this.logger = config.getLogger('client-api/delta.js');
this.route({
method: 'get',
path: '',
handler: this.getDelta,
permission: NONE,
middleware: [
openApiService.validPath({
summary: 'Get partial updates (SDK)',
description:
'Initially returns the full set of feature flags available to the provided API key. When called again with the returned etag, only returns the flags that have changed',
operationId: 'getDelta',
tags: ['Unstable'],
responses: {
200: createResponseSchema('clientFeaturesDeltaSchema'),
},
}),
],
});
}
private async resolveQuery(
req: IAuthRequest,
): Promise<IFeatureToggleQuery> {
const { user, query } = req;
const override: QueryOverride = {};
if (user instanceof ApiUser) {
if (!isAllProjects(user.projects)) {
override.project = user.projects;
}
if (user.environment !== ALL) {
override.environment = user.environment;
}
}
const inlineSegmentConstraints =
!this.clientSpecService.requestSupportsSpec(req, 'segments');
return this.prepQuery({
...query,
...override,
inlineSegmentConstraints,
});
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
private paramToArray(param: any) {
if (!param) {
return param;
}
return Array.isArray(param) ? param : [param];
}
private async prepQuery({
tag,
project,
namePrefix,
environment,
inlineSegmentConstraints,
}: IFeatureToggleQuery): Promise<IFeatureToggleQuery> {
if (
!tag &&
!project &&
!namePrefix &&
!environment &&
!inlineSegmentConstraints
) {
return {};
}
const tagQuery = this.paramToArray(tag);
const projectQuery = this.paramToArray(project);
const query = await querySchema.validateAsync({
tag: tagQuery,
project: projectQuery,
namePrefix,
environment,
inlineSegmentConstraints,
});
if (query.tag) {
query.tag = query.tag.map((q) => q.split(':'));
}
return query;
}
async getDelta(
req: IAuthRequest,
res: Response<RevisionCacheEntry>,
): Promise<void> {
if (!this.flagResolver.isEnabled('deltaApi')) {
throw new NotFoundError();
}
const query = await this.resolveQuery(req);
const etag = req.headers['if-none-match'];
const currentSdkRevisionId = etag ? Number.parseInt(etag) : undefined;
const changedFeatures =
await this.clientFeatureToggleService.getClientDelta(
currentSdkRevisionId,
query,
);
if (!changedFeatures) {
res.status(304);
res.getHeaderNames().forEach((header) => res.removeHeader(header));
res.end();
return;
}
if (changedFeatures.revisionId === currentSdkRevisionId) {
res.status(304);
res.getHeaderNames().forEach((header) => res.removeHeader(header));
res.end();
return;
}
res.setHeader('ETag', changedFeatures.revisionId.toString());
this.openApiService.respondWithValidation(
200,
res,
clientFeaturesDeltaSchema.$id,
changedFeatures,
);
}
}

View File

@ -33,11 +33,10 @@ import {
} from '../../openapi/spec/client-features-schema';
import type ConfigurationRevisionService from '../feature-toggle/configuration-revision-service';
import type { ClientFeatureToggleService } from './client-feature-toggle-service';
import type { RevisionCacheEntry } from './cache/client-feature-toggle-cache';
const version = 2;
interface QueryOverride {
export interface QueryOverride {
project?: string[];
environment?: string;
}
@ -95,25 +94,6 @@ export default class FeatureController extends Controller {
this.flagResolver = config.flagResolver;
this.logger = config.getLogger('client-api/feature.js');
this.route({
method: 'get',
path: '/delta',
handler: this.getDelta,
permission: NONE,
middleware: [
openApiService.validPath({
summary: 'Get partial updates (SDK)',
description:
'Initially returns the full set of feature flags available to the provided API key. When called again with the returned etag, only returns the flags that have changed',
operationId: 'getDelta',
tags: ['Unstable'],
responses: {
200: createResponseSchema('clientFeaturesSchema'),
},
}),
],
});
this.route({
method: 'get',
path: '/:featureName',
@ -298,42 +278,6 @@ export default class FeatureController extends Controller {
}
}
async getDelta(
req: IAuthRequest,
res: Response<RevisionCacheEntry>,
): Promise<void> {
if (!this.flagResolver.isEnabled('deltaApi')) {
throw new NotFoundError();
}
const query = await this.resolveQuery(req);
const etag = req.headers['if-none-match'];
const currentSdkRevisionId = etag ? Number.parseInt(etag) : undefined;
const changedFeatures =
await this.clientFeatureToggleService.getClientDelta(
currentSdkRevisionId,
query,
);
if (!changedFeatures) {
res.status(304);
res.getHeaderNames().forEach((header) => res.removeHeader(header));
res.end();
return;
}
if (changedFeatures.revisionId === currentSdkRevisionId) {
res.status(304);
res.getHeaderNames().forEach((header) => res.removeHeader(header));
res.end();
return;
}
res.setHeader('ETag', changedFeatures.revisionId.toString());
res.send(changedFeatures);
}
async calculateMeta(query: IFeatureToggleQuery): Promise<IMeta> {
// TODO: We will need to standardize this to be able to implement this a cross languages (Edge in Rust?).
const revisionId =

View File

@ -324,7 +324,7 @@ test('should match snapshot from /api/client/features', async () => {
expect(result.body).toMatchSnapshot();
});
test('should match with /api/client/features/delta', async () => {
test('should match with /api/client/delta', async () => {
await setupFeatures(db, app);
const { body } = await app.request
@ -333,7 +333,7 @@ test('should match with /api/client/features/delta', async () => {
.expect(200);
const { body: deltaBody } = await app.request
.get('/api/client/features/delta')
.get('/api/client/delta')
.expect('Content-Type', /json/)
.expect(200);

View File

@ -0,0 +1,56 @@
import type { FromSchema } from 'json-schema-to-ts';
import { constraintSchema } from './constraint-schema';
import { clientFeatureSchema } from './client-feature-schema';
import { environmentSchema } from './environment-schema';
import { clientSegmentSchema } from './client-segment-schema';
import { overrideSchema } from './override-schema';
import { parametersSchema } from './parameters-schema';
import { featureStrategySchema } from './feature-strategy-schema';
import { strategyVariantSchema } from './strategy-variant-schema';
import { variantSchema } from './variant-schema';
import { dependentFeatureSchema } from './dependent-feature-schema';
export const clientFeaturesDeltaSchema = {
$id: '#/components/schemas/clientFeaturesDeltaSchema',
type: 'object',
required: ['updated', 'revisionId', 'removed'],
description: 'Schema for delta updates of feature configurations.',
properties: {
updated: {
description: 'A list of updated feature configurations.',
type: 'array',
items: {
$ref: '#/components/schemas/clientFeatureSchema',
},
},
revisionId: {
type: 'number',
description: 'The revision ID of the delta update.',
},
removed: {
description: 'A list of feature names that were removed.',
type: 'array',
items: {
type: 'string',
},
},
},
components: {
schemas: {
constraintSchema,
clientFeatureSchema,
environmentSchema,
clientSegmentSchema,
overrideSchema,
parametersSchema,
featureStrategySchema,
strategyVariantSchema,
variantSchema,
dependentFeatureSchema,
},
},
} as const;
export type ClientFeaturesDeltaSchema = FromSchema<
typeof clientFeaturesDeltaSchema
>;

View File

@ -37,6 +37,7 @@ export * from './bulk-toggle-features-schema';
export * from './change-password-schema';
export * from './client-application-schema';
export * from './client-feature-schema';
export * from './client-features-delta-schema';
export * from './client-features-query-schema';
export * from './client-features-schema';
export * from './client-metrics-env-schema';

View File

@ -3,11 +3,16 @@ import FeatureController from '../../features/client-feature-toggles/client-feat
import MetricsController from '../../features/metrics/instance/metrics';
import RegisterController from '../../features/metrics/instance/register';
import type { IUnleashConfig, IUnleashServices } from '../../types';
import ClientFeatureToggleDeltaController from '../../features/client-feature-toggles/cache/client-feature-toggle-cache-controller';
export default class ClientApi extends Controller {
constructor(config: IUnleashConfig, services: IUnleashServices) {
super(config);
this.use(
'/delta',
new ClientFeatureToggleDeltaController(services, config).router,
);
this.use('/features', new FeatureController(services, config).router);
this.use('/metrics', new MetricsController(services, config).router);
this.use('/register', new RegisterController(services, config).router);