Skip to content

Commit

Permalink
N21-1941 Bulk import for external tools (#5023)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarvinOehlerkingCap authored May 27, 2024
1 parent c64ee05 commit 62820fb
Show file tree
Hide file tree
Showing 12 changed files with 421 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import { SchoolExternalToolEntity } from '../../../school-external-tool/entity';
import { ExternalToolEntity } from '../../entity';
import { externalToolEntityFactory, externalToolFactory } from '../../testing';
import {
ExternalToolBulkCreateParams,
ExternalToolCreateParams,
ExternalToolImportResultListResponse,
ExternalToolImportResultResponse,
ExternalToolMetadataResponse,
ExternalToolResponse,
ExternalToolSearchListResponse,
Expand Down Expand Up @@ -221,6 +224,108 @@ describe('ToolController (API)', () => {
});
});

describe('[POST] tools/external-tools', () => {
const logoUrl = 'https://link.to-my-logo.com';
const postParams: ExternalToolCreateParams = {
name: 'Tool 1',
parameters: [
{
name: 'key',
description: 'This is a parameter.',
displayName: 'User Friendly Name',
defaultValue: 'abc',
isOptional: false,
isProtected: false,
type: CustomParameterTypeParams.STRING,
regex: 'abc',
regexComment: 'Regex accepts "abc" as value.',
location: CustomParameterLocationParams.PATH,
scope: CustomParameterScopeTypeParams.GLOBAL,
},
],
config: {
type: ToolConfigType.BASIC,
baseUrl: 'https://link.to-my-tool.com/:key',
},
isHidden: false,
isDeactivated: false,
logoUrl,
url: 'https://link.to-my-tool.com',
openNewTab: true,
medium: {
mediumId: 'medium:1',
mediaSourceId: 'source:1',
},
};

describe('when valid data is given', () => {
const setup = async () => {
const params: ExternalToolBulkCreateParams = { data: [{ ...postParams }] };

const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({}, [Permission.TOOL_ADMIN]);
await em.persistAndFlush([adminAccount, adminUser]);
em.clear();

const base64Logo: string = externalToolFactory.withBase64Logo().build().logo as string;
const logoBuffer: Buffer = Buffer.from(base64Logo, 'base64');
axiosMock.onGet(logoUrl).reply(HttpStatus.OK, logoBuffer);

const loggedInClient: TestApiClient = await testApiClient.login(adminAccount);

return {
loggedInClient,
params,
};
};

it('should create a tool', async () => {
const { loggedInClient, params } = await setup();

const response: Response = await loggedInClient.post('/import', params);
const body: ExternalToolImportResultListResponse = response.body as ExternalToolImportResultListResponse;

expect(body.results[0]?.toolId).toBeDefined();
const loaded: Loaded<ExternalToolEntity> = await em.findOneOrFail(ExternalToolEntity, {
id: body.results[0].toolId,
});
expect(loaded).toBeDefined();
});

it('should return the created tool', async () => {
const { loggedInClient, params } = await setup();

const response: Response = await loggedInClient.post('/import', params);
const body: ExternalToolImportResultListResponse = response.body as ExternalToolImportResultListResponse;

expect(response.statusCode).toEqual(HttpStatus.CREATED);
expect(body.results[0]).toBeDefined();
expect(body.results[0]).toEqual<ExternalToolImportResultResponse>({
toolId: expect.any(String),
mediumId: postParams.medium?.mediumId,
mediumSourceId: postParams.medium?.mediaSourceId,
toolName: postParams.name,
error: undefined,
});
});
});

describe('when user is not authenticated', () => {
const setup = () => {
const params: ExternalToolBulkCreateParams = { data: [{ ...postParams }] };

return { params };
};

it('should return unauthorized', async () => {
const { params } = setup();

const response: Response = await testApiClient.post('/import', params);

expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED);
});
});
});

