diff --git a/src/lib/db/feature-toggle-client-store.ts b/src/lib/db/feature-toggle-client-store.ts index 659d354241..285d0d7163 100644 --- a/src/lib/db/feature-toggle-client-store.ts +++ b/src/lib/db/feature-toggle-client-store.ts @@ -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)[]; @@ -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): IStrategyConfig { - return { + private rowToStrategy(row: Record): 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): ITag { diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index c69bffd9c4..6b8bbff505 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -93,6 +93,7 @@ export const createStores = ( db, eventBus, getLogger, + config.flagResolver, ), environmentStore: new EnvironmentStore(db, eventBus, getLogger), featureTagStore: new FeatureTagStore(db, eventBus, getLogger), diff --git a/src/lib/db/segment-store.ts b/src/lib/db/segment-store.ts index 956080edfb..1dc880e54a 100644 --- a/src/lib/db/segment-store.ts +++ b/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'; @@ -150,6 +155,16 @@ export default class SegmentStore implements ISegmentStore { return rows.map(this.mapRow); } + async getActiveForClient(): Promise { + const fullSegments = await this.getActive(); + + return fullSegments.map((segments) => ({ + id: segments.id, + name: segments.name, + constraints: segments.constraints, + })); + } + async getByStrategy(strategyId: string): Promise { const rows = await this.db .select(this.prefixColumns()) diff --git a/src/lib/features/feature-toggle/createFeatureToggleService.ts b/src/lib/features/feature-toggle/createFeatureToggleService.ts index 67829f12fd..faa220ae77 100644 --- a/src/lib/features/feature-toggle/createFeatureToggleService.ts +++ b/src/lib/features/feature-toggle/createFeatureToggleService.ts @@ -55,6 +55,7 @@ export const createFeatureToggleService = ( db, eventBus, getLogger, + flagResolver, ); const projectStore = new ProjectStore( db, diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index dea596dd27..9772f3849b 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -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. diff --git a/src/lib/openapi/spec/client-features-schema.test.ts b/src/lib/openapi/spec/client-features-schema.test.ts index 7ca5d681b3..ddb8ad42f9 100644 --- a/src/lib/openapi/spec/client-features-schema.test.ts +++ b/src/lib/openapi/spec/client-features-schema.test.ts @@ -151,7 +151,6 @@ test('clientFeaturesSchema unleash-proxy expected response', () => { { "id": 1, "name": "some-name", - "description": null, "constraints": [ { "contextName": "some-name", diff --git a/src/lib/openapi/spec/client-features-schema.ts b/src/lib/openapi/spec/client-features-schema.ts index 44a795d73c..66ffda0d57 100644 --- a/src/lib/openapi/spec/client-features-schema.ts +++ b/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'; @@ -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, diff --git a/src/lib/openapi/spec/client-segment-schema.ts b/src/lib/openapi/spec/client-segment-schema.ts new file mode 100644 index 0000000000..904065f92b --- /dev/null +++ b/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; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index 2f271284ce..d20a8acf8b 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -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'; diff --git a/src/lib/openapi/spec/segment-schema.ts b/src/lib/openapi/spec/segment-schema.ts index 52d287b53e..1963d26188 100644 --- a/src/lib/openapi/spec/segment-schema.ts +++ b/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', @@ -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: { diff --git a/src/lib/routes/client-api/feature.test.ts b/src/lib/routes/client-api/feature.test.ts index 83ffa23a31..c4e5324d57 100644 --- a/src/lib/routes/client-api/feature.test.ts +++ b/src/lib/routes/client-api/feature.test.ts @@ -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 }; diff --git a/src/lib/routes/client-api/feature.ts b/src/lib/routes/client-api/feature.ts index b4aca26cfc..79713876b4 100644 --- a/src/lib/routes/client-api/feature.ts +++ b/src/lib/routes/client-api/feature.ts @@ -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(), ]); } diff --git a/src/lib/segments/segment-service-interface.ts b/src/lib/segments/segment-service-interface.ts index 3f3ba205f2..2a547e66b0 100644 --- a/src/lib/segments/segment-service-interface.ts +++ b/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: ( @@ -19,6 +19,8 @@ export interface ISegmentService { getActive(): Promise; + getActiveForClient(): Promise; + getAll(): Promise; create( diff --git a/src/lib/services/segment-service.ts b/src/lib/services/segment-service.ts index b65e0a2697..72a1d87ad6 100644 --- a/src/lib/services/segment-service.ts +++ b/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'; @@ -57,6 +57,10 @@ export class SegmentService implements ISegmentService { return this.segmentStore.getActive(); } + async getActiveForClient(): Promise { + return this.segmentStore.getActiveForClient(); + } + // Used by unleash-enterprise. async getByStrategy(strategyId: string): Promise { return this.segmentStore.getByStrategy(strategyId); diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 3a8a5c942b..e13a18100c 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -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; diff --git a/src/lib/types/stores/segment-store.ts b/src/lib/types/stores/segment-store.ts index c28185140d..dea16c374b 100644 --- a/src/lib/types/stores/segment-store.ts +++ b/src/lib/types/stores/segment-store.ts @@ -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 { getActive(): Promise; + getActiveForClient(): Promise; + getByStrategy(strategyId: string): Promise; create( diff --git a/src/test/fixtures/fake-segment-store.ts b/src/test/fixtures/fake-segment-store.ts index 620c767ca3..da1b73b759 100644 --- a/src/test/fixtures/fake-segment-store.ts +++ b/src/test/fixtures/fake-segment-store.ts @@ -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 { @@ -34,6 +38,10 @@ export default class FakeSegmentStore implements ISegmentStore { return []; } + async getActiveForClient(): Promise { + return []; + } + async getByStrategy(): Promise { return []; }