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:
parent
3afcf690de
commit
eb0699ca03
183
src/lib/features/client-feature-toggles/cache/client-feature-toggle-cache-controller.ts
vendored
Normal file
183
src/lib/features/client-feature-toggles/cache/client-feature-toggle-cache-controller.ts
vendored
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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 =
|
||||
|
@ -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);
|
||||
|
||||
|
56
src/lib/openapi/spec/client-features-delta-schema.ts
Normal file
56
src/lib/openapi/spec/client-features-delta-schema.ts
Normal 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
|
||||
>;
|
@ -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';
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user