Skip to content

Commit

Permalink
feat: move delta controller to new path (#8981)
Browse files Browse the repository at this point in the history
Feature delta is now at api//client/delta
  • Loading branch information
sjaanus authored Dec 16, 2024
1 parent 3afcf69 commit eb0699c
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand Down
56 changes: 56 additions & 0 deletions src/lib/openapi/spec/client-features-delta-schema.ts
Original file line number Diff line number Diff line change
@@ -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
>;
1 change: 1 addition & 0 deletions src/lib/openapi/spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 5 additions & 0 deletions src/lib/routes/client-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit eb0699c

Please sign in to comment.