diff --git a/backend/packages/Upgrade/src/api/DTO/ExperimentDTO.ts b/backend/packages/Upgrade/src/api/DTO/ExperimentDTO.ts index 14cccbf72c..b4158ca568 100644 --- a/backend/packages/Upgrade/src/api/DTO/ExperimentDTO.ts +++ b/backend/packages/Upgrade/src/api/DTO/ExperimentDTO.ts @@ -296,6 +296,14 @@ export class ParticipantsValidator { public segment: SegmentValidator; } +export class ParticipantsArrayValidator { + @IsNotEmpty() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SegmentValidator) + public segments: SegmentValidator[]; +} + class StateTimeLogValidator { @IsNotEmpty() @IsString() diff --git a/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts b/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts index 27a6d561c1..889d655682 100644 --- a/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts +++ b/backend/packages/Upgrade/src/api/controllers/FeatureFlagController.ts @@ -1,14 +1,16 @@ -import { JsonController, Authorized, Post, Body, CurrentUser, Delete, Param, Put, Req, Get } from 'routing-controllers'; +import { JsonController, Authorized, Post, Body, Delete, Put, Req, Get, Params } from 'routing-controllers'; import { FeatureFlagService } from '../services/FeatureFlagService'; import { FeatureFlag } from '../models/FeatureFlag'; -import { User } from '../models/User'; +import { FeatureFlagSegmentExclusion } from '../models/FeatureFlagSegmentExclusion'; +import { FeatureFlagSegmentInclusion } from '../models/FeatureFlagSegmentInclusion'; import { FeatureFlagStatusUpdateValidator } from './validators/FeatureFlagStatusUpdateValidator'; import { FeatureFlagPaginatedParamsValidator } from './validators/FeatureFlagsPaginatedParamsValidator'; import { AppRequest, PaginationResponse } from '../../types'; import { SERVER_ERROR } from 'upgrade_types'; -import { FeatureFlagValidation, UserParamsValidator } from './validators/FeatureFlagValidator'; +import { FeatureFlagValidation, IdValidator, UserParamsValidator } from './validators/FeatureFlagValidator'; import { ExperimentUserService } from '../services/ExperimentUserService'; -import { isUUID } from 'class-validator'; +import { FeatureFlagListValidator } from '../controllers/validators/FeatureFlagListValidator'; +import { Segment } from 'src/api/models/Segment'; interface FeatureFlagsPaginationInfo extends PaginationResponse { nodes: FeatureFlag[]; @@ -45,89 +47,63 @@ interface FeatureFlagsPaginationInfo extends PaginationResponse { * type: array * items: * type: string - * featureFlagSegmentInclusion: - * type: object - * properties: - * segment: - * type: object - * properties: - * type: - * type: string - * example: private - * individualForSegment: - * type: array - * items: - * type: object - * properties: - * userId: - * type: string - * example: user1 - * groupForSegment: - * type: array - * items: - * type: object - * properties: - * groupId: - * type: string - * example: school1 - * type: - * type: string - * example: schoolId - * subSegments: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * context: - * type: string - * featureFlagSegmentExclusion: + * filterMode: + * type: string + * enum: [includeAll, excludeAll] + * FeatureFlagInclusionExclusionList: + * required: + * - name + * - context + * - userIds + * - groups + * - subSegmentIds + * properties: + * id: + * type: string + * name: + * type: string + * description: + * type: string + * context: + * type: string + * userIds: + * type: array + * items: + * type: string + * groups: + * type: array + * items: * type: object * properties: - * segment: - * type: object - * properties: - * type: - * type: string - * example: private - * individualForSegment: - * type: array - * items: - * type: object - * properties: - * userId: - * type: string - * example: user1 - * groupForSegment: - * type: array - * items: - * type: object - * properties: - * groupId: - * type: string - * example: school1 - * type: - * type: string - * example: schoolId - * subSegments: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * name: - * type: string - * context: - * type: string + * groupId: + * type: string + * type: + * type: string + * subSegmentIds: + * type: array + * items: + * type: string + * FeatureFlagSegmentListInput: + * required: + * - flagId + * - enabled + * - listType + * - list + * properties: + * flagId: + * type: string + * enabled: + * type: boolean + * listType: + * type: string + * list: + * type: object + * $ref: '#/definitions/FeatureFlagInclusionExclusionList' */ /** * @swagger - * flags: + * tags: * - name: Feature Flags * description: Get Feature flags related data */ @@ -162,42 +138,6 @@ export class FeatureFlagsController { return this.featureFlagService.find(request.logger); } - /** - * @swagger - * /flags: - * post: - * description: Get feature flags for user - * consumes: - * - application/json - * parameters: - * - in: body - * name: user - * required: true - * schema: - * type: object - * properties: - * userId: - * type: string - * example: user1 - * context: - * type: string - * example: add - * description: User Document - * tags: - * - Feature Flags - * produces: - * - application/json - * responses: - * '200': - * description: Feature Flag List - * schema: - * type: array - * items: - * $ref: '#/definitions/FeatureFlag' - * '401': - * description: AuthorizationRequiredError - */ - @Post('/keys') public async getKeys( @Body({ validate: true }) @@ -244,14 +184,10 @@ export class FeatureFlagsController { * description: id should be of type UUID */ @Get('/:id') - public findOne(@Param('id') id: string, @Req() request: AppRequest): Promise { - if (!isUUID(id)) { - return Promise.reject( - new Error( - JSON.stringify({ type: SERVER_ERROR.INCORRECT_PARAM_FORMAT, message: ' : id should be of type UUID.' }) - ) - ); - } + public findOne( + @Params({ validate: true }) { id }: IdValidator, + @Req() request: AppRequest + ): Promise { return this.featureFlagService.findOne(id, request.logger); } @@ -359,7 +295,6 @@ export class FeatureFlagsController { @Post() public create( @Body({ validate: true }) flag: FeatureFlagValidation, - @CurrentUser() currentUser: User, @Req() request: AppRequest ): Promise { return this.featureFlagService.create(flag, request.logger); @@ -425,15 +360,10 @@ export class FeatureFlagsController { */ @Delete('/:id') - public delete(@Param('id') id: string, @Req() request: AppRequest): Promise { - // TODO: Add server error - // if (!isUUID(id)) { - // return Promise.reject( - // new Error( - // JSON.stringify({ type: SERVER_ERROR.INCORRECT_PARAM_FORMAT, message: ' : id should be of type UUID.' }) - // ) - // ); - // } + public delete( + @Params({ validate: true }) { id }: IdValidator, + @Req() request: AppRequest + ): Promise { return this.featureFlagService.delete(id, request.logger); } @@ -468,20 +398,205 @@ export class FeatureFlagsController { */ @Put('/:id') public update( - @Param('id') id: string, + @Params({ validate: true }) { id }: IdValidator, @Body({ validate: true }) flag: FeatureFlagValidation, - @CurrentUser() currentUser: User, @Req() request: AppRequest ): Promise { - // TODO: Add error log - // if (!isUUID(id)) { - // return Promise.reject( - // new Error( - // JSON.stringify({ type: SERVER_ERROR.INCORRECT_PARAM_FORMAT, message: ' : id should be of type UUID.' }) - // ) - // ); - // } return this.featureFlagService.update(flag, request.logger); } + + /** + * @swagger + * /flags/inclusionList: + * post: + * description: Add Feature Flag Inclusion List + * consumes: + * - application/json + * parameters: + * - in: body + * name: addinclusionList + * description: Adding an inclusion list to the feature flag + * schema: + * type: object + * $ref: '#/definitions/FeatureFlagSegmentListInput' + * tags: + * - Feature Flags + * produces: + * - application/json + * responses: + * '200': + * description: New Feature flag inclusion list is added + */ + @Post('/inclusionList') + public async addInclusionList( + @Body({ validate: true }) inclusionList: FeatureFlagListValidator, + @Req() request: AppRequest + ): Promise { + return this.featureFlagService.addList(inclusionList, 'inclusion', request.logger); + } + + /** + * @swagger + * /flags/exclusionList: + * post: + * description: Add Feature Flag Exclusion List + * consumes: + * - application/json + * parameters: + * - in: body + * name: addExclusionList + * description: Adding an exclusion list to the feature flag + * schema: + * type: object + * $ref: '#/definitions/FeatureFlagSegmentListInput' + * tags: + * - Feature Flags + * produces: + * - application/json + * responses: + * '200': + * description: New Feature flag exclusion list is added + */ + @Post('/exclusionList') + public async addExclusionList( + @Body({ validate: true }) exclusionList: FeatureFlagListValidator, + @Req() request: AppRequest + ): Promise { + return this.featureFlagService.addList(exclusionList, 'exclusion', request.logger); + } + + /** + * @swagger + * /flags/exclusionList: + * put: + * description: Update Feature Flag Exclusion List + * consumes: + * - application/json + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: ID of the segment + * - in: body + * name: updateExclusionList + * description: Updating an exclusion list on the feature flag + * schema: + * type: object + * $ref: '#/definitions/FeatureFlagSegmentListInput' + * tags: + * - Feature Flags + * produces: + * - application/json + * responses: + * '200': + * description: Feature flag exclusion list is updated + */ + @Put('/exclusionList/:id') + public async updateExclusionList( + @Params({ validate: true }) { id }: IdValidator, + @Body({ validate: true }) exclusionList: FeatureFlagListValidator, + @Req() request: AppRequest + ): Promise { + return this.featureFlagService.addList(exclusionList, 'exclusion', request.logger); + } + + /** + * @swagger + * /flags/exclusionList: + * put: + * description: Update Feature Flag Inclusion List + * consumes: + * - application/json + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: ID of the segment + * - in: body + * name: updateInclusionList + * description: Updating an inclusion list on the feature flag + * schema: + * type: object + * $ref: '#/definitions/FeatureFlagSegmentListInput' + * tags: + * - Feature Flags + * produces: + * - application/json + * responses: + * '200': + * description: Feature flag inclusion list is updated + */ + @Put('/inclusionList/:id') + public async updateInclusionList( + @Params({ validate: true }) { id }: IdValidator, + @Body({ validate: true }) inclusionList: FeatureFlagListValidator, + @Req() request: AppRequest + ): Promise { + return this.featureFlagService.addList(inclusionList, 'inclusion', request.logger); + } + + /** + * @swagger + * /flags/inclusionList: + * delete: + * description: Delete Feature Flag Inclusion List + * consumes: + * - application/json + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Segment Id of private segment + * tags: + * - Feature Flags + * produces: + * - application/json + * responses: + * '200': + * description: Delete Feature Flag Inclusion List by segment Id + */ + @Delete('/inclusionList/:id') + public async deleteInclusionList( + @Params({ validate: true }) { id }: IdValidator, + @Req() request: AppRequest + ): Promise { + return this.featureFlagService.deleteList(id, request.logger); + } + + /** + * @swagger + * /flags/exclusionList: + * delete: + * description: Delete Feature Flag Exclusion List + * consumes: + * - application/json + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: Segment Id of private segment + * tags: + * - Feature Flags + * produces: + * - application/json + * responses: + * '200': + * description: Delete Feature Flag Exclusion List by segment Id + */ + @Delete('/exclusionList/:id') + public async deleteExclusionList( + @Params({ validate: true }) { id }: IdValidator, + @Req() request: AppRequest + ): Promise { + return this.featureFlagService.deleteList(id, request.logger); + } } diff --git a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagListValidator.ts b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagListValidator.ts new file mode 100644 index 0000000000..9352688e90 --- /dev/null +++ b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagListValidator.ts @@ -0,0 +1,22 @@ +import { Type } from 'class-transformer'; +import { IsNotEmpty, IsDefined, IsUUID, IsBoolean, ValidateNested } from 'class-validator'; +import { SegmentInputValidator } from './SegmentInputValidator'; + +export class FeatureFlagListValidator { + @IsNotEmpty() + @IsUUID() + @IsDefined() + public flagId: string; + + @IsDefined() + @IsBoolean() + public enabled: boolean; + + @IsNotEmpty() + @IsDefined() + public listType: string; + + @ValidateNested() + @Type(() => SegmentInputValidator) + public list: SegmentInputValidator; +} diff --git a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts index f6a4e732fa..7dfa2c4095 100644 --- a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts +++ b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagValidator.ts @@ -1,5 +1,5 @@ -import { IsNotEmpty, IsDefined, IsString, IsArray, IsEnum, IsOptional, ValidateNested } from 'class-validator'; -import { ParticipantsValidator } from '../../DTO/ExperimentDTO'; +import { IsNotEmpty, IsDefined, IsString, IsArray, IsEnum, IsOptional, ValidateNested, IsUUID } from 'class-validator'; +import { ParticipantsArrayValidator } from '../../DTO/ExperimentDTO'; import { FILTER_MODE } from 'upgrade_types'; import { FEATURE_FLAG_STATUS } from 'upgrade_types'; import { Type } from 'class-transformer'; @@ -42,15 +42,15 @@ export class FeatureFlagValidation { @IsString({ each: true }) public tags: string[]; - @IsNotEmpty() + @IsOptional() @ValidateNested() - @Type(() => ParticipantsValidator) - public featureFlagSegmentInclusion: ParticipantsValidator; + @Type(() => ParticipantsArrayValidator) + public featureFlagSegmentInclusion?: ParticipantsArrayValidator; - @IsNotEmpty() + @IsOptional() @ValidateNested() - @Type(() => ParticipantsValidator) - public featureFlagSegmentExclusion: ParticipantsValidator; + @Type(() => ParticipantsArrayValidator) + public featureFlagSegmentExclusion?: ParticipantsArrayValidator; } export class UserParamsValidator { @@ -64,3 +64,10 @@ export class UserParamsValidator { @IsString() public context: string; } + +export class IdValidator { + @IsNotEmpty() + @IsDefined() + @IsUUID() + public id: string; +} diff --git a/backend/packages/Upgrade/src/api/models/FeatureFlag.ts b/backend/packages/Upgrade/src/api/models/FeatureFlag.ts index 43a52c5078..aa7b6489cd 100644 --- a/backend/packages/Upgrade/src/api/models/FeatureFlag.ts +++ b/backend/packages/Upgrade/src/api/models/FeatureFlag.ts @@ -1,10 +1,10 @@ -import { Column, Entity, PrimaryColumn, OneToOne } from 'typeorm'; +import { Column, Entity, PrimaryColumn, OneToMany } from 'typeorm'; import { IsNotEmpty } from 'class-validator'; import { BaseModel } from './base/BaseModel'; import { Type } from 'class-transformer'; +import { FEATURE_FLAG_STATUS, FILTER_MODE } from 'upgrade_types'; import { FeatureFlagSegmentInclusion } from './FeatureFlagSegmentInclusion'; import { FeatureFlagSegmentExclusion } from './FeatureFlagSegmentExclusion'; -import { FEATURE_FLAG_STATUS, FILTER_MODE } from 'upgrade_types'; @Entity() export class FeatureFlag extends BaseModel { @PrimaryColumn('uuid') @@ -42,11 +42,17 @@ export class FeatureFlag extends BaseModel { }) public filterMode: FILTER_MODE; - @OneToOne(() => FeatureFlagSegmentInclusion, (featureFlagSegmentInclusion) => featureFlagSegmentInclusion.featureFlag) + @OneToMany( + () => FeatureFlagSegmentInclusion, + (featureFlagSegmentInclusion) => featureFlagSegmentInclusion.featureFlag + ) @Type(() => FeatureFlagSegmentInclusion) - public featureFlagSegmentInclusion: FeatureFlagSegmentInclusion; + public featureFlagSegmentInclusion: FeatureFlagSegmentInclusion[]; - @OneToOne(() => FeatureFlagSegmentExclusion, (featureFlagSegmentExclusion) => featureFlagSegmentExclusion.featureFlag) + @OneToMany( + () => FeatureFlagSegmentExclusion, + (featureFlagSegmentExclusion) => featureFlagSegmentExclusion.featureFlag + ) @Type(() => FeatureFlagSegmentExclusion) - public featureFlagSegmentExclusion: FeatureFlagSegmentExclusion; + public featureFlagSegmentExclusion: FeatureFlagSegmentExclusion[]; } diff --git a/backend/packages/Upgrade/src/api/models/FeatureFlagSegmentExclusion.ts b/backend/packages/Upgrade/src/api/models/FeatureFlagSegmentExclusion.ts index 26b464a938..574dcb115d 100644 --- a/backend/packages/Upgrade/src/api/models/FeatureFlagSegmentExclusion.ts +++ b/backend/packages/Upgrade/src/api/models/FeatureFlagSegmentExclusion.ts @@ -1,4 +1,4 @@ -import { Entity, JoinColumn, OneToOne } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; import { BaseModel } from './base/BaseModel'; import { FeatureFlag } from './FeatureFlag'; import { Segment } from './Segment'; @@ -9,10 +9,18 @@ export class FeatureFlagSegmentExclusion extends BaseModel { @JoinColumn() public segment: Segment; - @OneToOne(() => FeatureFlag, (featureFlag) => featureFlag.featureFlagSegmentExclusion, { + @ManyToOne(() => FeatureFlag, (featureFlag) => featureFlag.featureFlagSegmentExclusion, { onDelete: 'CASCADE', primary: true, }) @JoinColumn() public featureFlag: FeatureFlag; + + @Column({ + default: false, + }) + public enabled: boolean; + + @Column() + public listType: string; } diff --git a/backend/packages/Upgrade/src/api/models/FeatureFlagSegmentInclusion.ts b/backend/packages/Upgrade/src/api/models/FeatureFlagSegmentInclusion.ts index 905631cc11..ddd529da03 100644 --- a/backend/packages/Upgrade/src/api/models/FeatureFlagSegmentInclusion.ts +++ b/backend/packages/Upgrade/src/api/models/FeatureFlagSegmentInclusion.ts @@ -1,4 +1,4 @@ -import { Entity, JoinColumn, OneToOne } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; import { BaseModel } from './base/BaseModel'; import { FeatureFlag } from './FeatureFlag'; import { Segment } from './Segment'; @@ -9,10 +9,18 @@ export class FeatureFlagSegmentInclusion extends BaseModel { @JoinColumn() public segment: Segment; - @OneToOne(() => FeatureFlag, (featureFlag) => featureFlag.featureFlagSegmentInclusion, { + @ManyToOne(() => FeatureFlag, (featureFlag) => featureFlag.featureFlagSegmentInclusion, { onDelete: 'CASCADE', primary: true, }) @JoinColumn() public featureFlag: FeatureFlag; + + @Column({ + default: false, + }) + public enabled: boolean; + + @Column() + public listType: string; } diff --git a/backend/packages/Upgrade/src/api/models/Segment.ts b/backend/packages/Upgrade/src/api/models/Segment.ts index 3a0b9d042f..dd7bd3b989 100644 --- a/backend/packages/Upgrade/src/api/models/Segment.ts +++ b/backend/packages/Upgrade/src/api/models/Segment.ts @@ -4,10 +4,10 @@ import { SEGMENT_TYPE } from 'upgrade_types'; import { BaseModel } from './base/BaseModel'; import { ExperimentSegmentExclusion } from './ExperimentSegmentExclusion'; import { ExperimentSegmentInclusion } from './ExperimentSegmentInclusion'; -import { FeatureFlagSegmentExclusion } from './FeatureFlagSegmentExclusion'; -import { FeatureFlagSegmentInclusion } from './FeatureFlagSegmentInclusion'; import { GroupForSegment } from './GroupForSegment'; import { IndividualForSegment } from './IndividualForSegment'; +import { FeatureFlagSegmentInclusion } from './FeatureFlagSegmentInclusion'; +import { FeatureFlagSegmentExclusion } from './FeatureFlagSegmentExclusion'; @Entity() export class Segment extends BaseModel { diff --git a/backend/packages/Upgrade/src/api/repositories/FeatureFlagRepository.ts b/backend/packages/Upgrade/src/api/repositories/FeatureFlagRepository.ts index 0176b73884..b1870c70f5 100644 --- a/backend/packages/Upgrade/src/api/repositories/FeatureFlagRepository.ts +++ b/backend/packages/Upgrade/src/api/repositories/FeatureFlagRepository.ts @@ -21,8 +21,9 @@ export class FeatureFlagRepository extends Repository { return result.raw; } - public async deleteById(id: string): Promise { - const result = await this.createQueryBuilder('featureFlag') + public async deleteById(id: string, entityManager: EntityManager): Promise { + const result = await entityManager + .createQueryBuilder() .delete() .from(FeatureFlag) .where('id = :id', { id }) diff --git a/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentExclusionRepository.ts b/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentExclusionRepository.ts index 8e78046141..95f36816d2 100644 --- a/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentExclusionRepository.ts +++ b/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentExclusionRepository.ts @@ -28,7 +28,6 @@ export class FeatureFlagSegmentExclusionRepository extends Repository { + public async deleteData(segmentId: string, logger: UpgradeLogger): Promise { const result = await this.createQueryBuilder() .delete() .from(FeatureFlagSegmentExclusion) - .where('segmentId=:segmentId AND featureFlagId=:featureFlagId', { segmentId, featureFlagId }) + .where('segmentId=:segmentId', { segmentId }) .returning('*') .execute() .catch((errorMsg: any) => { const errorMsgString = repositoryError( 'FeatureFlagSegmentExclusionRepository', 'deleteFeatureFlagSegmentExclusion', - { segmentId, featureFlagId }, + { segmentId }, errorMsg ); logger.error(errorMsg); diff --git a/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentInclusionRepository.ts b/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentInclusionRepository.ts index d6f29b9a46..ab032dd213 100644 --- a/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentInclusionRepository.ts +++ b/backend/packages/Upgrade/src/api/repositories/FeatureFlagSegmentInclusionRepository.ts @@ -47,22 +47,18 @@ export class FeatureFlagSegmentInclusionRepository extends Repository { + public async deleteData(segmentId: string, logger: UpgradeLogger): Promise { const result = await this.createQueryBuilder() .delete() .from(FeatureFlagSegmentInclusion) - .where('segmentId=:segmentId AND featureFlagId=:featureFlagId', { segmentId, featureFlagId }) + .where('segmentId=:segmentId', { segmentId }) .returning('*') .execute() .catch((errorMsg: any) => { const errorMsgString = repositoryError( 'FeatureFlagSegmentInclusionRepository', 'deleteFeatureFlagSegmentInclusion', - { segmentId, featureFlagId }, + { segmentId }, errorMsg ); logger.error(errorMsg); diff --git a/backend/packages/Upgrade/src/api/repositories/SegmentRepository.ts b/backend/packages/Upgrade/src/api/repositories/SegmentRepository.ts index be2503282c..7e93e57a68 100644 --- a/backend/packages/Upgrade/src/api/repositories/SegmentRepository.ts +++ b/backend/packages/Upgrade/src/api/repositories/SegmentRepository.ts @@ -76,7 +76,6 @@ export class SegmentRepository extends Repository { logger.error(errorMsg); throw errorMsgString; }); - return result.raw; } } diff --git a/backend/packages/Upgrade/src/api/services/ExperimentService.ts b/backend/packages/Upgrade/src/api/services/ExperimentService.ts index 2e789dec29..87a2d2fb8a 100644 --- a/backend/packages/Upgrade/src/api/services/ExperimentService.ts +++ b/backend/packages/Upgrade/src/api/services/ExperimentService.ts @@ -84,8 +84,6 @@ import { ArchivedStatsRepository } from '../repositories/ArchivedStatsRepository import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; import { StratificationFactorRepository } from '../repositories/StratificationFactorRepository'; -import { FeatureFlagSegmentExclusion } from '../models/FeatureFlagSegmentExclusion'; -import { FeatureFlagSegmentInclusion } from '../models/FeatureFlagSegmentInclusion'; import { ExperimentCSVData } from '../repositories/AnalyticsRepository'; const errorRemovePart = 'instance of ExperimentDTO has failed the validation:\n - '; @@ -1875,11 +1873,7 @@ export class ExperimentService { public includeExcludeSegmentCreation( experimentSegment: ParticipantsValidator, - experimentDocSegmentData: - | ExperimentSegmentInclusion - | ExperimentSegmentExclusion - | FeatureFlagSegmentExclusion - | FeatureFlagSegmentInclusion, + experimentDocSegmentData: ExperimentSegmentInclusion | ExperimentSegmentExclusion, experimentId: string, context: string[], isIncludeMode: boolean diff --git a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts index c97747aee6..7865548fe6 100644 --- a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts +++ b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts @@ -1,7 +1,12 @@ import { Service } from 'typedi'; import { FeatureFlag } from '../models/FeatureFlag'; +import { Segment } from '../models/Segment'; +import { FeatureFlagSegmentInclusion } from '../models/FeatureFlagSegmentInclusion'; +import { FeatureFlagSegmentExclusion } from '../models/FeatureFlagSegmentExclusion'; import { InjectRepository } from 'typeorm-typedi-extensions'; import { FeatureFlagRepository } from '../repositories/FeatureFlagRepository'; +import { FeatureFlagSegmentInclusionRepository } from '../repositories/FeatureFlagSegmentInclusionRepository'; +import { FeatureFlagSegmentExclusionRepository } from '../repositories/FeatureFlagSegmentExclusionRepository'; import { getConnection } from 'typeorm'; import { v4 as uuid } from 'uuid'; import { @@ -9,20 +14,14 @@ import { IFeatureFlagSortParams, FLAG_SEARCH_KEY, } from '../controllers/validators/FeatureFlagsPaginatedParamsValidator'; -import { SERVER_ERROR, FEATURE_FLAG_STATUS, SEGMENT_TYPE, FILTER_MODE } from 'upgrade_types'; +import { FeatureFlagListValidator } from '../controllers/validators/FeatureFlagListValidator'; +import { SERVER_ERROR, FEATURE_FLAG_STATUS, FILTER_MODE, SEGMENT_TYPE } from 'upgrade_types'; import { UpgradeLogger } from '../../lib/logger/UpgradeLogger'; import { FeatureFlagValidation } from '../controllers/validators/FeatureFlagValidator'; -import { FeatureFlagSegmentInclusion } from '../models/FeatureFlagSegmentInclusion'; -import { Segment } from '../models/Segment'; -import { SegmentInputValidator } from '../controllers/validators/SegmentInputValidator'; -import { ErrorWithType } from '../errors/ErrorWithType'; -import { FeatureFlagSegmentExclusion } from '../models/FeatureFlagSegmentExclusion'; -import { FeatureFlagSegmentExclusionRepository } from '../repositories/FeatureFlagSegmentExclusionRepository'; -import { FeatureFlagSegmentInclusionRepository } from '../repositories/FeatureFlagSegmentInclusionRepository'; -import { SegmentService } from './SegmentService'; -import { ExperimentService } from './ExperimentService'; import { ExperimentUser } from '../models/ExperimentUser'; import { ExperimentAssignmentService } from './ExperimentAssignmentService'; +import { SegmentService } from './SegmentService'; +import { ErrorWithType } from '../errors/ErrorWithType'; @Service() export class FeatureFlagService { @@ -30,9 +29,8 @@ export class FeatureFlagService { @InjectRepository() private featureFlagRepository: FeatureFlagRepository, @InjectRepository() private featureFlagSegmentInclusionRepository: FeatureFlagSegmentInclusionRepository, @InjectRepository() private featureFlagSegmentExclusionRepository: FeatureFlagSegmentExclusionRepository, - public segmentService: SegmentService, - public experimentService: ExperimentService, - public experimentAssignmentService: ExperimentAssignmentService + public experimentAssignmentService: ExperimentAssignmentService, + public segmentService: SegmentService ) {} public find(logger: UpgradeLogger): Promise { @@ -110,17 +108,39 @@ export class FeatureFlagService { public async delete(featureFlagId: string, logger: UpgradeLogger): Promise { logger.info({ message: `Delete Feature Flag => ${featureFlagId}` }); - const featureFlag = await this.featureFlagRepository.find({ - where: { id: featureFlagId }, + return getConnection().transaction(async (transactionalEntityManager) => { + const featureFlag = await this.findOne(featureFlagId, logger); + + if (featureFlag) { + const deletedFlag = await this.featureFlagRepository.deleteById(featureFlagId, transactionalEntityManager); + + featureFlag.featureFlagSegmentInclusion.forEach(async (segmentInclusion) => { + try { + await transactionalEntityManager.getRepository(Segment).delete(segmentInclusion.segment.id); + } catch (err) { + const error = err as ErrorWithType; + error.details = 'Error in deleting Feature Flag Included Segment fron DB'; + error.type = SERVER_ERROR.QUERY_FAILED; + logger.error(error); + throw error; + } + }); + featureFlag.featureFlagSegmentExclusion.forEach(async (segmentExclusion) => { + try { + await transactionalEntityManager.getRepository(Segment).delete(segmentExclusion.segment.id); + } catch (err) { + const error = err as ErrorWithType; + error.details = 'Error in deleting Feature Flag Excluded Segment fron DB'; + error.type = SERVER_ERROR.QUERY_FAILED; + logger.error(error); + throw error; + } + }); + // TODO: Add entry in audit log for delete feature flag + return deletedFlag; + } + return undefined; }); - - if (featureFlag) { - const deletedFlag = await this.featureFlagRepository.deleteById(featureFlagId); - - // TODO: Add entry in audit log for delete feature flag - return deletedFlag; - } - return undefined; } public async updateState(flagId: string, status: FEATURE_FLAG_STATUS): Promise { @@ -136,15 +156,13 @@ export class FeatureFlagService { } private async addFeatureFlagInDB(flag: FeatureFlag, logger: UpgradeLogger): Promise { - const createdFeatureFlag = await getConnection().transaction(async (transactionalEntityManager) => { - flag.id = uuid(); - // saving feature flag doc - const { featureFlagSegmentExclusion, featureFlagSegmentInclusion, ...flagDoc } = flag; - - let featureFlagDoc: FeatureFlag; + flag.id = uuid(); + // saving feature flag doc + let featureFlagDoc: FeatureFlag; + await getConnection().transaction(async (transactionalEntityManager) => { try { featureFlagDoc = ( - await this.featureFlagRepository.insertFeatureFlag(flagDoc as any, transactionalEntityManager) + await this.featureFlagRepository.insertFeatureFlag(flag as any, transactionalEntityManager) )[0]; } catch (err) { const error = new Error(`Error in creating feature flag document "addFeatureFlagInDB" ${err}`); @@ -152,66 +170,13 @@ export class FeatureFlagService { logger.error(error); throw error; } - - const { - segmentExists: includeSegmentExists, - segmentDoc: segmentIncludeDoc, - segmentDocToSave: segmentIncludeDocToSave, - } = await this.addPrivateSegmentToDB(featureFlagSegmentInclusion, flag, 'Inclusion', logger); - const { - segmentExists: excludeSegmentExists, - segmentDoc: segmentExcludeDoc, - segmentDocToSave: segmentExcludeDocToSave, - } = await this.addPrivateSegmentToDB(featureFlagSegmentExclusion, flag, 'Exclusion', logger); - - let featureFlagSegmentInclusionDoc: FeatureFlagSegmentInclusion; - let featureFlagSegmentExclusionDoc: FeatureFlagSegmentExclusion; - - try { - [featureFlagSegmentInclusionDoc, featureFlagSegmentExclusionDoc] = await Promise.all([ - includeSegmentExists - ? this.featureFlagSegmentInclusionRepository.insertData( - segmentIncludeDocToSave, - logger, - transactionalEntityManager - ) - : (Promise.resolve([]) as any), - excludeSegmentExists - ? this.featureFlagSegmentExclusionRepository.insertData( - segmentExcludeDocToSave, - logger, - transactionalEntityManager - ) - : (Promise.resolve([]) as any), - ]); - } catch (err) { - const error = err as Error; - error.message = `Error in creating inclusion or exclusion segments "addFeatureFlagInDB"`; - logger.error(error); - throw error; - } - - const newFeatureFlagObject = { - ...featureFlagDoc, - ...(includeSegmentExists && { - featureFlagSegmentInclusion: { ...featureFlagSegmentInclusionDoc, segment: segmentIncludeDoc } as any, - }), - ...(excludeSegmentExists && { - featureFlagSegmentExclusion: { ...featureFlagSegmentExclusionDoc, segment: segmentExcludeDoc } as any, - }), - }; - - return newFeatureFlagObject; }); // TODO: Add log for feature flag creation - return createdFeatureFlag; + return featureFlagDoc; } private async updateFeatureFlagInDB(flag: FeatureFlag, logger: UpgradeLogger): Promise { - // get old feature flag document - const oldFeatureFlag = await this.findOne(flag.id); - return getConnection().transaction(async (transactionalEntityManager) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { @@ -231,50 +196,70 @@ export class FeatureFlagService { logger.error(error); throw error; } - featureFlagDoc.featureFlagSegmentInclusion = oldFeatureFlag.featureFlagSegmentInclusion; - const segmentIncludeData = this.experimentService.includeExcludeSegmentCreation( - featureFlagSegmentInclusion, - featureFlagDoc.featureFlagSegmentInclusion, - flag.id, - flag.context, - true - ); - - featureFlagDoc.featureFlagSegmentExclusion = oldFeatureFlag.featureFlagSegmentExclusion; - const segmentExcludeData = this.experimentService.includeExcludeSegmentCreation( - featureFlagSegmentExclusion, - featureFlagDoc.featureFlagSegmentExclusion, - flag.id, - flag.context, - false - ); + return featureFlagDoc; + }); + } - let segmentIncludeDoc: Segment; + public async deleteList(segmentId: string, logger: UpgradeLogger): Promise { + return this.segmentService.deleteSegment(segmentId, logger); + } + + public async addList( + listInput: FeatureFlagListValidator, + filterType: string, + logger: UpgradeLogger + ): Promise { + logger.info({ message: `Add ${filterType} list to feature flag` }); + const createdList = await getConnection().transaction(async (transactionalEntityManager) => { + const featureFlagSegmentInclusionOrExclusion = + filterType === 'inclusion' ? new FeatureFlagSegmentInclusion() : new FeatureFlagSegmentExclusion(); + featureFlagSegmentInclusionOrExclusion.enabled = listInput.enabled; + featureFlagSegmentInclusionOrExclusion.listType = listInput.listType; + const featureFlag = await this.featureFlagRepository.findOne(listInput.flagId); + + featureFlagSegmentInclusionOrExclusion.featureFlag = featureFlag; + + // create a new private segment + listInput.list.type = SEGMENT_TYPE.PRIVATE; + let newSegment: Segment; try { - segmentIncludeDoc = await this.segmentService.upsertSegment(segmentIncludeData, logger); + newSegment = await this.segmentService.upsertSegmentInPipeline( + listInput.list, + logger, + transactionalEntityManager + ); } catch (err) { - const error = err as ErrorWithType; - error.details = 'Error in updating IncludeSegment in DB'; - error.type = SERVER_ERROR.QUERY_FAILED; + const error = new Error(`Error in creating private segment for feature flag ${filterType} list ${err}`); + (error as any).type = SERVER_ERROR.QUERY_FAILED; logger.error(error); throw error; } + featureFlagSegmentInclusionOrExclusion.segment = newSegment; + // } - let segmentExcludeDoc: Segment; try { - segmentExcludeDoc = await this.segmentService.upsertSegment(segmentExcludeData, logger); + if (filterType === 'inclusion') { + await this.featureFlagSegmentInclusionRepository.insertData( + featureFlagSegmentInclusionOrExclusion, + logger, + transactionalEntityManager + ); + } else { + await this.featureFlagSegmentExclusionRepository.insertData( + featureFlagSegmentInclusionOrExclusion, + logger, + transactionalEntityManager + ); + } } catch (err) { - const error = err as ErrorWithType; - error.details = 'Error in updating ExcludeSegment in DB'; - error.type = SERVER_ERROR.QUERY_FAILED; + const error = new Error(`Error in adding segment for feature flag ${filterType} list ${err}`); + (error as any).type = SERVER_ERROR.QUERY_FAILED; logger.error(error); throw error; } - - featureFlagDoc.featureFlagSegmentInclusion.segment = segmentIncludeDoc; - featureFlagDoc.featureFlagSegmentExclusion.segment = segmentExcludeDoc; - return featureFlagDoc; + return featureFlagSegmentInclusionOrExclusion; }); + return createdList; } private postgresSearchString(type: FLAG_SEARCH_KEY): string { @@ -313,35 +298,25 @@ export class FeatureFlagService { featureFlag.status = flagDTO.status; featureFlag.context = flagDTO.context; featureFlag.tags = flagDTO.tags; - const newExclusion = new FeatureFlagSegmentExclusion(); - const newInclusion = new FeatureFlagSegmentInclusion(); - featureFlag.featureFlagSegmentExclusion = { ...flagDTO.featureFlagSegmentExclusion, ...newExclusion }; - featureFlag.featureFlagSegmentInclusion = { ...flagDTO.featureFlagSegmentInclusion, ...newInclusion }; featureFlag.filterMode = flagDTO.filterMode; return featureFlag; } - private getSegmentDoc(doc: FeatureFlagSegmentInclusion | FeatureFlagSegmentExclusion) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { createdAt, updatedAt, versionNumber, ...newDoc } = doc; - return newDoc; - } - private async featureFlagLevelInclusionExclusion( featureFlags: FeatureFlag[], experimentUser: ExperimentUser ): Promise { const segmentObjMap = {}; featureFlags.forEach((flag) => { - const includeId = flag.featureFlagSegmentInclusion.segment.id; - const excludeId = flag.featureFlagSegmentExclusion.segment.id; + const includeIds = flag.featureFlagSegmentInclusion.map((segmentInclusion) => segmentInclusion.segment.id); + const excludeIds = flag.featureFlagSegmentExclusion.map((segmentExclusion) => segmentExclusion.segment.id); segmentObjMap[flag.id] = { - segmentIdsQueue: [includeId, excludeId], - currentIncludedSegmentIds: [includeId], - currentExcludedSegmentIds: [excludeId], - allIncludedSegmentIds: [includeId], - allExcludedSegmentIds: [excludeId], + segmentIdsQueue: [...includeIds, ...excludeIds], + currentIncludedSegmentIds: includeIds, + currentExcludedSegmentIds: excludeIds, + allIncludedSegmentIds: includeIds, + allExcludedSegmentIds: excludeIds, }; }); @@ -358,60 +333,4 @@ export class FeatureFlagService { const includedFeatureFlags = featureFlags.filter(({ id }) => includedFeatureFlagIds.includes(id)); return includedFeatureFlags; } - - private async addPrivateSegmentToDB( - segmentInclusionExclusion: FeatureFlagSegmentExclusion | FeatureFlagSegmentInclusion, - flag: FeatureFlag, - type: string, - logger: UpgradeLogger - ) { - let segmentExists = true; - let segmentDoc: Segment; - let segmentDocToSave: Partial = {}; - if (segmentInclusionExclusion) { - const segment: any = this.setSegmentInclusionOrExclusion(segmentInclusionExclusion); - const segmentData: SegmentInputValidator = { - ...segment, - id: segment.id || uuid(), - name: flag.id + ' ' + type + ' Segment', - description: flag.id + ' ' + type + ' Segment', - context: flag.context[0], - type: SEGMENT_TYPE.PRIVATE, - }; - try { - segmentDoc = await this.segmentService.upsertSegment(segmentData, logger); - } catch (err) { - const error = err as ErrorWithType; - error.details = 'Error in adding segment in DB'; - error.type = SERVER_ERROR.QUERY_FAILED; - logger.error(error); - throw error; - } - // creating segment doc - const tempDoc = type === 'Inclusion' ? new FeatureFlagSegmentInclusion() : new FeatureFlagSegmentExclusion(); - tempDoc.segment = segmentDoc; - tempDoc.featureFlag = flag; - segmentDocToSave = this.getSegmentDoc(tempDoc); - } else { - segmentExists = false; - } - return { segmentExists, segmentDoc, segmentDocToSave }; - } - - private setSegmentInclusionOrExclusion( - inclusionOrExclusion: FeatureFlagSegmentExclusion | FeatureFlagSegmentInclusion - ) { - const segment = inclusionOrExclusion.segment; - return segment - ? { - type: segment.type, - userIds: segment.individualForSegment?.map((x) => x.userId) || [], - groups: - segment.groupForSegment?.map((x) => { - return { type: x.type, groupId: x.groupId }; - }) || [], - subSegmentIds: segment.subSegments?.map((x) => x.id) || [], - } - : inclusionOrExclusion; - } } diff --git a/backend/packages/Upgrade/src/api/services/SegmentService.ts b/backend/packages/Upgrade/src/api/services/SegmentService.ts index 4d4e746fc2..e2104c5250 100644 --- a/backend/packages/Upgrade/src/api/services/SegmentService.ts +++ b/backend/packages/Upgrade/src/api/services/SegmentService.ts @@ -6,7 +6,7 @@ import { GroupForSegmentRepository } from '../repositories/GroupForSegmentReposi import { Segment } from '../models/Segment'; import { UpgradeLogger } from '../../lib/logger/UpgradeLogger'; import { SEGMENT_TYPE, SERVER_ERROR, SEGMENT_STATUS, CACHE_PREFIX } from 'upgrade_types'; -import { getConnection } from 'typeorm'; +import { EntityManager, getConnection } from 'typeorm'; import Papa from 'papaparse'; import { env } from '../../env'; import { v4 as uuid } from 'uuid'; @@ -114,6 +114,10 @@ export class SegmentService { .where('segment.id IN (:...ids)', { ids }) .getMany(); + if (!result.length) { + return []; + } + // sort according to ids const sortedData = ids.map((id) => { return result.find((data) => data.id === id); @@ -211,6 +215,15 @@ export class SegmentService { return this.addSegmentDataInDB(segment, logger); } + public upsertSegmentInPipeline( + segment: SegmentInputValidator, + logger: UpgradeLogger, + transactionalEntityManager: EntityManager + ): Promise { + logger.info({ message: `Upsert segment => ${JSON.stringify(segment, undefined, 2)}` }); + return this.addSegmentDataWithPipeline(segment, logger, transactionalEntityManager); + } + public async deleteSegment(id: string, logger: UpgradeLogger): Promise { logger.info({ message: `Delete segment by id. segmentId: ${id}` }); return await this.segmentRepository.deleteSegment(id, logger); @@ -426,109 +439,122 @@ export class SegmentService { async addSegmentDataInDB(segment: SegmentInputValidator, logger: UpgradeLogger): Promise { const createdSegment = await getConnection().transaction(async (transactionalEntityManager) => { - let segmentDoc: Segment; - - if (segment.id) { - try { - // get segment by ids - segmentDoc = await transactionalEntityManager - .getRepository(Segment) - .findOne(segment.id, { relations: ['individualForSegment', 'groupForSegment', 'subSegments'] }); - - // delete individual for segment - if (segmentDoc && segmentDoc.individualForSegment && segmentDoc.individualForSegment.length > 0) { - const usersToDelete = segmentDoc.individualForSegment.map((individual) => { - return { userId: individual.userId, segment: segment.id }; - }); - await transactionalEntityManager.getRepository(IndividualForSegment).delete(usersToDelete as any); - } + return this.addSegmentDataWithPipeline(segment, logger, transactionalEntityManager); + }); - // delete group for segment - if (segmentDoc && segmentDoc.groupForSegment && segmentDoc.groupForSegment.length > 0) { - const groupToDelete = segmentDoc.groupForSegment.map((group) => { - return { groupId: group.groupId, type: group.type, segment: segment.id }; - }); - await transactionalEntityManager.getRepository(GroupForSegment).delete(groupToDelete as any); - } - } catch (err) { - const error = err as ErrorWithType; - error.details = 'Error in deleting segment from DB'; - error.type = SERVER_ERROR.QUERY_FAILED; - logger.error(error); - throw error; - } - } + return createdSegment; + } - // create/update segment document - segment.id = segment.id || uuid(); - const { id, name, description, context, type } = segment; - const allSegments = await this.getSegmentByIds(segment.subSegmentIds); - const subSegmentData = segment.subSegmentIds - .filter((subSegmentId) => { - // check if segment exists: - const subSegment = allSegments.find((segmentId) => subSegmentId === segmentId.id); - if (subSegment) { - return true; - } else { - const error = new Error( - 'SubSegment: ' + subSegmentId + ' not found. Please import subSegment and link in experiment.' - ); - (error as any).type = SERVER_ERROR.QUERY_FAILED; - logger.error(error); - return false; - } - }) - .map((subSegmentId) => ({ id: subSegmentId })); + async addSegmentDataWithPipeline( + segment: SegmentInputValidator, + logger: UpgradeLogger, + transactionalEntityManager: EntityManager + ): Promise { + let segmentDoc: Segment; + if (segment.id) { try { + // get segment by ids segmentDoc = await transactionalEntityManager .getRepository(Segment) - .save({ id, name, description, context, type, subSegments: subSegmentData }); + .findOne(segment.id, { relations: ['individualForSegment', 'groupForSegment', 'subSegments'] }); + + // delete individual for segment + if (segmentDoc && segmentDoc.individualForSegment && segmentDoc.individualForSegment.length > 0) { + const usersToDelete = segmentDoc.individualForSegment.map((individual) => { + return { userId: individual.userId, segment: segment.id }; + }); + await transactionalEntityManager.getRepository(IndividualForSegment).delete(usersToDelete as any); + } + + // delete group for segment + if (segmentDoc && segmentDoc.groupForSegment && segmentDoc.groupForSegment.length > 0) { + const groupToDelete = segmentDoc.groupForSegment.map((group) => { + return { groupId: group.groupId, type: group.type, segment: segment.id }; + }); + await transactionalEntityManager.getRepository(GroupForSegment).delete(groupToDelete as any); + } } catch (err) { const error = err as ErrorWithType; - error.details = 'Error in saving segment in DB'; + error.details = 'Error in deleting segment from DB'; error.type = SERVER_ERROR.QUERY_FAILED; logger.error(error); throw error; } + } - const individualForSegmentDocsToSave = segment.userIds.map((userId) => ({ - userId, - segment: segmentDoc, - })); - - const groupForSegmentDocsToSave = segment.groups.map((group) => { - return { ...group, segment: segmentDoc }; + // create/update segment document + segment.id = segment.id || uuid(); + const { id, name, description, context, type } = segment; + const allSegments = await this.getSegmentByIds(segment.subSegmentIds); + const subSegmentData = segment.subSegmentIds + .filter((subSegmentId) => { + // check if segment exists: + const subSegment = allSegments.find((segment) => subSegmentId === segment.id); + if (subSegment) { + return true; + } else { + const error = new Error( + 'SubSegment: ' + subSegmentId + ' not found. Please import subSegment and link in experiment.' + ); + (error as any).type = SERVER_ERROR.QUERY_FAILED; + logger.error(error); + return false; + } + }) + .map((subSegmentId) => ({ id: subSegmentId })); + + try { + segmentDoc = await transactionalEntityManager.getRepository(Segment).save({ + id, + name, + description, + context, + type, + subSegments: subSegmentData, }); + } catch (err) { + const error = err as ErrorWithType; + error.details = 'Error in saving segment in DB'; + error.type = SERVER_ERROR.QUERY_FAILED; + logger.error(error); + throw error; + } - try { - await Promise.all([ - this.individualForSegmentRepository.insertIndividualForSegment( - individualForSegmentDocsToSave, - transactionalEntityManager, - logger - ), - this.groupForSegmentRepository.insertGroupForSegment( - groupForSegmentDocsToSave, - transactionalEntityManager, - logger - ), - ]); - } catch (err) { - const error = err as Error; - error.message = `Error in creating individualDocs, groupDocs in "addSegmentInDB"`; - logger.error(error); - throw error; - } + const individualForSegmentDocsToSave = segment.userIds.map((userId) => ({ + userId, + segment: segmentDoc, + })); - return transactionalEntityManager - .getRepository(Segment) - .findOne(segmentDoc.id, { relations: ['individualForSegment', 'groupForSegment', 'subSegments'] }); + const groupForSegmentDocsToSave = segment.groups.map((group) => { + return { ...group, segment: segmentDoc }; }); - // reset caching + try { + await Promise.all([ + this.individualForSegmentRepository.insertIndividualForSegment( + individualForSegmentDocsToSave, + transactionalEntityManager, + logger + ), + this.groupForSegmentRepository.insertGroupForSegment( + groupForSegmentDocsToSave, + transactionalEntityManager, + logger + ), + ]); + } catch (err) { + const error = err as Error; + error.message = `Error in creating individualDocs, groupDocs in "addSegmentInDB"`; + logger.error(error); + throw error; + } + + // reset cache await this.cacheService.resetPrefixCache(CACHE_PREFIX.SEGMENT_KEY_PREFIX); - return createdSegment; + return transactionalEntityManager + .getRepository(Segment) + .findOne(segmentDoc.id, { relations: ['individualForSegment', 'groupForSegment', 'subSegments'] }); } } diff --git a/backend/packages/Upgrade/src/database/migrations/1720810654183-feature-flag-lists.ts b/backend/packages/Upgrade/src/database/migrations/1720810654183-feature-flag-lists.ts new file mode 100644 index 0000000000..64af208d1f --- /dev/null +++ b/backend/packages/Upgrade/src/database/migrations/1720810654183-feature-flag-lists.ts @@ -0,0 +1,55 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class featureFlagLists1720810654183 implements MigrationInterface { + name = 'featureFlagLists1720810654183'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "feature_flag_segment_exclusion" ADD "enabled" boolean NOT NULL`); + await queryRunner.query(`ALTER TABLE "feature_flag_segment_exclusion" ADD "listType" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "feature_flag_segment_inclusion" ADD "enabled" boolean NOT NULL`); + await queryRunner.query(`ALTER TABLE "feature_flag_segment_inclusion" ADD "listType" character varying NOT NULL`); + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_exclusion" DROP CONSTRAINT "FK_d45d8b0089b0965c79b57fe84e7"` + ); + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_exclusion" DROP CONSTRAINT "REL_d45d8b0089b0965c79b57fe84e"` + ); + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_inclusion" DROP CONSTRAINT "FK_e9d3d49c9779b47390961eb1cff"` + ); + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_inclusion" DROP CONSTRAINT "REL_e9d3d49c9779b47390961eb1cf"` + ); + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_exclusion" ADD CONSTRAINT "FK_d45d8b0089b0965c79b57fe84e7" FOREIGN KEY ("featureFlagId") REFERENCES "feature_flag"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_inclusion" ADD CONSTRAINT "FK_e9d3d49c9779b47390961eb1cff" FOREIGN KEY ("featureFlagId") REFERENCES "feature_flag"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_inclusion" DROP CONSTRAINT "FK_e9d3d49c9779b47390961eb1cff"` + ); + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_exclusion" DROP CONSTRAINT "FK_d45d8b0089b0965c79b57fe84e7"` + ); + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_inclusion" ADD CONSTRAINT "REL_e9d3d49c9779b47390961eb1cf" UNIQUE ("featureFlagId")` + ); + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_inclusion" ADD CONSTRAINT "FK_e9d3d49c9779b47390961eb1cff" FOREIGN KEY ("featureFlagId") REFERENCES "feature_flag"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_exclusion" ADD CONSTRAINT "REL_d45d8b0089b0965c79b57fe84e" UNIQUE ("featureFlagId")` + ); + await queryRunner.query( + `ALTER TABLE "feature_flag_segment_exclusion" ADD CONSTRAINT "FK_d45d8b0089b0965c79b57fe84e7" FOREIGN KEY ("featureFlagId") REFERENCES "feature_flag"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query(`ALTER TABLE "feature_flag_segment_inclusion" DROP COLUMN "listType"`); + await queryRunner.query(`ALTER TABLE "feature_flag_segment_inclusion" DROP COLUMN "enabled"`); + await queryRunner.query(`ALTER TABLE "feature_flag_segment_exclusion" DROP COLUMN "listType"`); + await queryRunner.query(`ALTER TABLE "feature_flag_segment_exclusion" DROP COLUMN "enabled"`); + } +} diff --git a/backend/packages/Upgrade/test/integration/FeatureFlags/FeatureFlagInclusionExclusion.ts b/backend/packages/Upgrade/test/integration/FeatureFlags/FeatureFlagInclusionExclusion.ts index 82480aaf85..18aef804dc 100644 --- a/backend/packages/Upgrade/test/integration/FeatureFlags/FeatureFlagInclusionExclusion.ts +++ b/backend/packages/Upgrade/test/integration/FeatureFlags/FeatureFlagInclusionExclusion.ts @@ -3,7 +3,8 @@ import { UpgradeLogger } from '../../../src/lib/logger/UpgradeLogger'; import { FeatureFlagService } from '../../../src/api/services/FeatureFlagService'; import { featureFlag } from '../mockData/featureFlag'; import { experimentUsers } from '../mockData/experimentUsers/index'; -import { ExperimentUser } from 'src/api/models/ExperimentUser'; +import { ExperimentUser } from '../../../src/api/models/ExperimentUser'; +import { SEGMENT_TYPE } from '../../../../../../types/src'; export default async function FeatureFlagInclusionExclusionLogic(): Promise { const featureFlagService = Container.get(FeatureFlagService); @@ -13,7 +14,41 @@ export default async function FeatureFlagInclusionExclusionLogic(): Promise { status: 'enabled', context: ['foo'], tags: ['bar'], - featureFlagSegmentInclusion: { - segment: { - type: 'private', - }, - }, - featureFlagSegmentExclusion: { - segment: { - type: 'private', - }, - }, filterMode: 'includeAll', }) .set('Accept', 'application/json') @@ -114,23 +104,110 @@ describe('Feature Flag Controller Testing', () => { status: 'enabled', context: ['foo'], tags: ['bar'], - featureFlagSegmentInclusion: { - segment: { - type: 'private', - individualForSegment: [], - groupForSegment: [], - subSegments: [], - }, + filterMode: 'includeAll', + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Post request for /api/flags/inclusionList', () => { + return request(app) + .post('/api/flags/inclusionList') + .send({ + flagId: uuid(), + enabled: true, + listType: 'string', + list: { + name: 'string', + context: 'string', + type: 'private', + userIds: ['string'], + groups: [], + subSegmentIds: [], }, - featureFlagSegmentExclusion: { - segment: { - type: 'private', - individualForSegment: [], - groupForSegment: [], - subSegments: [], - }, + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Post request for /api/flags/exclusionList', () => { + return request(app) + .post('/api/flags/exclusionList') + .send({ + flagId: uuid(), + enabled: true, + listType: 'string', + list: { + name: 'string', + context: 'string', + type: 'private', + userIds: ['string'], + groups: [], + subSegmentIds: [], + }, + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Delete request for /api/flags/inclusionList/id', () => { + return request(app) + .delete('/api/flags/inclusionList/' + uuid()) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Delete request for /api/flags/exclusionList/id', () => { + return request(app) + .delete('/api/flags/exclusionList/' + uuid()) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Put request for /api/flags/inclusionList/id', () => { + const segmentId = uuid(); + return request(app) + .put('/api/flags/inclusionList/' + segmentId) + .send({ + flagId: uuid(), + enabled: true, + listType: 'string', + list: { + id: segmentId, + name: 'string', + context: 'string', + type: 'private', + userIds: ['string'], + groups: [], + subSegmentIds: [], + }, + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + test('Put request for /api/flags/exclusionList/id', () => { + const segmentId = uuid(); + return request(app) + .put('/api/flags/exclusionList/' + segmentId) + .send({ + flagId: uuid(), + enabled: true, + listType: 'string', + list: { + id: segmentId, + name: 'string', + context: 'string', + type: 'private', + userIds: ['string'], + groups: [], + subSegmentIds: [], }, - filterMode: 'includeAll', }) .set('Accept', 'application/json') .expect('Content-Type', /json/) diff --git a/backend/packages/Upgrade/test/unit/controllers/mocks/FeatureFlagServiceMock.ts b/backend/packages/Upgrade/test/unit/controllers/mocks/FeatureFlagServiceMock.ts index b4cff65d93..22a4842ef8 100644 --- a/backend/packages/Upgrade/test/unit/controllers/mocks/FeatureFlagServiceMock.ts +++ b/backend/packages/Upgrade/test/unit/controllers/mocks/FeatureFlagServiceMock.ts @@ -4,8 +4,9 @@ import { IFeatureFlagSortParams, } from '../../../../src/api/controllers/validators/FeatureFlagsPaginatedParamsValidator'; import { UpgradeLogger } from '../../../../src/lib/logger/UpgradeLogger'; -import { FeatureFlagValidation } from 'src/api/controllers/validators/FeatureFlagValidator'; -import { RequestedExperimentUser } from 'src/api/controllers/validators/ExperimentUserValidator'; +import { FeatureFlagValidation } from '../../../../src/api/controllers/validators/FeatureFlagValidator'; +import { RequestedExperimentUser } from '../../../../src/api/controllers/validators/ExperimentUserValidator'; +import { FeatureFlagListValidator } from '../../../../src/api/controllers/validators/FeatureFlagListValidator'; @Service() export default class FeatureFlagServiceMock { @@ -43,4 +44,12 @@ export default class FeatureFlagServiceMock { public update(flagDTO: FeatureFlagValidation, logger: UpgradeLogger): Promise<[]> { return Promise.resolve([]); } + + public addList(listInput: FeatureFlagListValidator, filterType: string, logger: UpgradeLogger): Promise<[]> { + return Promise.resolve([]); + } + + public deleteList(segmentId: string, logger: UpgradeLogger): Promise<[]> { + return Promise.resolve([]); + } } diff --git a/backend/packages/Upgrade/test/unit/repositories/FeatureFlagRepository.test.ts b/backend/packages/Upgrade/test/unit/repositories/FeatureFlagRepository.test.ts index 9c26118b4a..56399297b6 100644 --- a/backend/packages/Upgrade/test/unit/repositories/FeatureFlagRepository.test.ts +++ b/backend/packages/Upgrade/test/unit/repositories/FeatureFlagRepository.test.ts @@ -73,9 +73,7 @@ describe('FeatureFlagRepository Testing', () => { }); it('should delete a flag', async () => { - createQueryBuilderStub = sandbox - .stub(FeatureFlagRepository.prototype, 'createQueryBuilder') - .returns(deleteQueryBuilder); + createQueryBuilderStub = sandbox.stub(manager, 'createQueryBuilder').returns(deleteQueryBuilder); const result = { identifiers: [{ id: flag.id }], generatedMaps: [flag], @@ -88,16 +86,16 @@ describe('FeatureFlagRepository Testing', () => { deleteMock.expects('returning').once().returns(deleteQueryBuilder); deleteMock.expects('execute').once().returns(Promise.resolve(result)); - await repo.deleteById(flag.id); + const res = await repo.deleteById(flag.id, manager); sinon.assert.calledOnce(createQueryBuilderStub); deleteMock.verify(); + + expect(res).toEqual([flag]); }); it('should throw an error when delete fails', async () => { - createQueryBuilderStub = sandbox - .stub(FeatureFlagRepository.prototype, 'createQueryBuilder') - .returns(deleteQueryBuilder); + createQueryBuilderStub = sandbox.stub(manager, 'createQueryBuilder').returns(deleteQueryBuilder); deleteMock.expects('delete').once().returns(deleteQueryBuilder); deleteMock.expects('from').once().returns(deleteQueryBuilder); @@ -106,7 +104,7 @@ describe('FeatureFlagRepository Testing', () => { deleteMock.expects('execute').once().returns(Promise.reject(err)); expect(async () => { - await repo.deleteById(flag.id); + await repo.deleteById(flag.id, manager); }).rejects.toThrow(err); sinon.assert.calledOnce(createQueryBuilderStub); diff --git a/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts b/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts index 14ead32b83..adc106e8e7 100644 --- a/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts +++ b/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts @@ -4,16 +4,11 @@ import { Test, TestingModuleBuilder } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { FeatureFlag } from '../../../src/api/models/FeatureFlag'; -import { Segment } from '../../../src/api/models/Segment'; import { FeatureFlagRepository } from '../../../src/api/repositories/FeatureFlagRepository'; -import { FeatureFlagSegmentInclusionRepository } from '../../../src/api/repositories/FeatureFlagSegmentInclusionRepository'; -import { FeatureFlagSegmentExclusionRepository } from '../../../src/api/repositories/FeatureFlagSegmentExclusionRepository'; import { ErrorService } from '../../../src/api/services/ErrorService'; import { FeatureFlagService } from '../../../src/api/services/FeatureFlagService'; -import { SegmentService } from '../../../src/api/services/SegmentService'; -import { ExperimentService } from '../../../src/api/services/ExperimentService'; import { UpgradeLogger } from '../../../src/lib/logger/UpgradeLogger'; @@ -21,25 +16,25 @@ import { FLAG_SEARCH_KEY, FLAG_SORT_KEY, } from '../../../src/api/controllers/validators/FeatureFlagsPaginatedParamsValidator'; -import { SORT_AS_DIRECTION } from '../../../../../../types/src'; +import { SEGMENT_TYPE, SORT_AS_DIRECTION } from '../../../../../../types/src'; import { isUUID } from 'class-validator'; import { v4 as uuid } from 'uuid'; import { FEATURE_FLAG_STATUS } from 'upgrade_types'; import { ExperimentAssignmentService } from '../../../src/api/services/ExperimentAssignmentService'; +import { FeatureFlagValidation } from '../../../src/api/controllers/validators/FeatureFlagValidator'; +import { FeatureFlagListValidator } from '../../../src/api/controllers/validators/FeatureFlagListValidator'; +import { SegmentService } from '../../../src/api/services/SegmentService'; +import { FeatureFlagSegmentExclusionRepository } from '../../../src/api/repositories/FeatureFlagSegmentExclusionRepository'; +import { FeatureFlagSegmentInclusionRepository } from '../../../src/api/repositories/FeatureFlagSegmentInclusionRepository'; describe('Feature Flag Service Testing', () => { let service: FeatureFlagService; let flagRepo: FeatureFlagRepository; - let flagSegmentInclusionRepo: FeatureFlagSegmentInclusionRepository; - let flagSegmentExclusionRepo: FeatureFlagSegmentExclusionRepository; - let segmentService: SegmentService; let module: Awaited>; const logger = new UpgradeLogger(); - const seg1 = new Segment(); - const mockFlag1 = new FeatureFlag(); mockFlag1.id = uuid(); mockFlag1.name = 'name'; @@ -47,45 +42,32 @@ describe('Feature Flag Service Testing', () => { mockFlag1.description = 'description'; mockFlag1.context = ['context1']; mockFlag1.status = FEATURE_FLAG_STATUS.ENABLED; - mockFlag1.featureFlagSegmentExclusion = { - createdAt: new Date(), - updatedAt: new Date(), - versionNumber: 1, - segment: seg1, - featureFlag: mockFlag1, - }; - mockFlag1.featureFlagSegmentInclusion = { - createdAt: new Date(), - updatedAt: new Date(), - versionNumber: 1, - segment: seg1, - featureFlag: mockFlag1, - }; + mockFlag1.featureFlagSegmentInclusion = []; + mockFlag1.featureFlagSegmentExclusion = []; - const mockFlag2 = new FeatureFlag(); + const mockFlag2 = new FeatureFlagValidation(); mockFlag2.id = uuid(); mockFlag2.name = 'name'; mockFlag2.key = 'key'; mockFlag2.description = 'description'; mockFlag2.context = ['context']; mockFlag2.status = FEATURE_FLAG_STATUS.ENABLED; - mockFlag2.featureFlagSegmentExclusion = { - createdAt: new Date(), - updatedAt: new Date(), - versionNumber: 1, - segment: seg1, - featureFlag: mockFlag2, - }; - mockFlag2.featureFlagSegmentInclusion = { - createdAt: new Date(), - updatedAt: new Date(), - versionNumber: 1, - segment: seg1, - featureFlag: mockFlag2, - }; - - const mockFlag3 = new FeatureFlag(); + const mockFlag3 = new FeatureFlagValidation(); + + const mockList = new FeatureFlagListValidator(); + mockList.enabled = true; + mockList.flagId = uuid(); + mockList.listType = 'individual'; + mockList.list = { + name: 'name', + id: uuid(), + context: 'context', + type: SEGMENT_TYPE.PRIVATE, + userIds: ['user1'], + groups: [], + subSegmentIds: [], + }; const mockFlagArr = [mockFlag1, mockFlag2, mockFlag3]; const limitSpy = jest.fn().mockReturnThis(); @@ -116,23 +98,16 @@ describe('Feature Flag Service Testing', () => { providers: [ FeatureFlagService, { - provide: ExperimentService, + provide: ExperimentAssignmentService, useValue: { - includeExcludeSegmentCreation: jest.fn().mockResolvedValue({ subSegmentIds: [], userIds: [], groups: [] }), + inclusionExclusionLogic: jest.fn().mockResolvedValue([[mockFlag1.id]]), }, }, { provide: SegmentService, useValue: { - upsertSegment: jest.fn().mockResolvedValue({ id: uuid() }), - addSegmentDataInDB: jest.fn().mockResolvedValue({ id: uuid() }), - find: jest.fn().mockResolvedValue([]), - }, - }, - { - provide: ExperimentAssignmentService, - useValue: { - inclusionExclusionLogic: jest.fn().mockResolvedValue([[mockFlag1.id]]), + upsertSegmentInPipeline: jest.fn().mockResolvedValue(mockList), + deleteSegment: jest.fn().mockResolvedValue(mockList), }, }, { @@ -168,27 +143,18 @@ describe('Feature Flag Service Testing', () => { }, }, { - provide: getRepositoryToken(FeatureFlagSegmentInclusionRepository), + provide: getRepositoryToken(FeatureFlagSegmentExclusionRepository), useValue: { - find: jest.fn().mockResolvedValue(''), - insertData: jest.fn().mockResolvedValue(''), - getFeatureFlagSegmentInclusionData: jest.fn().mockResolvedValue(''), - deleteData: jest.fn().mockImplementation((seg) => { - return seg; - }), + insertData: jest.fn().mockResolvedValue(mockList), }, }, { - provide: getRepositoryToken(FeatureFlagSegmentExclusionRepository), + provide: getRepositoryToken(FeatureFlagSegmentInclusionRepository), useValue: { - find: jest.fn().mockResolvedValue(''), - insertData: jest.fn().mockResolvedValue(''), - getFeatureFlagSegmentExclusionData: jest.fn().mockResolvedValue(''), - deleteData: jest.fn().mockImplementation((seg) => { - return seg; - }), + insertData: jest.fn().mockResolvedValue(mockList), }, }, + { provide: ErrorService, useValue: { @@ -196,17 +162,14 @@ describe('Feature Flag Service Testing', () => { }, }, ], - }).compile(); + }) + .useMocker((token) => { + return token; + }) + .compile(); service = module.get(FeatureFlagService); flagRepo = module.get(getRepositoryToken(FeatureFlagRepository)); - flagSegmentInclusionRepo = module.get( - getRepositoryToken(FeatureFlagSegmentInclusionRepository) - ); - flagSegmentExclusionRepo = module.get( - getRepositoryToken(FeatureFlagSegmentExclusionRepository) - ); - segmentService = module.get(SegmentService); }); it('should be defined', async () => { @@ -222,36 +185,14 @@ describe('Feature Flag Service Testing', () => { expect(results).toEqual(mockFlagArr); }); - it('should create a feature flag with uuid', async () => { - const results = await service.create(mockFlag1, logger); - expect(isUUID(results.featureFlagSegmentInclusion.segment.id)).toBeTruthy(); - expect(isUUID(results.featureFlagSegmentExclusion.segment.id)).toBeTruthy(); - }); - it('should throw an error when create flag fails', async () => { const err = new Error('insert error'); flagRepo.insertFeatureFlag = jest.fn().mockRejectedValue(err); expect(async () => { - await service.create(mockFlag1, logger); + await service.create(mockFlag2, logger); }).rejects.toThrow(new Error('Error in creating feature flag document "addFeatureFlagInDB" Error: insert error')); }); - it('should throw an error when create segment inclusion fails', async () => { - const err = new Error('insert error'); - flagSegmentInclusionRepo.insertData = jest.fn().mockRejectedValue(err); - expect(async () => { - await service.create(mockFlag1, logger); - }).rejects.toThrow(new Error('Error in creating inclusion or exclusion segments "addFeatureFlagInDB"')); - }); - - it('should throw an error when create segment exclusion fails', async () => { - const err = new Error('insert error'); - flagSegmentExclusionRepo.insertData = jest.fn().mockRejectedValue(err); - expect(async () => { - await service.create(mockFlag1, logger); - }).rejects.toThrow(new Error('Error in creating inclusion or exclusion segments "addFeatureFlagInDB"')); - }); - it('should return a count of feature flags', async () => { const results = await service.getTotalCount(); expect(results).toEqual(mockFlagArr.length); @@ -348,7 +289,7 @@ describe('Feature Flag Service Testing', () => { }); it('should update the flag', async () => { - const results = await service.update(mockFlag1, logger); + const results = await service.update(mockFlag2, logger); expect(isUUID(results.id)).toBeTruthy(); }); @@ -361,20 +302,12 @@ describe('Feature Flag Service Testing', () => { const err = new Error('insert error'); flagRepo.updateFeatureFlag = jest.fn().mockRejectedValue(err); expect(async () => { - await service.update(mockFlag1, logger); + await service.update(mockFlag2, logger); }).rejects.toThrow( new Error('Error in updating feature flag document "updateFeatureFlagInDB" Error: insert error') ); }); - it('should throw an error when unable to update segment (for inclusion or exclusion', async () => { - const err = new Error('insert error'); - segmentService.upsertSegment = jest.fn().mockRejectedValue(err); - expect(async () => { - await service.update(mockFlag1, logger); - }).rejects.toThrow(err); - }); - it('should update the flag state', async () => { const results = await service.updateState(mockFlag1.id, FEATURE_FLAG_STATUS.ENABLED); expect(results).toBeTruthy(); @@ -386,7 +319,7 @@ describe('Feature Flag Service Testing', () => { }); it('should return undefined when no flag to delete', async () => { - flagRepo.find = jest.fn().mockResolvedValue(undefined); + service.findOne = jest.fn().mockResolvedValue(undefined); const results = await service.delete(mockFlag1.id, logger); expect(results).toEqual(undefined); }); @@ -401,7 +334,7 @@ describe('Feature Flag Service Testing', () => { expect(result).toEqual([]); }); - it('should return an flags belonging to context', async () => { + it('should return all flags belonging to context', async () => { const userDoc = { id: 'user123', group: {}, workingGroup: {} } as any; const context = 'context1'; @@ -411,4 +344,16 @@ describe('Feature Flag Service Testing', () => { expect(result.length).toEqual(1); expect(result).toEqual([mockFlag1.key]); }); + + it('should add an include list', async () => { + const result = await service.addList(mockList, 'include', logger); + + expect(result).toBeTruthy(); + }); + + it('should delete an include list', async () => { + const result = await service.deleteList(mockList.list.id, logger); + + expect(result).toBeTruthy(); + }); });