Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…into feature-flags/merge-typeorm-v0.3-updates
  • Loading branch information
ppratikcr7 committed Sep 4, 2024
2 parents e278544 + 11b035b commit b2012d7
Show file tree
Hide file tree
Showing 18 changed files with 143 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JsonController, Authorized, Post, Body, Delete, Put, Req, Get, Params, Patch } from 'routing-controllers';
import { JsonController, Authorized, Post, Body, Delete, Put, Req, Get, Params, Patch, Res } from 'routing-controllers';
import { FeatureFlagService } from '../services/FeatureFlagService';
import { FeatureFlag } from '../models/FeatureFlag';
import { FeatureFlagSegmentExclusion } from '../models/FeatureFlagSegmentExclusion';
Expand All @@ -8,10 +8,11 @@ import { FeatureFlagFilterModeUpdateValidator } from './validators/FeatureFlagFi
import { FeatureFlagPaginatedParamsValidator } from './validators/FeatureFlagsPaginatedParamsValidator';
import { AppRequest, PaginationResponse } from '../../types';
import { SERVER_ERROR } from 'upgrade_types';
import { FeatureFlagValidation, IdValidator, UserParamsValidator } from './validators/FeatureFlagValidator';
import { FeatureFlagValidation, IdValidator } from './validators/FeatureFlagValidator';
import { ExperimentUserService } from '../services/ExperimentUserService';
import { FeatureFlagListValidator } from '../controllers/validators/FeatureFlagListValidator';
import { Segment } from 'src/api/models/Segment';
import { Response } from 'express';

