Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement visitor to change ids in links #4625

Merged
merged 11 commits into from
Dec 18, 2023
1 change: 1 addition & 0 deletions apps/server/src/modules/board/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './service/card.service';
export * from './service/column-board.service';
export * from './service/column.service';
export * from './service/content-element.service';
export * from './service/column-board-copy.service';
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { LinkElement } from '@shared/domain/domainobject';
import { EntityId } from '@shared/domain/types';
import {
cardFactory,
columnBoardFactory,
columnFactory,
externalToolElementFactory,
fileElementFactory,
linkElementFactory,
richTextElementFactory,
submissionContainerElementFactory,
submissionItemFactory,
} from '@shared/testing';
import { drawingElementFactory } from '@shared/testing/factory/domainobject/board/drawing-element.do.factory';
import { ObjectId } from 'bson';
import { SwapInternalLinksVisitor } from './swap-internal-links.visitor';

describe('swap internal links visitor', () => {
it('should keep link unchanged', () => {
const map = new Map<EntityId, EntityId>();
const linkElement = linkElementFactory.build({ url: 'testurl.dev' });
const visitor = new SwapInternalLinksVisitor(map);

linkElement.accept(visitor);

expect(linkElement.url).toEqual('testurl.dev');
});

const setupIdPair = () => {
const originalId = new ObjectId().toString();
const newId = new ObjectId().toString();
const value = {
originalId,
newId,
originalUrl: `testurl.dev/${originalId}`,
expectedUrl: `testurl.dev/${newId}`,
};
return value;
};

const buildIdMap = (pairs: { originalId: string; newId: string }[]) => {
const map = new Map<EntityId, EntityId>();
pairs.forEach((pair) => {
map.set(pair.originalId, pair.newId);
});
return map;
};

const buildBoardContaining = (linkelelements: LinkElement[]) => {
const cardContainingLinks = cardFactory.build({ children: linkelelements });
const submissionContainer = submissionContainerElementFactory.build({
children: [
submissionItemFactory.build({
children: [richTextElementFactory.build()],
}),
],
});
const cardContainingOthers = cardFactory.build({
children: [
richTextElementFactory.build(),
fileElementFactory.build(),
submissionContainer,
drawingElementFactory.build(),
externalToolElementFactory.build(),
],
});
const column = columnFactory.build({
children: [cardContainingLinks, cardContainingOthers],
});
const columnBoard = columnBoardFactory.build({
children: [column],
});
return columnBoard;
};

describe('when a single id is matched', () => {
const setupWithIdPair = () => {
const pair = setupIdPair();
const map = buildIdMap([pair]);
const visitor = new SwapInternalLinksVisitor(map);

return { pair, visitor };
};

it('should change ids in link', () => {
const { pair, visitor } = setupWithIdPair();
const linkElement = linkElementFactory.build({ url: pair.originalUrl });

linkElement.accept(visitor);

expect(linkElement.url).toEqual(pair.expectedUrl);
});

it('should change ids in multiple matching links', () => {
const { pair, visitor } = setupWithIdPair();
const firstLinkElement = linkElementFactory.build({ url: pair.originalUrl });
const secondLinkElement = linkElementFactory.build({ url: pair.originalUrl });
const root = buildBoardContaining([firstLinkElement, secondLinkElement]);

root.accept(visitor);

expect(firstLinkElement.url).toEqual(pair.expectedUrl);
expect(secondLinkElement.url).toEqual(pair.expectedUrl);
});
});

describe('when multiple different ids are matched', () => {
const setupWithMultipleIds = () => {
const pairs = [setupIdPair(), setupIdPair()];

const idMap = buildIdMap(pairs);
const visitor = new SwapInternalLinksVisitor(idMap);

return { visitor, pairs };
};

const buildLinkElementsWithUrls = (urls: string[]) => urls.map((url) => linkElementFactory.build({ url }));

it('should change multiple ids in different links', () => {
const { visitor, pairs } = setupWithMultipleIds();
const linkElements = buildLinkElementsWithUrls(pairs.map((pair) => pair.originalUrl));
const root = buildBoardContaining(linkElements);

root.accept(visitor);

expect(linkElements[0].url).toEqual(pairs[0].expectedUrl);
expect(linkElements[1].url).toEqual(pairs[1].expectedUrl);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
AnyBoardDo,
BoardCompositeVisitor,
Card,
Column,
ColumnBoard,
LinkElement,
SubmissionContainerElement,
SubmissionItem,
} from '@shared/domain/domainobject';
import { DrawingElement } from '@shared/domain/domainobject/board/drawing-element.do';
import { EntityId } from '@shared/domain/types';

export class SwapInternalLinksVisitor implements BoardCompositeVisitor {
constructor(private readonly idMap: Map<EntityId, EntityId>) {}

visitDrawingElement(drawingElement: DrawingElement): void {
this.visitChildrenOf(drawingElement);
}

visitCard(card: Card): void {
this.visitChildrenOf(card);
}

visitColumn(column: Column): void {
this.visitChildrenOf(column);
}

visitColumnBoard(columnBoard: ColumnBoard): void {
this.visitChildrenOf(columnBoard);
}

visitExternalToolElement(): void {
this.doNothing();
}

visitFileElement(): void {
this.doNothing();
}

visitLinkElement(linkElement: LinkElement): void {
this.idMap.forEach((value, key) => {
linkElement.url = linkElement.url.replace(key, value);
});
}

visitRichTextElement(): void {
this.doNothing();
}

visitSubmissionContainerElement(submissionContainerElement: SubmissionContainerElement): void {
this.visitChildrenOf(submissionContainerElement);
}

visitSubmissionItem(submissionItem: SubmissionItem): void {
this.visitChildrenOf(submissionItem);
}

private visitChildrenOf(boardDo: AnyBoardDo) {
boardDo.children.forEach((child) => child.accept(this));
}

private doNothing() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,19 @@ import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helpe
import { UserService } from '@modules/user';
import { Test, TestingModule } from '@nestjs/testing';
import { BoardExternalReferenceType, ColumnBoard, UserDO } from '@shared/domain/domainobject';
import { EntityId } from '@shared/domain/types';
import { CourseRepo } from '@shared/repo';
import { columnBoardFactory, courseFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing';
import {
cardFactory,
columnBoardFactory,
columnFactory,
courseFactory,
linkElementFactory,
schoolFactory,
setupEntities,
userFactory,
} from '@shared/testing';
import { ObjectId } from 'bson';
import { BoardDoRepo } from '../repo';
import {
BoardDoCopyService,
Expand Down Expand Up @@ -168,4 +179,53 @@ describe('column board copy service', () => {
expect(result).toEqual(expectedCopyStatus);
});
});

describe('when changing linked ids', () => {
const setup = () => {
const linkedIdBefore = new ObjectId().toString();
const linkElement = linkElementFactory.build({
url: `someurl/${linkedIdBefore}`,
});
const board = columnBoardFactory.build({
children: [
columnFactory.build({
children: [
cardFactory.build({
children: [linkElement],
}),
],
}),
],
});
boardRepo.findById.mockResolvedValue(board);

return { board, linkElement, linkedIdBefore };
};

it('should get board', async () => {
const { board } = setup();

await service.swapLinkedIds(board.id, new Map<EntityId, EntityId>());

expect(boardRepo.findById).toHaveBeenCalledWith(board.id);
});

it('should update links in board', async () => {
const { board, linkElement, linkedIdBefore } = setup();
const expectedId = new ObjectId().toString();
const map = new Map<EntityId, EntityId>().set(linkedIdBefore, expectedId);

await service.swapLinkedIds(board.id, map);

expect(linkElement.url).toEqual(`someurl/${expectedId}`);
});

it('should persist updates', async () => {
const { board } = setup();

await service.swapLinkedIds(board.id, new Map<EntityId, EntityId>());

expect(boardRepo.save).toHaveBeenCalledWith(board);
});
});
});
13 changes: 13 additions & 0 deletions apps/server/src/modules/board/service/column-board-copy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { EntityId } from '@shared/domain/types';
import { CourseRepo } from '@shared/repo';
import { BoardDoRepo } from '../repo';
import { BoardDoCopyService, SchoolSpecificFileCopyServiceFactory } from './board-do-copy-service';
import { SwapInternalLinksVisitor } from './board-do-copy-service/swap-internal-links.visitor';

@Injectable()
export class ColumnBoardCopyService {
Expand Down Expand Up @@ -54,4 +55,16 @@ export class ColumnBoardCopyService {

return copyStatus;
}

public async swapLinkedIds(boardId: EntityId, idMap: Map<EntityId, EntityId>) {
const board = await this.boardDoRepo.findById(boardId);

const visitor = new SwapInternalLinksVisitor(idMap);

board.accept(visitor);

await this.boardDoRepo.save(board);

return board;
}
}
Loading
Loading