Skip to content

Commit

Permalink
list endpoint and segment migration (#2145)
Browse files Browse the repository at this point in the history
* list endpoint and segment migration

* delete segments in transaction; fix typing
  • Loading branch information
bcb37 authored Dec 9, 2024
1 parent 9888aaf commit 25fa07d
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 10 deletions.
47 changes: 47 additions & 0 deletions backend/packages/Upgrade/src/api/controllers/SegmentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Segment } from '../models/Segment';
import { AppRequest } from '../../types';
import {
IdValidator,
ListInputValidator,
SegmentFile,
SegmentIds,
SegmentIdValidator,
Expand Down Expand Up @@ -60,6 +61,17 @@ export interface getSegmentData {
* items:
* type: string
* example: '5812a759-1dcf-47a8-b0ba-26c89092863e'
* ListInput:
* allOf:
* - $ref: '#/definitions/Segment'
* required:
* - parentSegmentId
* - listType
* properties:
* parentSegmentId:
* type: string
* listType:
* type: string
* segmentResponse:
* description: ''
* type: object
Expand Down Expand Up @@ -343,6 +355,41 @@ export class SegmentController {
return this.segmentService.upsertSegment(segment, request.logger);
}

/**
* @swagger
* /list:
* post:
* description: Create a new list
* tags:
* - Segment
* produces:
* - application/json
* parameters:
* - in: body
* name: list
* description: List object
* required: true
* schema:
* type: object
* $ref: '#/definitions/ListInput'
* responses:
* '200':
* description: Create a new list
* schema:
* $ref: '#/definitions/segmentResponse'
* '401':
* description: Authorization Required Error
* '500':
* description: Internal Server Error, Insert Error in database, SegmentId is not valid, JSON format is not valid
*/
@Post('/list')
public addList(
@Body({ validate: true }) listInput: ListInputValidator,
@Req() request: AppRequest
): Promise<Segment> {
return this.segmentService.addList(listInput, request.logger);
}

/**
* @swagger
* /segments/{segmentId}:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export class SegmentInputValidator {
@IsOptional()
public description?: string;

@IsString()
@IsOptional()
public listType?: string;

@IsNotEmpty()
@IsString()
public context: string;
Expand All @@ -46,6 +50,12 @@ export class SegmentInputValidator {
public subSegmentIds: string[];
}

export class ListInputValidator extends SegmentInputValidator {
@IsNotEmpty()
@IsUUID()
public parentSegmentId: string;
}

export class IdValidator {
@IsNotEmpty()
@IsUUID()
Expand Down
5 changes: 5 additions & 0 deletions backend/packages/Upgrade/src/api/models/Segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export class Segment extends BaseModel {
})
public description: string;

@Column({
nullable: true,
})
public listType: string;

@Column()
public context: string;

Expand Down
52 changes: 50 additions & 2 deletions backend/packages/Upgrade/src/api/services/SegmentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
SegmentFile,
Group,
SegmentValidationObj,
ListInputValidator,
} from '../controllers/validators/SegmentInputValidator';
import { ExperimentSegmentExclusionRepository } from '../repositories/ExperimentSegmentExclusionRepository';
import { ExperimentSegmentInclusionRepository } from '../repositories/ExperimentSegmentInclusionRepository';
Expand Down Expand Up @@ -297,6 +298,25 @@ export class SegmentService {
return this.addSegmentDataInDB(segment, logger);
}

public async addList(listInput: ListInputValidator, logger: UpgradeLogger): Promise<Segment> {
logger.info({ message: `Adding list => ${JSON.stringify(listInput, undefined, 2)}` });
const manager = this.dataSource;
const { parentSegmentId, ...segmentInput } = listInput;
const newList: SegmentInputValidator = { ...segmentInput, type: SEGMENT_TYPE.PRIVATE };
const createdSegment = await manager.transaction(async (transactionalEntityManager) => {
const createdSegment = await this.upsertSegmentInPipeline(newList, logger, transactionalEntityManager);
const parentSegment = await this.getSegmentById(parentSegmentId, logger);
if (!parentSegment) {
throw new Error('Parent Segment not found');
}
parentSegment.subSegments = [...parentSegment.subSegments, createdSegment];

await transactionalEntityManager.getRepository(Segment).save(parentSegment);
return createdSegment;
});
return createdSegment;
}

public upsertSegmentInPipeline(
segment: SegmentInputValidator,
logger: UpgradeLogger,
Expand All @@ -308,7 +328,34 @@ export class SegmentService {

public async deleteSegment(id: string, logger: UpgradeLogger): Promise<Segment> {
logger.info({ message: `Delete segment by id. segmentId: ${id}` });
return await this.segmentRepository.deleteSegment(id, logger);
const manager = this.dataSource;
const deletedSegment = manager.transaction(async (transactionalEntityManager) => {
return this.deleteSegmentAndPrivateSubsegments(id, logger, transactionalEntityManager);
});
return deletedSegment;
}

private async deleteSegmentAndPrivateSubsegments(
id: string,
logger: UpgradeLogger,
manager: EntityManager
): Promise<Segment> {
const segmentDoc = await manager.getRepository(Segment).findOne({
where: { id: id },
relations: ['individualForSegment', 'groupForSegment', 'subSegments'],
});
if (!segmentDoc) {
throw new Error(SERVER_ERROR.QUERY_FAILED);
}
await Promise.all(
segmentDoc.subSegments.map((subSegment) => {
if (subSegment.type === SEGMENT_TYPE.PRIVATE) {
this.deleteSegmentAndPrivateSubsegments(subSegment.id, logger, manager);
}
})
);
const deletedSegmentResponse = await this.segmentRepository.deleteSegments([id], logger, manager);
return deletedSegmentResponse[0];
}

public async validateSegments(segments: SegmentFile[], logger: UpgradeLogger): Promise<SegmentImportError[]> {
Expand Down Expand Up @@ -584,7 +631,7 @@ export class SegmentService {

// create/update segment document
segment.id = segment.id || uuid();
const { id, name, description, context, type } = segment;
const { id, name, description, context, type, listType } = segment;
const allSegments = await this.getSegmentByIds(segment.subSegmentIds);
const subSegmentData = segment.subSegmentIds
.filter((subSegmentId) => {
Expand All @@ -610,6 +657,7 @@ export class SegmentService {
description,
context,
type,
listType,
subSegments: subSegmentData,
});
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

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

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "segment" ADD "listType" character varying`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "segment" DROP COLUMN "listType"`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { globalExcludeSegment } from '../../../src/init/seed/globalExcludeSegmen
import Container from 'typedi';
import { SegmentService } from '../../../src/api/services/SegmentService';
import { UpgradeLogger } from '../../../src/lib/logger/UpgradeLogger';
import { segment, segmentSecond } from '../mockData/segment';
import { segment, segmentSecond, segmentList, segmentWithList } from '../mockData/segment';

export default async function SegmentDelete(): Promise<void> {
const segmentService = Container.get<SegmentService>(SegmentService);
Expand Down Expand Up @@ -174,4 +174,17 @@ export default async function SegmentDelete(): Promise<void> {
}),
])
);

// Create segment with List (private segment)
const segmentWithListObject = segmentWithList;

await segmentService.upsertSegment(segmentWithListObject, new UpgradeLogger());
const privateSegmentObject = segmentList;
// adds a private segment
await segmentService.addList(privateSegmentObject, new UpgradeLogger());
segments = await segmentService.getAllSegments(new UpgradeLogger());
expect(segments.length).toEqual(3);
await segmentService.deleteSegment(segmentWithListObject.id, new UpgradeLogger());
segments = await segmentService.getAllSegments(new UpgradeLogger());
expect(segments.length).toEqual(2);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ export const segmentThird = {
context: 'home',
type: SEGMENT_TYPE.PRIVATE,
userIds: [],
groups: [
{ groupId: '1', type: 'teacher' },
],
groups: [{ groupId: '1', type: 'teacher' }],
subSegmentIds: [],
};

Expand All @@ -50,4 +48,28 @@ export const segmentFourth = {
userId: 'student1',
groups: [],
subSegmentIds: [],
};
};

export const segmentList = {
id: '5e05fc7a-3553-47d7-87d8-b380d389ef7c',
name: 'segmentLiat',
description: 'included users',
context: 'home',
listType: 'user',
type: SEGMENT_TYPE.PRIVATE,
userIds: ['student1'],
groups: [],
subSegmentIds: [],
parentSegmentId: 'e0a0d838-d645-4d89-856e-89bdf6f39394',
};

export const segmentWithList = {
id: 'e0a0d838-d645-4d89-856e-89bdf6f39394',
name: 'segmentWithList',
description: 'segment with list description',
context: 'home',
type: SEGMENT_TYPE.PUBLIC,
userIds: [],
groups: [],
subSegmentIds: [],
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { ExperimentSegmentInclusionRepository } from '../../../src/api/repositor
import { FeatureFlagSegmentExclusionRepository } from '../../../src/api/repositories/FeatureFlagSegmentExclusionRepository';
import { FeatureFlagSegmentInclusionRepository } from '../../../src/api/repositories/FeatureFlagSegmentInclusionRepository';
import { CacheService } from '../../../src/api/services/CacheService';
import { SegmentFile, SegmentInputValidator } from '../../../src/api/controllers/validators/SegmentInputValidator';
import {
ListInputValidator,
SegmentFile,
SegmentInputValidator,
} from '../../../src/api/controllers/validators/SegmentInputValidator';
import { IndividualForSegment } from '../../../src/api/models/IndividualForSegment';
import { GroupForSegment } from '../../../src/api/models/GroupForSegment';
import { Experiment } from '../../../src/api/models/Experiment';
Expand All @@ -37,6 +41,7 @@ const segValSegment = new Segment();
const logger = new UpgradeLogger();
const segmentArr = [seg1, seg2];
const segVal = new SegmentInputValidator();
const listVal = new ListInputValidator();
const include = [{ segment: seg1, experiment: exp }];
const ff_include = [{ segment: seg1, featureFlag: ff }];
const segValImportFile: SegmentFile = {
Expand Down Expand Up @@ -171,6 +176,9 @@ describe('Segment Service Testing', () => {
deleteSegment: jest.fn().mockImplementation((seg) => {
return seg;
}),
deleteSegments: jest.fn().mockImplementation((seg) => {
return seg;
}),
createQueryBuilder: jest.fn(() => ({
insert: jest.fn().mockReturnThis(),
leftJoinAndSelect: jest.fn().mockReturnThis(),
Expand Down Expand Up @@ -494,4 +502,10 @@ describe('Segment Service Testing', () => {
await service.exportSegments([seg1.id], logger);
}).rejects.toThrow(new Error(SERVER_ERROR.QUERY_FAILED));
});

it('should add a list', async () => {
service.upsertSegmentInPipeline = jest.fn().mockResolvedValue(segValSegment);
const segment = await service.addList(listVal, logger);
expect(segment).toEqual(segValSegment);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ describe('SegmentsEffects', () => {
}));

it('should dispatch actionDeleteSegmentSuccess and navigate to segments page on success', fakeAsync(() => {
segmentsDataService.deleteSegment = jest.fn().mockReturnValue(of([{ ...mockSegment }]));
segmentsDataService.deleteSegment = jest.fn().mockReturnValue(of({ ...mockSegment }));

const expectedAction = SegmentsActions.actionDeleteSegmentSuccess({
segment: { ...mockSegment },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class SegmentsEffects {
this.segmentsDataService.deleteSegment(id).pipe(
map((data: any) => {
this.router.navigate(['/segments']);
return SegmentsActions.actionDeleteSegmentSuccess({ segment: data[0] });
return SegmentsActions.actionDeleteSegmentSuccess({ segment: data });
}),
catchError(() => [SegmentsActions.actionDeleteSegmentFailure()])
)
Expand Down

0 comments on commit 25fa07d

Please sign in to comment.