Skip to content

Commit

Permalink
N21-1506-sharing-ctl-tools (#5353)
Browse files Browse the repository at this point in the history
- migration script for garbage data cleanup
- adapt logic for sharing tools in course for different schools
  • Loading branch information
GordonNicholasCap authored Dec 9, 2024
1 parent 2104505 commit 2b7c697
Show file tree
Hide file tree
Showing 27 changed files with 1,425 additions and 196 deletions.
65 changes: 65 additions & 0 deletions apps/server/src/migrations/mikro-orm/Migration20241120100616.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Migration } from '@mikro-orm/migrations-mongodb';
import { ObjectId } from '@mikro-orm/mongodb';

export class Migration20241120100616 extends Migration {
async up(): Promise<void> {
const cursor = this.getCollection<{ contextType: string; contextId: ObjectId; schoolTool: ObjectId }>(
'context-external-tools'
).find({
$or: [{ contextType: 'course' }, { contextType: 'boardElement' }],
});

let numberOfDeletedTools = 0;
let numberOfDeletedElements = 0;
for await (const tool of cursor) {
let courseId: ObjectId | undefined;
if (tool.contextType === 'course') {
courseId = tool.contextId;
} else if (tool.contextType === 'boardElement') {
const element = await this.getCollection<{ path: string }>('boardnodes').findOne({
_id: tool.contextId,
});

if (element) {
const boardId = new ObjectId(element.path.split(',')[1]);

const board = await this.getCollection<{ context: ObjectId }>('boardnodes').findOne({
_id: boardId,
});

if (board) {
courseId = board.context;
}
}
}

if (courseId) {
const course = await this.getCollection<{ schoolId: ObjectId }>('courses').findOne({ _id: courseId });

const schoolTool = await this.getCollection<{ school: ObjectId }>('school-external-tools').findOne({
_id: tool.schoolTool,
});

if (!course || !schoolTool || course.schoolId.toString() !== schoolTool.school.toString()) {
await this.driver.nativeDelete('context-external-tools', { _id: tool._id });
console.info(`deleted context external tool: ${tool._id.toString()}`);
numberOfDeletedTools += 1;
if (tool.contextType === 'boardElement') {
await this.driver.nativeDelete('boardnodes', { _id: tool.contextId });
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
console.info(`deleted boardnode: ${tool.contextId}`);
numberOfDeletedElements += 1;
}
}
}
}
console.info(
`Deleted ${numberOfDeletedTools} context external tools and ${numberOfDeletedElements} external tool elements.`
);
}

// eslint-disable-next-line @typescript-eslint/require-await
async down(): Promise<void> {
console.info('Unfortunately the deleted documents cannot be restored. Use a backup.');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export class BoardController {
@Param() urlParams: BoardUrlParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<CopyApiResponse> {
const copyStatus = await this.boardUc.copyBoard(currentUser.userId, urlParams.boardId);
const copyStatus = await this.boardUc.copyBoard(currentUser.userId, urlParams.boardId, currentUser.schoolId);
const dto = CopyMapper.mapToResponse(copyStatus);
return dto;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { EntityId } from '@shared/domain/types';
import { StorageLocation } from '@src/modules/files-storage/interface';
import { ObjectId } from '@mikro-orm/mongodb';
import { CopyElementType, CopyStatus, CopyStatusEnum } from '../../copy-helper';
import { BoardExternalReference, BoardExternalReferenceType, ColumnBoard } from '../domain';
import { BoardNodeRepo } from '../repo';
Expand Down Expand Up @@ -114,6 +115,7 @@ describe('ColumnBoardService', () => {
sourceStorageLocationReference: { id: '1', type: StorageLocation.SCHOOL },
targetStorageLocationReference: { id: '1', type: StorageLocation.SCHOOL },
userId: '1',
targetSchoolId: new ObjectId().toHexString(),
});

expect(result).toEqual(copyStatus);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe(BoardNodeCopyContext.name, () => {
targetStorageLocationReference: { id: new ObjectId().toHexString(), type: StorageLocation.SCHOOL },
userId: new ObjectId().toHexString(),
filesStorageClientAdapterService: createMock<FilesStorageClientAdapterService>(),
targetSchoolId: new ObjectId().toHexString(),
};

const copyContext = new BoardNodeCopyContext(contextProps);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ export type BoardNodeCopyContextProps = {
targetStorageLocationReference: StorageLocationReference;
userId: EntityId;
filesStorageClientAdapterService: FilesStorageClientAdapterService;
targetSchoolId: EntityId;
};

export class BoardNodeCopyContext implements CopyContext {
constructor(private readonly props: BoardNodeCopyContextProps) {}
readonly targetSchoolId: EntityId;

constructor(private readonly props: BoardNodeCopyContextProps) {
this.targetSchoolId = props.targetSchoolId;
}

copyFilesOfParent(sourceParentId: EntityId, targetParentId: EntityId): Promise<CopyFileDto[]> {
return this.props.filesStorageClientAdapterService.copyFilesOfParent({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createMock } from '@golevelup/ts-jest';
import { ObjectId } from '@mikro-orm/mongodb';
import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper';
import { StorageLocation } from '@modules/files-storage/interface';
import { SchoolExternalToolService } from '@modules/tool/school-external-tool/service';
import { ContextExternalToolService } from '@modules/tool/context-external-tool/service';
import { ToolConfig } from '@modules/tool/tool-config';
import { ConfigService } from '@nestjs/config';
Expand Down Expand Up @@ -48,6 +49,10 @@ describe(BoardNodeCopyService.name, () => {
provide: CopyHelperService,
useValue: createMock<CopyHelperService>(),
},
{
provide: SchoolExternalToolService,
useValue: createMock<SchoolExternalToolService>(),
},
],
}).compile();

Expand All @@ -70,6 +75,7 @@ describe(BoardNodeCopyService.name, () => {
targetStorageLocationReference: { id: new ObjectId().toHexString(), type: StorageLocation.SCHOOL },
userId: new ObjectId().toHexString(),
filesStorageClientAdapterService: createMock<FilesStorageClientAdapterService>(),
targetSchoolId: new ObjectId().toHexString(),
};

const copyContext = new BoardNodeCopyContext(contextProps);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '
import { StorageLocation } from '@modules/files-storage/interface';
import { ContextExternalToolService } from '@modules/tool/context-external-tool/service';
import { ToolConfig } from '@modules/tool/tool-config';
import { copyContextExternalToolRejectDataFactory } from '@modules/tool/context-external-tool/testing';
import { CopyContextExternalToolRejectData } from '@modules/tool/context-external-tool/domain';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { setupEntities } from '@shared/testing';
import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client';
import { CopyFileDto } from '@src/modules/files-storage-client/dto';
import { contextExternalToolFactory } from '@src/modules/tool/context-external-tool/testing';

import {
Card,
CollaborativeTextEditorElement,
Expand Down Expand Up @@ -103,6 +106,7 @@ describe(BoardNodeCopyService.name, () => {
targetStorageLocationReference: { id: new ObjectId().toHexString(), type: StorageLocation.SCHOOL },
userId: new ObjectId().toHexString(),
filesStorageClientAdapterService: createMock<FilesStorageClientAdapterService>(),
targetSchoolId: new ObjectId().toHexString(),
};

const copyContext = new BoardNodeCopyContext(contextProps);
Expand Down Expand Up @@ -420,23 +424,68 @@ describe(BoardNodeCopyService.name, () => {
const setupToolElement = () => {
const { copyContext, externalToolElement } = setupCopyEnabled();

const tool = contextExternalToolFactory.build();
const toolCopy = contextExternalToolFactory.build();
contextExternalToolService.findById.mockResolvedValueOnce(tool);
contextExternalToolService.copyContextExternalTool.mockResolvedValueOnce(toolCopy);
externalToolElement.contextExternalToolId = tool.id;
const contextTool = contextExternalToolFactory.build();
contextExternalToolService.findById.mockResolvedValueOnce(contextTool);
externalToolElement.contextExternalToolId = contextTool.id;

return { copyContext, externalToolElement, tool, toolCopy };
return { copyContext, externalToolElement, contextTool };
};

it('should copy the external tool', async () => {
const { copyContext, externalToolElement, tool, toolCopy } = setupToolElement();
describe('when the copying of context external tool is successful', () => {
const setupCopySuccess = () => {
const { copyContext, externalToolElement, contextTool } = setupToolElement();

const result = await service.copyExternalToolElement(externalToolElement, copyContext);
const copiedTool = contextExternalToolFactory.build();
contextExternalToolService.copyContextExternalTool.mockResolvedValue(copiedTool);

return { copyContext, externalToolElement, contextTool, copiedTool };
};

it('should return the copied entity as ExternalTool', async () => {
const { copyContext, externalToolElement, contextTool, copiedTool } = setupCopySuccess();

const result = await service.copyExternalToolElement(externalToolElement, copyContext);

expect(contextExternalToolService.copyContextExternalTool).toHaveBeenCalledWith(
contextTool,
result.copyEntity?.id,
copyContext.targetSchoolId
);
expect(result.copyEntity instanceof ExternalToolElement).toEqual(true);
expect((result.copyEntity as ExternalToolElement).contextExternalToolId).toEqual(copiedTool.id);
expect(result.type).toEqual(CopyElementType.EXTERNAL_TOOL_ELEMENT);
expect(result.status).toEqual(CopyStatusEnum.SUCCESS);
});
});

expect(contextExternalToolService.findById).toHaveBeenCalledWith(tool.id);
expect(contextExternalToolService.copyContextExternalTool).toHaveBeenCalledWith(tool, result.copyEntity?.id);
expect((result.copyEntity as ExternalToolElement).contextExternalToolId).toEqual(toolCopy.id);
describe('when the copying of context external tool is rejected', () => {
const setupCopyRejected = () => {
const { copyContext, externalToolElement, contextTool } = setupToolElement();

const copyRejectData = copyContextExternalToolRejectDataFactory.build();
const mockWithCorrectType = Object.create(
CopyContextExternalToolRejectData.prototype
) as CopyContextExternalToolRejectData;
Object.assign(mockWithCorrectType, { ...copyRejectData });
contextExternalToolService.copyContextExternalTool.mockResolvedValue(mockWithCorrectType);

return { copyContext, externalToolElement, contextTool };
};

it('should return the copied entity as DeletedElement', async () => {
const { externalToolElement, copyContext, contextTool } = setupCopyRejected();

const result = await service.copyExternalToolElement(externalToolElement, copyContext);

expect(contextExternalToolService.copyContextExternalTool).toHaveBeenCalledWith(
contextTool,
expect.any(String),
copyContext.targetSchoolId
);
expect(result.copyEntity instanceof DeletedElement).toEqual(true);
expect(result.type).toEqual(CopyElementType.EXTERNAL_TOOL_ELEMENT);
expect(result.status).toEqual(CopyStatusEnum.FAIL);
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ObjectId } from '@mikro-orm/mongodb';
import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper';
import { CopyFileDto } from '@modules/files-storage-client/dto';
import { ContextExternalToolService } from '@modules/tool/context-external-tool';
import { ContextExternalTool } from '@modules/tool/context-external-tool/domain';
import { ContextExternalTool, CopyContextExternalToolRejectData } from '@modules/tool/context-external-tool/domain';
import { ToolConfig } from '@modules/tool/tool-config';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
Expand All @@ -14,6 +14,7 @@ import {
CollaborativeTextEditorElement,
Column,
ColumnBoard,
ContentElementType,
DeletedElement,
DrawingElement,
ExternalToolElement,
Expand All @@ -30,6 +31,7 @@ import {
} from '../../domain';

export interface CopyContext {
targetSchoolId: EntityId;
copyFilesOfParent(sourceParentId: EntityId, targetParentId: EntityId): Promise<CopyFileDto[]>;
}

Expand Down Expand Up @@ -283,35 +285,60 @@ export class BoardNodeCopyService {
return Promise.resolve(result);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async copyExternalToolElement(original: ExternalToolElement, context: CopyContext): Promise<CopyStatus> {
const copy = new ExternalToolElement({
let copy: ExternalToolElement | DeletedElement;
copy = new ExternalToolElement({
...original.getProps(),
...this.buildSpecificProps([]),
});

let status: CopyStatusEnum;
if (this.configService.get('FEATURE_CTL_TOOLS_COPY_ENABLED') && original.contextExternalToolId) {
const linkedTool = await this.contextExternalToolService.findById(original.contextExternalToolId);
if (!this.configService.get('FEATURE_CTL_TOOLS_COPY_ENABLED') || !original.contextExternalToolId) {
const copyStatus: CopyStatus = {
copyEntity: copy,
type: CopyElementType.EXTERNAL_TOOL_ELEMENT,
status: CopyStatusEnum.SUCCESS,
};

if (linkedTool) {
const contextExternalToolCopy: ContextExternalTool =
await this.contextExternalToolService.copyContextExternalTool(linkedTool, copy.id);
return Promise.resolve(copyStatus);
}

copy.contextExternalToolId = contextExternalToolCopy.id;
const linkedTool = await this.contextExternalToolService.findById(original.contextExternalToolId);
if (!linkedTool) {
const copyStatus: CopyStatus = {
copyEntity: copy,
type: CopyElementType.EXTERNAL_TOOL_ELEMENT,
status: CopyStatusEnum.FAIL,
};

status = CopyStatusEnum.SUCCESS;
} else {
status = CopyStatusEnum.FAIL;
}
return copyStatus;
}

const contextToolCopyResult: ContextExternalTool | CopyContextExternalToolRejectData =
await this.contextExternalToolService.copyContextExternalTool(linkedTool, copy.id, context.targetSchoolId);

let copyStatus: CopyStatusEnum = CopyStatusEnum.SUCCESS;
if (contextToolCopyResult instanceof CopyContextExternalToolRejectData) {
copy = new DeletedElement({
id: new ObjectId().toHexString(),
path: copy.path,
level: copy.level,
position: copy.position,
children: [],
createdAt: new Date(),
updatedAt: new Date(),
deletedElementType: ContentElementType.EXTERNAL_TOOL,
title: contextToolCopyResult.externalToolName,
});

copyStatus = CopyStatusEnum.FAIL;
} else {
status = CopyStatusEnum.SUCCESS;
copy.contextExternalToolId = contextToolCopyResult.id;
}

const result: CopyStatus = {
copyEntity: copy,
type: CopyElementType.EXTERNAL_TOOL_ELEMENT,
status,
status: copyStatus,
};

return Promise.resolve(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ describe(ColumnBoardCopyService.name, () => {
describe('copyColumnBoard', () => {
const setup = () => {
const userId = new ObjectId().toHexString();
const targetSchoolId = new ObjectId().toHexString();
const course = courseFactory.buildWithId();
const originalBoard = columnBoardFactory.build({
context: { id: course.id, type: BoardExternalReferenceType.Course },
Expand All @@ -86,6 +87,7 @@ describe(ColumnBoardCopyService.name, () => {
targetStorageLocationReference: { id: course.school.id, type: StorageLocation.SCHOOL },
userId,
copyTitle: 'Board Copy',
targetSchoolId,
};

return { originalBoard, userId, copyParams };
Expand Down Expand Up @@ -165,6 +167,7 @@ describe(ColumnBoardCopyService.name, () => {
describe('when the copy response is not a ColumnBoard', () => {
const setup = () => {
const userId = new ObjectId().toHexString();
const targetSchoolId = new ObjectId().toHexString();
const course = courseFactory.buildWithId();
const originalBoard = columnBoardFactory.build({
context: { id: course.id, type: BoardExternalReferenceType.Course },
Expand All @@ -178,6 +181,7 @@ describe(ColumnBoardCopyService.name, () => {
targetStorageLocationReference: { id: course.school.id, type: StorageLocation.SCHOOL },
userId,
copyTitle: 'Board Copy',
targetSchoolId,
};

return { originalBoard, userId, copyParams };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type CopyColumnBoardParams = {
targetStorageLocationReference: StorageLocationReference;
userId: EntityId;
copyTitle?: string;
targetSchoolId: EntityId;
};

@Injectable()
Expand All @@ -36,6 +37,7 @@ export class ColumnBoardCopyService {
targetStorageLocationReference: params.targetStorageLocationReference,
userId: params.userId,
filesStorageClientAdapterService: this.filesStorageClientAdapterService,
targetSchoolId: params.targetSchoolId,
});

const copyStatus = await this.boardNodeCopyService.copy(originalBoard, copyContext);
Expand Down
Loading

0 comments on commit 2b7c697

Please sign in to comment.