describe('[GET] tools/external-tools', () => {
describe('when requesting tools', () => {
const setup = async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ValidateNested } from 'class-validator';
import { ExternalToolCreateParams } from './external-tool-create.params';

export class ExternalToolBulkCreateParams {
@ValidateNested()
@Type(() => ExternalToolCreateParams)
@ApiProperty({ type: [ExternalToolCreateParams], description: 'List of external tools' })
data!: ExternalToolCreateParams[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './context-ref.params';
export * from './school-external-tool-id.params';
export * from './context-external-tool-id.params';
export { ExternalToolMediumParams } from './external-tool-medium.params';
export { ExternalToolBulkCreateParams } from './external-tool-bulk-create.params';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ApiProperty } from '@nestjs/swagger';

export class ExternalToolImportResultResponse {
@ApiProperty({ description: 'Name of the external tool' })
toolName: string;

@ApiProperty({ description: 'Medium id of the external tool' })
mediumId?: string;

@ApiProperty({ description: 'Medium source of the external tool' })
mediumSourceId?: string;

@ApiProperty({ description: 'ObjectId of the created external tool' })
toolId?: string;

@ApiProperty({ description: 'Status message of the error that occurred' })
error?: string;

constructor(props: ExternalToolImportResultResponse) {
this.toolName = props.toolName;
this.mediumId = props.mediumId;
this.mediumSourceId = props.mediumSourceId;
this.toolId = props.toolId;
this.error = props.error;
}
}

export class ExternalToolImportResultListResponse {
@ApiProperty({
type: [ExternalToolImportResultResponse],
description: 'List of operation results for the provided external tools',
})
results: ExternalToolImportResultResponse[];

constructor(props: ExternalToolImportResultListResponse) {
this.results = props.results;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ export * from './school-external-tool-configuration-template-list.response';
export * from './external-tool-metadata.response';
export * from './tool-context-types-list.response';
export { ExternalToolMediumResponse } from './external-tool-medium.response';
export {
ExternalToolImportResultListResponse,
ExternalToolImportResultResponse,
} from './external-tool-import-result-response';
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
StreamableFile,
} from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiCreatedResponse,
ApiForbiddenResponse,
ApiFoundResponse,
Expand All @@ -36,10 +37,12 @@ import { ExternalToolLogo } from '../domain/external-tool-logo';

import { ExternalToolMetadataMapper, ExternalToolRequestMapper, ExternalToolResponseMapper } from '../mapper';
import { ExternalToolLogoService } from '../service';
import { ExternalToolCreate, ExternalToolUc, ExternalToolUpdate } from '../uc';
import { ExternalToolCreate, ExternalToolImportResult, ExternalToolUc, ExternalToolUpdate } from '../uc';
import {
ExternalToolBulkCreateParams,
ExternalToolCreateParams,
ExternalToolIdParams,
ExternalToolImportResultListResponse,
ExternalToolMetadataResponse,
ExternalToolResponse,
ExternalToolSearchListResponse,
Expand Down Expand Up @@ -81,6 +84,28 @@ export class ToolController {
return mapped;
}

@Post('/import')
@ApiCreatedResponse({ description: 'The Tool has been successfully created.', type: ExternalToolResponse })
@ApiForbiddenResponse({ description: 'User is not allowed to access this resource.' })
@ApiUnauthorizedResponse({ description: 'User is not logged in.' })
@ApiBadRequestResponse({ description: 'Request data has invalid format.' })
@ApiOperation({ summary: 'Creates multiple ExternalTools at the same time.' })
async importExternalTools(
@CurrentUser() currentUser: ICurrentUser,
@Body() externalToolBulkParams: ExternalToolBulkCreateParams
): Promise<ExternalToolImportResultListResponse> {
const externalTools: ExternalToolCreate[] = this.externalToolDOMapper.mapBulkCreateRequest(externalToolBulkParams);

const results: ExternalToolImportResult[] = await this.externalToolUc.importExternalTools(
currentUser.userId,
externalTools
);

const response: ExternalToolImportResultListResponse = ExternalToolResponseMapper.mapToImportResponse(results);

return response;
}

@Get()
@ApiFoundResponse({ description: 'Tools has been found.', type: ExternalToolSearchListResponse })
@ApiUnauthorizedResponse()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ExternalToolSearchQuery } from '../../common/interface';
import {
BasicToolConfigParams,
CustomParameterPostParams,
ExternalToolBulkCreateParams,
ExternalToolCreateParams,
ExternalToolMediumParams,
ExternalToolSearchParams,
Expand Down Expand Up @@ -119,6 +120,14 @@ export class ExternalToolRequestMapper {
};
}

public mapBulkCreateRequest(externalToolCreateParams: ExternalToolBulkCreateParams): ExternalToolCreate[] {
const toolList: ExternalToolCreate[] = externalToolCreateParams.data.map(
(createParams: ExternalToolCreateParams): ExternalToolCreate => this.mapCreateRequest(createParams)
);

return toolList;
}

private mapRequestToExternalToolMedium(
externalToolMediumParams: ExternalToolMediumParams | undefined
): ExternalToolMediumDto | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import {
import {
BasicToolConfigResponse,
CustomParameterResponse,
ExternalToolImportResultListResponse,
ExternalToolImportResultResponse,
ExternalToolMediumResponse,
ExternalToolResponse,
Lti11ToolConfigResponse,
Oauth2ToolConfigResponse,
} from '../controller/dto';
import { BasicToolConfig, ExternalTool, ExternalToolMedium, Lti11ToolConfig, Oauth2ToolConfig } from '../domain';
import { ExternalToolImportResult } from '../uc';

const scopeMapping: Record<CustomParameterScope, CustomParameterScopeTypeParams> = {
[CustomParameterScope.GLOBAL]: CustomParameterScopeTypeParams.GLOBAL,
Expand Down Expand Up @@ -110,4 +113,15 @@ export class ExternalToolResponseMapper {
};
});
}

public static mapToImportResponse(results: ExternalToolImportResult[]): ExternalToolImportResultListResponse {
const response: ExternalToolImportResultListResponse = new ExternalToolImportResultListResponse({
results: results.map(
(result: ExternalToolImportResult): ExternalToolImportResultResponse =>
new ExternalToolImportResultResponse(result)
),
});

return response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export class ExternalToolImportResult {
toolName: string;

mediumId?: string;

mediumSourceId?: string;

toolId?: string;

error?: string;

constructor(props: ExternalToolImportResult) {
this.toolName = props.toolName;
this.mediumId = props.mediumId;
this.mediumSourceId = props.mediumSourceId;
this.toolId = props.toolId;
this.error = props.error;
}
}
1 change: 1 addition & 0 deletions apps/server/src/modules/tool/external-tool/uc/dto/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './external-tool.types';
export * from './external-tool-configuration.types';
export { ExternalToolImportResult } from './external-tool-import-result';
Loading

0 comments on commit 62820fb

Please sign in to comment.