Skip to content

Commit

Permalink
feat: client api with proper client segments and strategy variants (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Jul 14, 2023
1 parent 4cd4153 commit e8ea79c
Show file tree
Hide file tree
Showing 17 changed files with 136 additions and 38 deletions.
24 changes: 18 additions & 6 deletions src/lib/db/feature-toggle-client-store.ts
Expand Up @@ -6,6 +6,7 @@ import {
IFeatureToggleClient,
IFeatureToggleClientStore,
IFeatureToggleQuery,
IFlagResolver,
IStrategyConfig,
ITag,
PartialDeep,
Expand Down Expand Up @@ -38,14 +39,22 @@ 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) =>
metricsHelper.wrapTimer(eventBus, DB_TIME, {
store: 'feature-toggle',
action,
});
this.flagResolver = flagResolver;
}

private async getAll({
Expand Down Expand Up @@ -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>)[];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -233,15 +241,19 @@ 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,
constraints: row.constraints || [],
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 {
Expand Down
1 change: 1 addition & 0 deletions src/lib/db/index.ts
Expand Up @@ -93,6 +93,7 @@ export const createStores = (
db,
eventBus,
getLogger,
config.flagResolver,
),
environmentStore: new EnvironmentStore(db, eventBus, getLogger),
featureTagStore: new FeatureTagStore(db, eventBus, getLogger),
Expand Down
17 changes: 16 additions & 1 deletion src/lib/db/segment-store.ts
@@ -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';
Expand Down Expand Up @@ -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())
Expand Down
Expand Up @@ -55,6 +55,7 @@ export const createFeatureToggleService = (
db,
eventBus,
getLogger,
flagResolver,
);
const projectStore = new ProjectStore(
db,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/openapi/index.ts
Expand Up @@ -150,6 +150,7 @@ import {
telemetrySettingsSchema,
strategyVariantSchema,
createStrategyVariantSchema,
clientSegmentSchema,
} from './spec';
import { IServerOption } from '../types';
import { mapValues, omitKeys } from '../util';
Expand Down Expand Up @@ -357,6 +358,7 @@ export const schemas: UnleashSchemas = {
telemetrySettingsSchema,
strategyVariantSchema,
createStrategyVariantSchema,
clientSegmentSchema,
};

// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
Expand Down
1 change: 0 additions & 1 deletion src/lib/openapi/spec/client-features-schema.test.ts
Expand Up @@ -151,7 +151,6 @@ test('clientFeaturesSchema unleash-proxy expected response', () => {
{
"id": 1,
"name": "some-name",
"description": null,
"constraints": [
{
"contextName": "some-name",
Expand Down
6 changes: 3 additions & 3 deletions src/lib/openapi/spec/client-features-schema.ts
@@ -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';
Expand Down Expand Up @@ -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: {
Expand All @@ -50,7 +50,7 @@ export const clientFeaturesSchema = {
constraintSchema,
clientFeatureSchema,
environmentSchema,
segmentSchema,
clientSegmentSchema,
clientFeaturesQuerySchema,
overrideSchema,
parametersSchema,
Expand Down
37 changes: 37 additions & 0 deletions src/lib/openapi/spec/client-segment-schema.ts
@@ -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>;
1 change: 1 addition & 0 deletions src/lib/openapi/spec/index.ts
Expand Up @@ -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';
33 changes: 18 additions & 15 deletions src/lib/openapi/spec/segment-schema.ts
@@ -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',
Expand All @@ -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: {
Expand Down
11 changes: 8 additions & 3 deletions src/lib/routes/client-api/feature.test.ts
Expand Up @@ -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,
},
);
};

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 };

Expand Down
10 changes: 5 additions & 5 deletions src/lib/routes/client-api/feature.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -58,7 +58,7 @@ export default class FeatureController extends Controller {
private featuresAndSegments: (
query: IFeatureToggleQuery,
etag: string,
) => Promise<[FeatureConfigurationClient[], ISegment[]]>;
) => Promise<[FeatureConfigurationClient[], IClientSegment[]]>;

constructor(
{
Expand Down Expand Up @@ -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(),
]);
}

Expand Down
4 changes: 3 additions & 1 deletion src/lib/segments/segment-service-interface.ts
@@ -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: (
Expand All @@ -19,6 +19,8 @@ export interface ISegmentService {

getActive(): Promise<ISegment[]>;

getActiveForClient(): Promise<IClientSegment[]>;

getAll(): Promise<ISegment[]>;

create(
Expand Down
6 changes: 5 additions & 1 deletion src/lib/services/segment-service.ts
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions src/lib/types/model.ts
Expand Up @@ -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;
Expand Down

0 comments on commit e8ea79c

Please sign in to comment.