mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-27 11:02:16 +01:00
refactor: Switch client feature toggles to segment read model (#6425)
This commit is contained in:
parent
62361847f5
commit
454f44dec5
@ -1,10 +1,5 @@
|
|||||||
import { ISegmentStore } from '../types/stores/segment-store';
|
import { ISegmentStore } from '../types/stores/segment-store';
|
||||||
import {
|
import { IConstraint, IFeatureStrategySegment, ISegment } from '../types/model';
|
||||||
IClientSegment,
|
|
||||||
IConstraint,
|
|
||||||
IFeatureStrategySegment,
|
|
||||||
ISegment,
|
|
||||||
} from '../types/model';
|
|
||||||
import { Logger, LogProvider } from '../logger';
|
import { Logger, LogProvider } from '../logger';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import NotFoundError from '../error/notfound-error';
|
import NotFoundError from '../error/notfound-error';
|
||||||
@ -284,30 +279,6 @@ export default class SegmentStore implements ISegmentStore {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActive(): Promise<ISegment[]> {
|
|
||||||
const rows: ISegmentRow[] = await this.db
|
|
||||||
.distinct(this.prefixColumns())
|
|
||||||
.from(T.segments)
|
|
||||||
.orderBy('name', 'asc')
|
|
||||||
.join(
|
|
||||||
T.featureStrategySegment,
|
|
||||||
`${T.featureStrategySegment}.segment_id`,
|
|
||||||
`${T.segments}.id`,
|
|
||||||
);
|
|
||||||
|
|
||||||
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[]> {
|
async getByStrategy(strategyId: string): Promise<ISegment[]> {
|
||||||
const rows = await this.db
|
const rows = await this.db
|
||||||
.select(this.prefixColumns())
|
.select(this.prefixColumns())
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
IFeatureToggleClientStore,
|
IFeatureToggleClientStore,
|
||||||
IFeatureToggleQuery,
|
IFeatureToggleQuery,
|
||||||
|
ISegmentReadModel,
|
||||||
IUnleashConfig,
|
IUnleashConfig,
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
@ -14,16 +15,24 @@ export class ClientFeatureToggleService {
|
|||||||
|
|
||||||
private clientFeatureToggleStore: IFeatureToggleClientStore;
|
private clientFeatureToggleStore: IFeatureToggleClientStore;
|
||||||
|
|
||||||
|
private segmentReadModel: ISegmentReadModel;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
clientFeatureToggleStore,
|
clientFeatureToggleStore,
|
||||||
}: Pick<IUnleashStores, 'clientFeatureToggleStore'>,
|
}: Pick<IUnleashStores, 'clientFeatureToggleStore'>,
|
||||||
|
segmentReadModel: ISegmentReadModel,
|
||||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
|
{ getLogger }: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
|
||||||
) {
|
) {
|
||||||
this.logger = getLogger('services/client-feature-toggle-service.ts');
|
this.logger = getLogger('services/client-feature-toggle-service.ts');
|
||||||
|
this.segmentReadModel = segmentReadModel;
|
||||||
this.clientFeatureToggleStore = clientFeatureToggleStore;
|
this.clientFeatureToggleStore = clientFeatureToggleStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getActiveSegmentsForClient() {
|
||||||
|
return this.segmentReadModel.getActiveForClient();
|
||||||
|
}
|
||||||
|
|
||||||
async getClientFeatures(
|
async getClientFeatures(
|
||||||
query?: IFeatureToggleQuery,
|
query?: IFeatureToggleQuery,
|
||||||
): Promise<FeatureConfigurationClient[]> {
|
): Promise<FeatureConfigurationClient[]> {
|
||||||
|
|||||||
@ -31,7 +31,6 @@ import {
|
|||||||
clientFeaturesSchema,
|
clientFeaturesSchema,
|
||||||
ClientFeaturesSchema,
|
ClientFeaturesSchema,
|
||||||
} from '../../openapi/spec/client-features-schema';
|
} from '../../openapi/spec/client-features-schema';
|
||||||
import { ISegmentService } from '../../segments/segment-service-interface';
|
|
||||||
import ConfigurationRevisionService from '../feature-toggle/configuration-revision-service';
|
import ConfigurationRevisionService from '../feature-toggle/configuration-revision-service';
|
||||||
import { ClientFeatureToggleService } from './client-feature-toggle-service';
|
import { ClientFeatureToggleService } from './client-feature-toggle-service';
|
||||||
|
|
||||||
@ -53,8 +52,6 @@ export default class FeatureController extends Controller {
|
|||||||
|
|
||||||
private clientFeatureToggleService: ClientFeatureToggleService;
|
private clientFeatureToggleService: ClientFeatureToggleService;
|
||||||
|
|
||||||
private segmentService: ISegmentService;
|
|
||||||
|
|
||||||
private clientSpecService: ClientSpecService;
|
private clientSpecService: ClientSpecService;
|
||||||
|
|
||||||
private openApiService: OpenApiService;
|
private openApiService: OpenApiService;
|
||||||
@ -73,7 +70,6 @@ export default class FeatureController extends Controller {
|
|||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
clientFeatureToggleService,
|
clientFeatureToggleService,
|
||||||
segmentService,
|
|
||||||
clientSpecService,
|
clientSpecService,
|
||||||
openApiService,
|
openApiService,
|
||||||
configurationRevisionService,
|
configurationRevisionService,
|
||||||
@ -81,7 +77,6 @@ export default class FeatureController extends Controller {
|
|||||||
}: Pick<
|
}: Pick<
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
| 'clientFeatureToggleService'
|
| 'clientFeatureToggleService'
|
||||||
| 'segmentService'
|
|
||||||
| 'clientSpecService'
|
| 'clientSpecService'
|
||||||
| 'openApiService'
|
| 'openApiService'
|
||||||
| 'configurationRevisionService'
|
| 'configurationRevisionService'
|
||||||
@ -92,7 +87,6 @@ export default class FeatureController extends Controller {
|
|||||||
super(config);
|
super(config);
|
||||||
const { clientFeatureCaching } = config;
|
const { clientFeatureCaching } = config;
|
||||||
this.clientFeatureToggleService = clientFeatureToggleService;
|
this.clientFeatureToggleService = clientFeatureToggleService;
|
||||||
this.segmentService = segmentService;
|
|
||||||
this.clientSpecService = clientSpecService;
|
this.clientSpecService = clientSpecService;
|
||||||
this.openApiService = openApiService;
|
this.openApiService = openApiService;
|
||||||
this.configurationRevisionService = configurationRevisionService;
|
this.configurationRevisionService = configurationRevisionService;
|
||||||
@ -162,7 +156,7 @@ export default class FeatureController extends Controller {
|
|||||||
): Promise<[FeatureConfigurationClient[], IClientSegment[]]> {
|
): Promise<[FeatureConfigurationClient[], IClientSegment[]]> {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this.clientFeatureToggleService.getClientFeatures(query),
|
this.clientFeatureToggleService.getClientFeatures(query),
|
||||||
this.segmentService.getActiveForClient(),
|
this.clientFeatureToggleService.getActiveSegmentsForClient(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { Db } from '../../db/db';
|
|||||||
import { IUnleashConfig } from '../../types';
|
import { IUnleashConfig } from '../../types';
|
||||||
import FakeClientFeatureToggleStore from './fakes/fake-client-feature-toggle-store';
|
import FakeClientFeatureToggleStore from './fakes/fake-client-feature-toggle-store';
|
||||||
import { ClientFeatureToggleService } from './client-feature-toggle-service';
|
import { ClientFeatureToggleService } from './client-feature-toggle-service';
|
||||||
|
import { SegmentReadModel } from '../segment/segment-read-model';
|
||||||
|
import { FakeSegmentReadModel } from '../segment/fake-segment-read-model';
|
||||||
|
|
||||||
export const createClientFeatureToggleService = (
|
export const createClientFeatureToggleService = (
|
||||||
db: Db,
|
db: Db,
|
||||||
@ -17,10 +19,13 @@ export const createClientFeatureToggleService = (
|
|||||||
flagResolver,
|
flagResolver,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const segmentReadModel = new SegmentReadModel(db);
|
||||||
|
|
||||||
const clientFeatureToggleService = new ClientFeatureToggleService(
|
const clientFeatureToggleService = new ClientFeatureToggleService(
|
||||||
{
|
{
|
||||||
clientFeatureToggleStore: featureToggleClientStore,
|
clientFeatureToggleStore: featureToggleClientStore,
|
||||||
},
|
},
|
||||||
|
segmentReadModel,
|
||||||
{ getLogger, flagResolver },
|
{ getLogger, flagResolver },
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -34,10 +39,13 @@ export const createFakeClientFeatureToggleService = (
|
|||||||
|
|
||||||
const fakeClientFeatureToggleStore = new FakeClientFeatureToggleStore();
|
const fakeClientFeatureToggleStore = new FakeClientFeatureToggleStore();
|
||||||
|
|
||||||
|
const fakeSegmentReadModel = new FakeSegmentReadModel();
|
||||||
|
|
||||||
const clientFeatureToggleService = new ClientFeatureToggleService(
|
const clientFeatureToggleService = new ClientFeatureToggleService(
|
||||||
{
|
{
|
||||||
clientFeatureToggleStore: fakeClientFeatureToggleStore,
|
clientFeatureToggleStore: fakeClientFeatureToggleStore,
|
||||||
},
|
},
|
||||||
|
fakeSegmentReadModel,
|
||||||
{ getLogger, flagResolver },
|
{ getLogger, flagResolver },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -70,15 +70,16 @@ test('should get empty getFeatures via client', () => {
|
|||||||
|
|
||||||
test('if caching is enabled should memoize', async () => {
|
test('if caching is enabled should memoize', async () => {
|
||||||
const getClientFeatures = jest.fn().mockReturnValue([]);
|
const getClientFeatures = jest.fn().mockReturnValue([]);
|
||||||
const getActive = jest.fn().mockReturnValue([]);
|
const getActiveSegmentsForClient = jest.fn().mockReturnValue([]);
|
||||||
const getActiveForClient = jest.fn().mockReturnValue([]);
|
|
||||||
const respondWithValidation = jest.fn().mockReturnValue({});
|
const respondWithValidation = jest.fn().mockReturnValue({});
|
||||||
const validPath = jest.fn().mockReturnValue(jest.fn());
|
const validPath = jest.fn().mockReturnValue(jest.fn());
|
||||||
const clientSpecService = new ClientSpecService({ getLogger });
|
const clientSpecService = new ClientSpecService({ getLogger });
|
||||||
const openApiService = { respondWithValidation, validPath };
|
const openApiService = { respondWithValidation, validPath };
|
||||||
const clientFeatureToggleService = { getClientFeatures };
|
const clientFeatureToggleService = {
|
||||||
|
getClientFeatures,
|
||||||
|
getActiveSegmentsForClient,
|
||||||
|
};
|
||||||
const featureToggleService = { getClientFeatures };
|
const featureToggleService = { getClientFeatures };
|
||||||
const segmentService = { getActive, getActiveForClient };
|
|
||||||
const configurationRevisionService = { getMaxRevisionId: () => 1 };
|
const configurationRevisionService = { getMaxRevisionId: () => 1 };
|
||||||
|
|
||||||
const controller = new FeatureController(
|
const controller = new FeatureController(
|
||||||
@ -91,8 +92,6 @@ test('if caching is enabled should memoize', async () => {
|
|||||||
// @ts-expect-error due to partial implementation
|
// @ts-expect-error due to partial implementation
|
||||||
featureToggleService,
|
featureToggleService,
|
||||||
// @ts-expect-error due to partial implementation
|
// @ts-expect-error due to partial implementation
|
||||||
segmentService,
|
|
||||||
// @ts-expect-error due to partial implementation
|
|
||||||
configurationRevisionService,
|
configurationRevisionService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -112,13 +111,14 @@ test('if caching is enabled should memoize', async () => {
|
|||||||
|
|
||||||
test('if caching is not enabled all calls goes to service', async () => {
|
test('if caching is not enabled all calls goes to service', async () => {
|
||||||
const getClientFeatures = jest.fn().mockReturnValue([]);
|
const getClientFeatures = jest.fn().mockReturnValue([]);
|
||||||
const getActive = jest.fn().mockReturnValue([]);
|
const getActiveSegmentsForClient = jest.fn().mockReturnValue([]);
|
||||||
const getActiveForClient = jest.fn().mockReturnValue([]);
|
|
||||||
const respondWithValidation = jest.fn().mockReturnValue({});
|
const respondWithValidation = jest.fn().mockReturnValue({});
|
||||||
const validPath = jest.fn().mockReturnValue(jest.fn());
|
const validPath = jest.fn().mockReturnValue(jest.fn());
|
||||||
const clientSpecService = new ClientSpecService({ getLogger });
|
const clientSpecService = new ClientSpecService({ getLogger });
|
||||||
const clientFeatureToggleService = { getClientFeatures };
|
const clientFeatureToggleService = {
|
||||||
const segmentService = { getActive, getActiveForClient };
|
getClientFeatures,
|
||||||
|
getActiveSegmentsForClient,
|
||||||
|
};
|
||||||
const featureToggleService = { getClientFeatures };
|
const featureToggleService = { getClientFeatures };
|
||||||
const openApiService = { respondWithValidation, validPath };
|
const openApiService = { respondWithValidation, validPath };
|
||||||
const configurationRevisionService = { getMaxRevisionId: () => 1 };
|
const configurationRevisionService = { getMaxRevisionId: () => 1 };
|
||||||
@ -133,8 +133,6 @@ test('if caching is not enabled all calls goes to service', async () => {
|
|||||||
// @ts-expect-error due to partial implementation
|
// @ts-expect-error due to partial implementation
|
||||||
featureToggleService,
|
featureToggleService,
|
||||||
// @ts-expect-error due to partial implementation
|
// @ts-expect-error due to partial implementation
|
||||||
segmentService,
|
|
||||||
// @ts-expect-error due to partial implementation
|
|
||||||
configurationRevisionService,
|
configurationRevisionService,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ChangeRequestStrategy } from '../features/change-request-segment-usage-service/change-request-segment-usage-read-model';
|
import { ChangeRequestStrategy } from '../features/change-request-segment-usage-service/change-request-segment-usage-read-model';
|
||||||
import { UpsertSegmentSchema } from '../openapi';
|
import { UpsertSegmentSchema } from '../openapi';
|
||||||
import { IClientSegment, IFeatureStrategy, ISegment, IUser } from '../types';
|
import { IFeatureStrategy, ISegment, IUser } from '../types';
|
||||||
|
|
||||||
export type StrategiesUsingSegment = {
|
export type StrategiesUsingSegment = {
|
||||||
strategies: IFeatureStrategy[];
|
strategies: IFeatureStrategy[];
|
||||||
@ -33,8 +33,6 @@ export interface ISegmentService {
|
|||||||
|
|
||||||
validateName(name: string): Promise<void>;
|
validateName(name: string): Promise<void>;
|
||||||
|
|
||||||
getActiveForClient(): Promise<IClientSegment[]>;
|
|
||||||
|
|
||||||
getAll(): Promise<ISegment[]>;
|
getAll(): Promise<ISegment[]>;
|
||||||
|
|
||||||
create(
|
create(
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { IUnleashConfig } from '../types/option';
|
import { IUnleashConfig } from '../types/option';
|
||||||
import {
|
import {
|
||||||
IClientSegment,
|
|
||||||
IFlagResolver,
|
IFlagResolver,
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
SKIP_CHANGE_REQUEST,
|
SKIP_CHANGE_REQUEST,
|
||||||
@ -79,10 +78,6 @@ export class SegmentService implements ISegmentService {
|
|||||||
return this.segmentStore.getAll(this.config.isEnterprise);
|
return this.segmentStore.getAll(this.config.isEnterprise);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActiveForClient(): Promise<IClientSegment[]> {
|
|
||||||
return this.segmentStore.getActiveForClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getByStrategy(strategyId: string): Promise<ISegment[]> {
|
async getByStrategy(strategyId: string): Promise<ISegment[]> {
|
||||||
return this.segmentStore.getByStrategy(strategyId);
|
return this.segmentStore.getByStrategy(strategyId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
import { IClientSegment, IFeatureStrategySegment, ISegment } from '../model';
|
import { IFeatureStrategySegment, ISegment } from '../model';
|
||||||
import { Store } from './store';
|
import { Store } from './store';
|
||||||
import User from '../user';
|
import User from '../user';
|
||||||
|
|
||||||
export interface ISegmentStore extends Store<ISegment, number> {
|
export interface ISegmentStore extends Store<ISegment, number> {
|
||||||
getAll(includeChangeRequestUsageData?: boolean): Promise<ISegment[]>;
|
getAll(includeChangeRequestUsageData?: boolean): Promise<ISegment[]>;
|
||||||
|
|
||||||
getActive(): Promise<ISegment[]>;
|
|
||||||
|
|
||||||
getActiveForClient(): Promise<IClientSegment[]>;
|
|
||||||
|
|
||||||
getByStrategy(strategyId: string): Promise<ISegment[]>;
|
getByStrategy(strategyId: string): Promise<ISegment[]>;
|
||||||
|
|
||||||
create(
|
create(
|
||||||
|
|||||||
14
src/test/fixtures/fake-segment-store.ts
vendored
14
src/test/fixtures/fake-segment-store.ts
vendored
@ -1,9 +1,5 @@
|
|||||||
import { ISegmentStore } from '../../lib/types/stores/segment-store';
|
import { ISegmentStore } from '../../lib/types/stores/segment-store';
|
||||||
import {
|
import { IFeatureStrategySegment, ISegment } from '../../lib/types/model';
|
||||||
IClientSegment,
|
|
||||||
IFeatureStrategySegment,
|
|
||||||
ISegment,
|
|
||||||
} from '../../lib/types/model';
|
|
||||||
|
|
||||||
export default class FakeSegmentStore implements ISegmentStore {
|
export default class FakeSegmentStore implements ISegmentStore {
|
||||||
count(): Promise<number> {
|
count(): Promise<number> {
|
||||||
@ -34,14 +30,6 @@ export default class FakeSegmentStore implements ISegmentStore {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActive(): Promise<ISegment[]> {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getActiveForClient(): Promise<IClientSegment[]> {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getByStrategy(): Promise<ISegment[]> {
|
async getByStrategy(): Promise<ISegment[]> {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user