Skip to content

Commit

Permalink
implement visitor to change ids in links (#4625)
Browse files Browse the repository at this point in the history
* implement a board visitor that recieves a mapping if ids, and replaces them in all linkelements in the board.

* add a function to boardCopyService to use said visitor

* after copying a course, the function is called for all Columnboards in the course, with the ids of all elements of the courseboard as well as the course itself
  • Loading branch information
Metauriel authored Dec 18, 2023
1 parent 32720d3 commit 5d3043a
Show file tree
Hide file tree
Showing 10 changed files with 401 additions and 9 deletions.
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;
}
}
1 change: 0 additions & 1 deletion apps/server/src/modules/files-storage/helper/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './file-name';
export * from './file-record';
export * from './path';
export * from './promise';
Loading

0 comments on commit 5d3043a

Please sign in to comment.