mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: client api with proper client segments and strategy variants (#4244)
This commit is contained in:
parent
4cd4153412
commit
e8ea79c967
@ -6,6 +6,7 @@ import {
|
||||
IFeatureToggleClient,
|
||||
IFeatureToggleClientStore,
|
||||
IFeatureToggleQuery,
|
||||
IFlagResolver,
|
||||
IStrategyConfig,
|
||||
ITag,
|
||||
PartialDeep,
|
||||
@ -38,7 +39,14 @@ export default class FeatureToggleClientStore
|
||||
|
||||
private timer: Function;
|
||||
|
||||
constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
|
||||
private flagResolver: IFlagResolver;
|
||||
|
||||
constructor(
|
||||
db: Db,
|
||||
eventBus: EventEmitter,
|
||||
getLogger: LogProvider,
|
||||
flagResolver: IFlagResolver,
|
||||
) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('feature-toggle-client-store.ts');
|
||||
this.timer = (action) =>
|
||||
@ -46,6 +54,7 @@ export default class FeatureToggleClientStore
|
||||
store: 'feature-toggle',
|
||||
action,
|
||||
});
|
||||
this.flagResolver = flagResolver;
|
||||
}
|
||||
|
||||
private async getAll({
|
||||
@ -78,6 +87,7 @@ export default class FeatureToggleClientStore
|
||||
'fs.parameters as parameters',
|
||||
'fs.constraints as constraints',
|
||||
'fs.sort_order as sort_order',
|
||||
'fs.variants as strategy_variants',
|
||||
'segments.id as segment_id',
|
||||
'segments.constraints as segment_constraints',
|
||||
] as (string | Raw<any>)[];
|
||||
@ -170,9 +180,7 @@ export default class FeatureToggleClientStore
|
||||
strategies: [],
|
||||
};
|
||||
if (this.isUnseenStrategyRow(feature, r) && !r.strategy_disabled) {
|
||||
feature.strategies?.push(
|
||||
FeatureToggleClientStore.rowToStrategy(r),
|
||||
);
|
||||
feature.strategies?.push(this.rowToStrategy(r));
|
||||
}
|
||||
if (this.isNewTag(feature, r)) {
|
||||
this.addTag(feature, r);
|
||||
@ -233,8 +241,8 @@ export default class FeatureToggleClientStore
|
||||
return cleanedFeatures;
|
||||
}
|
||||
|
||||
private static rowToStrategy(row: Record<string, any>): IStrategyConfig {
|
||||
return {
|
||||
private rowToStrategy(row: Record<string, any>): IStrategyConfig {
|
||||
const strategy: IStrategyConfig = {
|
||||
id: row.strategy_id,
|
||||
name: row.strategy_name,
|
||||
title: row.strategy_title,
|
||||
@ -242,6 +250,10 @@ export default class FeatureToggleClientStore
|
||||
parameters: mapValues(row.parameters || {}, ensureStringValue),
|
||||
sortOrder: row.sort_order,
|
||||
};
|
||||
if (this.flagResolver.isEnabled('strategyVariant')) {
|
||||
strategy.variants = row.strategy_variants || [];
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
|
||||
private static rowToTag(row: Record<string, any>): ITag {
|
||||
|
@ -93,6 +93,7 @@ export const createStores = (
|
||||
db,
|
||||
eventBus,
|
||||
getLogger,
|
||||
config.flagResolver,
|
||||
),
|
||||
environmentStore: new EnvironmentStore(db, eventBus, getLogger),
|
||||
featureTagStore: new FeatureTagStore(db, eventBus, getLogger),
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { ISegmentStore } from '../types/stores/segment-store';
|
||||
import { IConstraint, IFeatureStrategySegment, ISegment } from '../types/model';
|
||||
import {
|
||||
IClientSegment,
|
||||
IConstraint,
|
||||
IFeatureStrategySegment,
|
||||
ISegment,
|
||||
} from '../types/model';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
import EventEmitter from 'events';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
@ -150,6 +155,16 @@ export default class SegmentStore implements ISegmentStore {
|
||||
return rows.map(this.mapRow);
|
||||
}
|
||||
|
||||
async getActiveForClient(): Promise<IClientSegment[]> {
|
||||
const fullSegments = await this.getActive();
|
||||
|
||||
return fullSegments.map((segments) => ({
|
||||
id: segments.id,
|
||||
name: segments.name,
|
||||
constraints: segments.constraints,
|
||||
}));
|
||||
}
|
||||
|
||||
async getByStrategy(strategyId: string): Promise<ISegment[]> {
|
||||
const rows = await this.db
|
||||
.select(this.prefixColumns())
|
||||
|
@ -55,6 +55,7 @@ export const createFeatureToggleService = (
|
||||
db,
|
||||
eventBus,
|
||||
getLogger,
|
||||
flagResolver,
|
||||
);
|
||||
const projectStore = new ProjectStore(
|
||||
db,
|
||||
|
@ -150,6 +150,7 @@ import {
|
||||
telemetrySettingsSchema,
|
||||
strategyVariantSchema,
|
||||
createStrategyVariantSchema,
|
||||
clientSegmentSchema,
|
||||
} from './spec';
|
||||
import { IServerOption } from '../types';
|
||||
import { mapValues, omitKeys } from '../util';
|
||||
@ -357,6 +358,7 @@ export const schemas: UnleashSchemas = {
|
||||
telemetrySettingsSchema,
|
||||
strategyVariantSchema,
|
||||
createStrategyVariantSchema,
|
||||
clientSegmentSchema,
|
||||
};
|
||||
|
||||
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
||||
|
@ -151,7 +151,6 @@ test('clientFeaturesSchema unleash-proxy expected response', () => {
|
||||
{
|
||||
"id": 1,
|
||||
"name": "some-name",
|
||||
"description": null,
|
||||
"constraints": [
|
||||
{
|
||||
"contextName": "some-name",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { clientFeaturesQuerySchema } from './client-features-query-schema';
|
||||
import { segmentSchema } from './segment-schema';
|
||||
import { clientSegmentSchema } from './client-segment-schema';
|
||||
import { constraintSchema } from './constraint-schema';
|
||||
import { environmentSchema } from './environment-schema';
|
||||
import { overrideSchema } from './override-schema';
|
||||
@ -36,7 +36,7 @@ export const clientFeaturesSchema = {
|
||||
'A list of [Segments](https://docs.getunleash.io/reference/segments) configured for this Unleash instance',
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/segmentSchema',
|
||||
$ref: '#/components/schemas/clientSegmentSchema',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
@ -50,7 +50,7 @@ export const clientFeaturesSchema = {
|
||||
constraintSchema,
|
||||
clientFeatureSchema,
|
||||
environmentSchema,
|
||||
segmentSchema,
|
||||
clientSegmentSchema,
|
||||
clientFeaturesQuerySchema,
|
||||
overrideSchema,
|
||||
parametersSchema,
|
||||
|
37
src/lib/openapi/spec/client-segment-schema.ts
Normal file
37
src/lib/openapi/spec/client-segment-schema.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { constraintSchema } from './constraint-schema';
|
||||
|
||||
export const clientSegmentSchema = {
|
||||
$id: '#/components/schemas/clientSegmentSchema',
|
||||
type: 'object',
|
||||
description:
|
||||
'Represents a client API segment of users defined by a set of constraints.',
|
||||
additionalProperties: false,
|
||||
required: ['id', 'constraints'],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'number',
|
||||
description: "The segment's id.",
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The name of the segment.',
|
||||
example: 'segment A',
|
||||
},
|
||||
constraints: {
|
||||
type: 'array',
|
||||
description:
|
||||
'List of constraints that determine which users are part of the segment',
|
||||
items: {
|
||||
$ref: '#/components/schemas/constraintSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
constraintSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type ClientSegmentSchema = FromSchema<typeof clientSegmentSchema>;
|
@ -149,3 +149,4 @@ export * from './advanced-playground-request-schema';
|
||||
export * from './telemetry-settings-schema';
|
||||
export * from './create-strategy-variant-schema';
|
||||
export * from './strategy-variant-schema';
|
||||
export * from './client-segment-schema';
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { constraintSchema } from './constraint-schema';
|
||||
import { clientSegmentSchema } from './client-segment-schema';
|
||||
|
||||
export const segmentSchema = {
|
||||
$id: '#/components/schemas/segmentSchema',
|
||||
@ -9,28 +10,30 @@ export const segmentSchema = {
|
||||
additionalProperties: false,
|
||||
required: ['id', 'constraints'],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'number',
|
||||
description: "The segment's id.",
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'The name of the segment.',
|
||||
example: 'segment A',
|
||||
},
|
||||
...clientSegmentSchema.properties,
|
||||
description: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The description of the segment.',
|
||||
example: 'Segment A description',
|
||||
},
|
||||
constraints: {
|
||||
type: 'array',
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description:
|
||||
'List of constraints that determine which users are part of the segment',
|
||||
items: {
|
||||
$ref: '#/components/schemas/constraintSchema',
|
||||
},
|
||||
'The time the segment was created as a RFC 3339-conformant timestamp.',
|
||||
example: '2023-07-05T12:56:00.000Z',
|
||||
},
|
||||
createdBy: {
|
||||
type: 'string',
|
||||
description: 'Which user created this segment',
|
||||
example: 'johndoe',
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The project the segment relates to, if applicable.',
|
||||
example: 'default',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
@ -34,7 +34,10 @@ const callGetAll = async (controller: FeatureController) => {
|
||||
await controller.getAll(
|
||||
// @ts-expect-error
|
||||
{ query: {}, header: () => undefined, headers: {} },
|
||||
{ json: () => {}, setHeader: () => undefined },
|
||||
{
|
||||
json: () => {},
|
||||
setHeader: () => undefined,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@ -76,12 +79,13 @@ test('should get empty getFeatures via client', () => {
|
||||
test('if caching is enabled should memoize', async () => {
|
||||
const getClientFeatures = jest.fn().mockReturnValue([]);
|
||||
const getActive = jest.fn().mockReturnValue([]);
|
||||
const getActiveForClient = jest.fn().mockReturnValue([]);
|
||||
const respondWithValidation = jest.fn().mockReturnValue({});
|
||||
const validPath = jest.fn().mockReturnValue(jest.fn());
|
||||
const clientSpecService = new ClientSpecService({ getLogger });
|
||||
const openApiService = { respondWithValidation, validPath };
|
||||
const featureToggleServiceV2 = { getClientFeatures };
|
||||
const segmentService = { getActive };
|
||||
const segmentService = { getActive, getActiveForClient };
|
||||
const configurationRevisionService = { getMaxRevisionId: () => 1 };
|
||||
|
||||
const controller = new FeatureController(
|
||||
@ -114,11 +118,12 @@ test('if caching is enabled should memoize', async () => {
|
||||
test('if caching is not enabled all calls goes to service', async () => {
|
||||
const getClientFeatures = jest.fn().mockReturnValue([]);
|
||||
const getActive = jest.fn().mockReturnValue([]);
|
||||
const getActiveForClient = jest.fn().mockReturnValue([]);
|
||||
const respondWithValidation = jest.fn().mockReturnValue({});
|
||||
const validPath = jest.fn().mockReturnValue(jest.fn());
|
||||
const clientSpecService = new ClientSpecService({ getLogger });
|
||||
const featureToggleServiceV2 = { getClientFeatures };
|
||||
const segmentService = { getActive };
|
||||
const segmentService = { getActive, getActiveForClient };
|
||||
const openApiService = { respondWithValidation, validPath };
|
||||
const configurationRevisionService = { getMaxRevisionId: () => 1 };
|
||||
|
||||
|
@ -3,11 +3,11 @@ import { Response } from 'express';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import hashSum from 'hash-sum';
|
||||
import Controller from '../controller';
|
||||
import { IUnleashConfig, IUnleashServices } from '../../types';
|
||||
import { IClientSegment, IUnleashConfig, IUnleashServices } from '../../types';
|
||||
import FeatureToggleService from '../../services/feature-toggle-service';
|
||||
import { Logger } from '../../logger';
|
||||
import { querySchema } from '../../schema/feature-schema';
|
||||
import { IFeatureToggleQuery, ISegment } from '../../types/model';
|
||||
import { IFeatureToggleQuery } from '../../types/model';
|
||||
import NotFoundError from '../../error/notfound-error';
|
||||
import { IAuthRequest } from '../unleash-types';
|
||||
import ApiUser from '../../types/api-user';
|
||||
@ -58,7 +58,7 @@ export default class FeatureController extends Controller {
|
||||
private featuresAndSegments: (
|
||||
query: IFeatureToggleQuery,
|
||||
etag: string,
|
||||
) => Promise<[FeatureConfigurationClient[], ISegment[]]>;
|
||||
) => Promise<[FeatureConfigurationClient[], IClientSegment[]]>;
|
||||
|
||||
constructor(
|
||||
{
|
||||
@ -145,10 +145,10 @@ export default class FeatureController extends Controller {
|
||||
|
||||
private async resolveFeaturesAndSegments(
|
||||
query?: IFeatureToggleQuery,
|
||||
): Promise<[FeatureConfigurationClient[], ISegment[]]> {
|
||||
): Promise<[FeatureConfigurationClient[], IClientSegment[]]> {
|
||||
return Promise.all([
|
||||
this.featureToggleServiceV2.getClientFeatures(query),
|
||||
this.segmentService.getActive(),
|
||||
this.segmentService.getActiveForClient(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { UpsertSegmentSchema } from 'lib/openapi';
|
||||
import { IFeatureStrategy, ISegment, IUser } from 'lib/types';
|
||||
import { IClientSegment, IFeatureStrategy, ISegment, IUser } from 'lib/types';
|
||||
|
||||
export interface ISegmentService {
|
||||
updateStrategySegments: (
|
||||
@ -19,6 +19,8 @@ export interface ISegmentService {
|
||||
|
||||
getActive(): Promise<ISegment[]>;
|
||||
|
||||
getActiveForClient(): Promise<IClientSegment[]>;
|
||||
|
||||
getAll(): Promise<ISegment[]>;
|
||||
|
||||
create(
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { IUnleashConfig } from '../types/option';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import { IUnleashStores } from '../types';
|
||||
import { IClientSegment, IUnleashStores } from '../types';
|
||||
import { Logger } from '../logger';
|
||||
import NameExistsError from '../error/name-exists-error';
|
||||
import { ISegmentStore } from '../types/stores/segment-store';
|
||||
@ -57,6 +57,10 @@ export class SegmentService implements ISegmentService {
|
||||
return this.segmentStore.getActive();
|
||||
}
|
||||
|
||||
async getActiveForClient(): Promise<IClientSegment[]> {
|
||||
return this.segmentStore.getActiveForClient();
|
||||
}
|
||||
|
||||
// Used by unleash-enterprise.
|
||||
async getByStrategy(strategyId: string): Promise<ISegment[]> {
|
||||
return this.segmentStore.getByStrategy(strategyId);
|
||||
|
@ -418,6 +418,12 @@ export interface IProjectWithCount extends IProject {
|
||||
favorite?: boolean;
|
||||
}
|
||||
|
||||
export interface IClientSegment {
|
||||
id: number;
|
||||
constraints: IConstraint[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ISegment {
|
||||
id: number;
|
||||
name: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IFeatureStrategySegment, ISegment } from '../model';
|
||||
import { IClientSegment, IFeatureStrategySegment, ISegment } from '../model';
|
||||
import { Store } from './store';
|
||||
import User from '../user';
|
||||
|
||||
@ -7,6 +7,8 @@ export interface ISegmentStore extends Store<ISegment, number> {
|
||||
|
||||
getActive(): Promise<ISegment[]>;
|
||||
|
||||
getActiveForClient(): Promise<IClientSegment[]>;
|
||||
|
||||
getByStrategy(strategyId: string): Promise<ISegment[]>;
|
||||
|
||||
create(
|
||||
|
10
src/test/fixtures/fake-segment-store.ts
vendored
10
src/test/fixtures/fake-segment-store.ts
vendored
@ -1,5 +1,9 @@
|
||||
import { ISegmentStore } from '../../lib/types/stores/segment-store';
|
||||
import { IFeatureStrategySegment, ISegment } from '../../lib/types/model';
|
||||
import {
|
||||
IClientSegment,
|
||||
IFeatureStrategySegment,
|
||||
ISegment,
|
||||
} from '../../lib/types/model';
|
||||
|
||||
export default class FakeSegmentStore implements ISegmentStore {
|
||||
count(): Promise<number> {
|
||||
@ -34,6 +38,10 @@ export default class FakeSegmentStore implements ISegmentStore {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getActiveForClient(): Promise<IClientSegment[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getByStrategy(): Promise<ISegment[]> {
|
||||
return [];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user