interface FeatureFlagsPaginationInfo extends PaginationResponse {
nodes: FeatureFlag[];
Expand Down Expand Up @@ -139,23 +140,6 @@ export class FeatureFlagsController {
return this.featureFlagService.find(request.logger);
}

@Post('/keys')
public async getKeys(
@Body({ validate: true })
userParams: UserParamsValidator,
@Req() request: AppRequest
): Promise<string[]> {
const experimentUserDoc = await this.experimentUserService.getUserDoc(userParams.userId, request.logger);
if (!experimentUserDoc) {
const error = new Error(`User not defined in markExperimentPoint: ${userParams.userId}`);
(error as any).type = SERVER_ERROR.EXPERIMENT_USER_NOT_DEFINED;
(error as any).httpCode = 404;
request.logger.error(error);
throw error;
}
return this.featureFlagService.getKeys(experimentUserDoc, userParams.context, request.logger);
}

/**
* @swagger
* /flags/{id}:
Expand Down Expand Up @@ -652,4 +636,44 @@ export class FeatureFlagsController {
): Promise<Segment> {
return this.featureFlagService.deleteList(id, request.logger);
}

/**
* @swagger
* /flags/export/{id}:
* get:
* description: Export Feature Flags JSON
* tags:
* - Feature Flags
* produces:
* - application/json
* parameters:
* - in: path
* flagId: Id
* description: Feature Flag Id
* required: true
* schema:
* type: string
* responses:
* '200':
* description: Get Feature Flag JSON
* '401':
* description: Authorization Required Error
* '404':
* description: Feature Flag Id not found
* '500':
* description: Internal Server Error
*/
@Get('/export/:id')
public async exportFeatureFlag(
@Params({ validate: true }) { id }: IdValidator,
@Req() request: AppRequest,
@Res() response: Response
): Promise<Response> {
const featureFlag = await this.featureFlagService.findOne(id, request.logger);
// download JSON file with appropriate headers to response body;
response.setHeader('Content-Disposition', `attachment; filename="${featureFlag.name}.json"`);
response.setHeader('Content-Type', 'application/json');
const plainFeatureFlag = JSON.stringify(featureFlag, null, 2); // Convert to JSON string
return response.send(plainFeatureFlag);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class StratificationController {

// return csv file with appropriate headers to request;
res.setHeader('Content-Type', 'text/csv; charset=UTF-8');
res.setHeader('Content-Disposition', `attachment; filename=data-${factor}.csv`);
res.setHeader('Content-Disposition', `attachment; filename="${factor}.csv"`);
return res.send(csvData);
}

Expand Down
4 changes: 4 additions & 0 deletions backend/packages/Upgrade/src/api/models/FeatureFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Type } from 'class-transformer';
import { FEATURE_FLAG_STATUS, FILTER_MODE } from 'upgrade_types';
import { FeatureFlagSegmentInclusion } from './FeatureFlagSegmentInclusion';
import { FeatureFlagSegmentExclusion } from './FeatureFlagSegmentExclusion';
import { FeatureFlagExposure } from './FeatureFlagExposure';
@Entity()
export class FeatureFlag extends BaseModel {
@PrimaryColumn('uuid')
Expand Down Expand Up @@ -55,4 +56,7 @@ export class FeatureFlag extends BaseModel {
)
@Type(() => FeatureFlagSegmentExclusion)
public featureFlagSegmentExclusion: FeatureFlagSegmentExclusion[];

@OneToMany(() => FeatureFlagExposure, (featureFlagExposure) => featureFlagExposure.featureFlag)
public featureFlagExposures: FeatureFlagExposure[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export class FeatureFlagExposure extends BaseModel {
@ManyToOne(() => FeatureFlag, { onDelete: 'CASCADE' })
public featureFlag: FeatureFlag;
@Index()
@ManyToOne(() => FeatureFlag, { onDelete: 'CASCADE' })
@ManyToOne(() => ExperimentUser, { onDelete: 'CASCADE' })
public experimentUser: ExperimentUser;
}
25 changes: 24 additions & 1 deletion backend/packages/Upgrade/src/api/services/FeatureFlagService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Service } from 'typedi';
import { FeatureFlag } from '../models/FeatureFlag';
import { Segment } from '../models/Segment';
import { FeatureFlagExposure } from '../models/FeatureFlagExposure';
import { FeatureFlagSegmentInclusion } from '../models/FeatureFlagSegmentInclusion';
import { FeatureFlagSegmentExclusion } from '../models/FeatureFlagSegmentExclusion';
import { FeatureFlagRepository } from '../repositories/FeatureFlagRepository';
Expand Down Expand Up @@ -65,6 +66,22 @@ export class FeatureFlagService {

const includedFeatureFlags = await this.featureFlagLevelInclusionExclusion(filteredFeatureFlags, experimentUserDoc);

// save exposures in db
try {
const exposureRepo = this.dataSource.getRepository(FeatureFlagExposure);
const exposuresToSave = includedFeatureFlags.map((flag) => ({
featureFlag: flag,
experimentUser: experimentUserDoc,
}));
if (exposuresToSave.length > 0) {
await exposureRepo.save(exposuresToSave);
}
} catch (err) {
const error = new Error(`Error in saving feature flag exposure records ${err}`);
(error as any).type = SERVER_ERROR.QUERY_FAILED;
logger.error(error);
}

return includedFeatureFlags.map((flags) => flags.key);
}

Expand Down Expand Up @@ -108,7 +125,9 @@ export class FeatureFlagService {
): Promise<FeatureFlag[]> {
logger.info({ message: 'Find paginated Feature flags' });

let queryBuilder = this.featureFlagRepository.createQueryBuilder('feature_flag');
let queryBuilder = this.featureFlagRepository
.createQueryBuilder('feature_flag')
.loadRelationCountAndMap('feature_flag.featureFlagExposures', 'feature_flag.featureFlagExposures');
if (searchParams) {
const customSearchString = searchParams.string.split(' ').join(`:*&`);
// add search query
Expand All @@ -123,6 +142,10 @@ export class FeatureFlagService {
}

queryBuilder = queryBuilder.offset(skip).limit(take);

// TODO: the type of queryBuilder.getMany() is Promise<FeatureFlag[]>
// However, the above query returns Promise<(Omit<FeatureFlag, 'featureFlagExposures'> & { featureFlagExposures: number })[]>
// This can be fixed by using a @VirtualColumn in the FeatureFlag entity, when we are on TypeORM 0.3
return queryBuilder.getMany();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class fixExposureTable1724955378832 implements MigrationInterface {
name = 'fixExposureTable1724955378832';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "feature_flag_exposure" DROP CONSTRAINT "FK_6cefc76de0ca7c9a38faae5a8c4"`);
await queryRunner.query(`ALTER TABLE "feature_flag_exposure" DROP CONSTRAINT "PK_99ab312ceb343f3121eeadb2d50"`);
await queryRunner.query(
`ALTER TABLE "feature_flag_exposure" ADD CONSTRAINT "PK_6908f3ead6dd3a6fd4ce38514e6" PRIMARY KEY ("featureFlagId")`
);
await queryRunner.query(`DROP INDEX "public"."IDX_6cefc76de0ca7c9a38faae5a8c"`);
await queryRunner.query(`ALTER TABLE "feature_flag_exposure" DROP COLUMN "experimentUserId"`);
await queryRunner.query(`ALTER TABLE "feature_flag_exposure" ADD "experimentUserId" character varying NOT NULL`);
await queryRunner.query(`ALTER TABLE "feature_flag_exposure" DROP CONSTRAINT "PK_6908f3ead6dd3a6fd4ce38514e6"`);
await queryRunner.query(
`ALTER TABLE "feature_flag_exposure" ADD CONSTRAINT "PK_99ab312ceb343f3121eeadb2d50" PRIMARY KEY ("featureFlagId", "experimentUserId")`
);
await queryRunner.query(`ALTER TABLE "experiment" ALTER COLUMN "filterMode" SET DEFAULT 'excludeAll'`);
await queryRunner.query(
`CREATE INDEX "IDX_6cefc76de0ca7c9a38faae5a8c" ON "feature_flag_exposure" ("experimentUserId") `
);
await queryRunner.query(
`ALTER TABLE "feature_flag_exposure" ADD CONSTRAINT "FK_6cefc76de0ca7c9a38faae5a8c4" FOREIGN KEY ("experimentUserId") REFERENCES "experiment_user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "feature_flag_exposure" DROP CONSTRAINT "FK_6cefc76de0ca7c9a38faae5a8c4"`);
await queryRunner.query(`DROP INDEX "public"."IDX_6cefc76de0ca7c9a38faae5a8c"`);
await queryRunner.query(`ALTER TABLE "experiment" ALTER COLUMN "filterMode" SET DEFAULT 'includeAll'`);
await queryRunner.query(`ALTER TABLE "feature_flag_exposure" DROP CONSTRAINT "PK_99ab312ceb343f3121eeadb2d50"`);
await queryRunner.query(
`ALTER TABLE "feature_flag_exposure" ADD CONSTRAINT "PK_6908f3ead6dd3a6fd4ce38514e6" PRIMARY KEY ("featureFlagId")`
);
await queryRunner.query(`ALTER TABLE "feature_flag_exposure" DROP COLUMN "experimentUserId"`);
await queryRunner.query(`ALTER TABLE "feature_flag_exposure" ADD "experimentUserId" uuid NOT NULL`);
await queryRunner.query(
`CREATE INDEX "IDX_6cefc76de0ca7c9a38faae5a8c" ON "feature_flag_exposure" ("experimentUserId") `
);
await queryRunner.query(`ALTER TABLE "feature_flag_exposure" DROP CONSTRAINT "PK_6908f3ead6dd3a6fd4ce38514e6"`);
await queryRunner.query(
`ALTER TABLE "feature_flag_exposure" ADD CONSTRAINT "PK_99ab312ceb343f3121eeadb2d50" PRIMARY KEY ("experimentUserId", "featureFlagId")`
);
await queryRunner.query(
`ALTER TABLE "feature_flag_exposure" ADD CONSTRAINT "FK_6cefc76de0ca7c9a38faae5a8c4" FOREIGN KEY ("experimentUserId") REFERENCES "feature_flag"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,8 @@ export default async function FeatureFlagInclusionExclusionLogic(): Promise<void
);

expect(keysAssign.length).toEqual(0);

// Check the number of exposures
const paginatedFind = await featureFlagService.findPaginated(0, 5, new UpgradeLogger());
expect(paginatedFind[0].featureFlagExposures).toEqual(2);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,6 @@ describe('Feature Flag Controller Testing', () => {
Container.reset();
});

test('Post request for /api/flags/keys', () => {
return request(app)
.post('/api/flags/keys')
.send({
userId: 'user',
context: 'context',
})
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200);
});

test('Post request for /api/flags/paginated', () => {
return request(app)
.post('/api/flags/paginated')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ describe('Feature Flag Service Testing', () => {
getMany: jest.fn().mockResolvedValue(mockFlagArr),
};

const exposureRepoMock = { save: jest.fn() };
const entityManagerMock = { createQueryBuilder: () => queryBuilderMock };

beforeAll(() => {
Expand Down Expand Up @@ -156,6 +157,7 @@ describe('Feature Flag Service Testing', () => {
limit: limitSpy,
innerJoinAndSelect: jest.fn().mockReturnThis(),
leftJoinAndSelect: jest.fn().mockReturnThis(),
loadRelationCountAndMap: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue(mockFlagArr),
getOne: jest.fn().mockResolvedValue(mockFlag1),
})),
Expand Down Expand Up @@ -367,6 +369,7 @@ describe('Feature Flag Service Testing', () => {

expect(result.length).toEqual(1);
expect(result).toEqual([mockFlag1.key]);
expect(exposureRepoMock.save).toHaveBeenCalledTimes(1);
});

it('should add an include list', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ export class FeatureFlagsDataService {
return of(true).pipe(delay(2000));
}

exportFeatureFlagsDesign(flagId: string) {
return this.fetchFeatureFlagById(flagId);
exportFeatureFlagsDesign(id: string) {
const url = `${this.environment.api.exportFlagsDesign}/${id}`;
return this.http.get(url);
}

deleteFeatureFlag(id: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,9 @@ export class FeatureFlagsEffects {
filter(({ featureFlagId }) => !!featureFlagId),
switchMap(({ featureFlagId }) =>
this.featureFlagsDataService.exportFeatureFlagsDesign(featureFlagId).pipe(
map((data) => {
if (data) {
this.commonExportHelpersService.convertDataToDownload([data], 'FeatureFlags');
map((exportFeatureFlagsDesign: FeatureFlag) => {
if (exportFeatureFlagsDesign) {
this.commonExportHelpersService.convertDataToDownload([exportFeatureFlagsDesign], 'FeatureFlags');
this.notificationService.showSuccess('Feature Flag Design JSON downloaded!');
}
return FeatureFlagsActions.actionExportFeatureFlagDesignSuccess();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class="flag-list-table"
mat-table
[dataSource]="dataSource$"
[ngClass]="{'no-data': !dataSource$?.data?.length}"
[ngClass]="{ 'no-data': !dataSource$?.data?.length }"
matSort
(matSortChange)="changeSorting($event)"
[matSortActive]="flagSortKey$ | async"
Expand Down Expand Up @@ -82,7 +82,7 @@
{{ FLAG_TRANSLATION_KEYS.EXPOSURES | translate }}
</th>
<td mat-cell *matCellDef="let flag" class="exposures-column ft-14-400">
<span *ngFor="let exposure of flag.exposures" class="exposure">{{ exposure }}</span>
<span class="exposure">{{ flag.featureFlagExposures }}</span>
</td>
</ng-container>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { InjectionToken } from '@angular/core';
export const ENV = new InjectionToken<Environment>('env.token');

export interface APIEndpoints {
exportSegmentCSV: string;
getAllExperiments: string;
createNewExperiments: string;
validateExperiment: string;
Expand Down Expand Up @@ -46,6 +45,7 @@ export interface APIEndpoints {
validateSegments: string;
importSegments: string;
exportSegments: string;
exportSegmentCSV: string;
getGroupAssignmentStatus: string;
stratification: string;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const environment = {
updateFilterMode: '/flags/filterMode',
getPaginatedFlags: '/flags/paginated',
exportFlagsDesign: '/flags/export',
emailFlagData: '/flags/mail',
emailFlagData: '/flags/email',
addFlagInclusionList: '/flags/inclusionList',
addFlagExclusionList: '/flags/exclusionList',
setting: '/setting',
Expand All @@ -59,7 +59,6 @@ export const environment = {
validateSegments: '/segments/validation',
importSegments: '/segments/import',
exportSegments: '/segments/export/json',
exportSegment: '/segments/export',
exportSegmentCSV: '/segments/export/csv',
getGroupAssignmentStatus: '/experiments/getGroupAssignmentStatus',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const environment = {
updateFilterMode: '/flags/filterMode',
getPaginatedFlags: '/flags/paginated',
exportFlagsDesign: '/flags/export',
emailFlagData: '/flags/mail',
emailFlagData: '/flags/email',
addFlagInclusionList: '/flags/inclusionList',
addFlagExclusionList: '/flags/exclusionList',
setting: '/setting',
Expand All @@ -59,7 +59,6 @@ export const environment = {
validateSegments: '/segments/validation',
importSegments: '/segments/import',
exportSegments: '/segments/export/json',
exportSegment: '/segments/export',
exportSegmentCSV: '/segments/export/csv',
getGroupAssignmentStatus: '/experiments/getGroupAssignmentStatus',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const environment = {
updateFilterMode: '/flags/filterMode',
getPaginatedFlags: '/flags/paginated',
exportFlagsDesign: '/flags/export',
emailFlagData: '/flags/mail',
emailFlagData: '/flags/email',
addFlagInclusionList: '/flags/inclusionList',
addFlagExclusionList: '/flags/exclusionList',
setting: '/setting',
Expand All @@ -59,7 +59,6 @@ export const environment = {
validateSegments: '/segments/validation',
importSegments: '/segments/import',
exportSegments: '/segments/export/json',
exportSegment: '/segments/export',
exportSegmentCSV: '/segments/export/csv',
getGroupAssignmentStatus: '/experiments/getGroupAssignmentStatus',
},
Expand Down
Loading

0 comments on commit b2012d7

Please sign in to comment.