Skip to content

Commit

Permalink
BC-5434 - meta data endpoint (#4488)
Browse files Browse the repository at this point in the history
* refactor meta data extract functionality to a module and an independent endpoint
* if no open graph data can be fetched, then filename is taken from the url and used as title
  • Loading branch information
hoeppner-dataport authored Nov 3, 2023
1 parent 0d2718d commit a3fd43f
Show file tree
Hide file tree
Showing 28 changed files with 642 additions and 207 deletions.
2 changes: 0 additions & 2 deletions apps/server/src/modules/board/board.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
ColumnBoardService,
ColumnService,
ContentElementService,
OpenGraphProxyService,
SubmissionItemService,
} from './service';
import { BoardDoCopyService, SchoolSpecificFileCopyServiceFactory } from './service/board-do-copy-service';
Expand All @@ -38,7 +37,6 @@ import { ColumnBoardCopyService } from './service/column-board-copy.service';
BoardDoCopyService,
ColumnBoardCopyService,
SchoolSpecificFileCopyServiceFactory,
OpenGraphProxyService,
],
exports: [
BoardDoAuthorizableService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger
import { ContentElementType } from '@shared/domain';
import { InputFormat } from '@shared/domain/types';
import { Type } from 'class-transformer';
import { IsDate, IsEnum, IsMongoId, IsOptional, IsString, ValidateNested } from 'class-validator';
import { IsDate, IsEnum, IsMongoId, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator';

export abstract class ElementContentBody {
@IsEnum(ContentElementType)
Expand Down Expand Up @@ -32,10 +32,26 @@ export class FileElementContentBody extends ElementContentBody {
@ApiProperty()
content!: FileContentBody;
}

export class LinkContentBody {
@IsString()
@IsUrl()
@ApiProperty({})
url!: string;

@IsString()
@IsOptional()
@ApiProperty({})
title?: string;

@IsString()
@IsOptional()
@ApiProperty({})
description?: string;

@IsString()
@IsOptional()
@ApiProperty({})
imageUrl?: string;
}

export class LinkElementContentBody extends ElementContentBody {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import {
submissionContainerElementFactory,
submissionItemFactory,
} from '@shared/testing';
import { ExternalToolContentBody, FileContentBody, RichTextContentBody } from '../controller/dto';
import { ExternalToolContentBody, FileContentBody, LinkContentBody, RichTextContentBody } from '../controller/dto';
import { ContentElementUpdateVisitor } from './content-element-update.visitor';
import { OpenGraphProxyService } from './open-graph-proxy.service';

describe(ContentElementUpdateVisitor.name, () => {
describe('when visiting an unsupported component', () => {
Expand All @@ -25,8 +24,7 @@ describe(ContentElementUpdateVisitor.name, () => {
content.text = 'a text';
content.inputFormat = InputFormat.RICH_TEXT_CK5;
const submissionItem = submissionItemFactory.build();
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { board, column, card, submissionItem, updater };
};
Expand Down Expand Up @@ -66,8 +64,7 @@ describe(ContentElementUpdateVisitor.name, () => {
const content = new RichTextContentBody();
content.text = 'a text';
content.inputFormat = InputFormat.RICH_TEXT_CK5;
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { fileElement, updater };
};
Expand All @@ -79,31 +76,12 @@ describe(ContentElementUpdateVisitor.name, () => {
});
});

describe('when visiting a link element using the wrong content', () => {
const setup = () => {
const linkElement = linkElementFactory.build();
const content = new FileContentBody();
content.caption = 'a caption';
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);

return { linkElement, updater };
};

it('should throw an error', async () => {
const { linkElement, updater } = setup();

await expect(() => updater.visitLinkElementAsync(linkElement)).rejects.toThrow();
});
});

describe('when visiting a rich text element using the wrong content', () => {
const setup = () => {
const richTextElement = richTextElementFactory.build();
const content = new FileContentBody();
content.caption = 'a caption';
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { richTextElement, updater };
};
Expand All @@ -121,8 +99,7 @@ describe(ContentElementUpdateVisitor.name, () => {
const content = new RichTextContentBody();
content.text = 'a text';
content.inputFormat = InputFormat.RICH_TEXT_CK5;
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { submissionContainerElement, updater };
};
Expand All @@ -134,14 +111,76 @@ describe(ContentElementUpdateVisitor.name, () => {
});
});

describe('when visiting a link element', () => {
describe('when content is valid', () => {
const setup = () => {
const linkElement = linkElementFactory.build();
const content = new LinkContentBody();
content.url = 'https://super-example.com/';
content.title = 'SuperExample - the best examples in the web';
content.imageUrl = '/preview/image.jpg';
const updater = new ContentElementUpdateVisitor(content);

return { linkElement, content, updater };
};

it('should update the content', async () => {
const { linkElement, content, updater } = setup();

await updater.visitLinkElementAsync(linkElement);

expect(linkElement.url).toEqual(content.url);
expect(linkElement.title).toEqual(content.title);
expect(linkElement.imageUrl).toEqual(content.imageUrl);
});
});

describe('when content is not a link element', () => {
const setup = () => {
const linkElement = linkElementFactory.build();
const content = new FileContentBody();
content.caption = 'a caption';
const updater = new ContentElementUpdateVisitor(content);

return { linkElement, updater };
};

it('should throw an error', async () => {
const { linkElement, updater } = setup();

await expect(() => updater.visitLinkElementAsync(linkElement)).rejects.toThrow();
});
});

describe('when imageUrl for preview image is not a relative url', () => {
const setup = () => {
const linkElement = linkElementFactory.build();
const content = new LinkContentBody();
content.url = 'https://super-example.com/';
content.title = 'SuperExample - the best examples in the web';
content.imageUrl = 'https://www.external.de/fake-preview-image.jpg';
const updater = new ContentElementUpdateVisitor(content);

return { linkElement, content, updater };
};

it('should ignore the image url', async () => {
const { linkElement, updater } = setup();

await updater.visitLinkElementAsync(linkElement);

expect(linkElement.imageUrl).toBe('');
});
});
});

describe('when visiting a external tool element', () => {
describe('when visiting a external tool element with valid content', () => {
const setup = () => {
const externalToolElement = externalToolElementFactory.build({ contextExternalToolId: undefined });
const content = new ExternalToolContentBody();
content.contextExternalToolId = new ObjectId().toHexString();
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { externalToolElement, updater, content };
};
Expand All @@ -161,8 +200,7 @@ describe(ContentElementUpdateVisitor.name, () => {
const content = new RichTextContentBody();
content.text = 'a text';
content.inputFormat = InputFormat.RICH_TEXT_CK5;
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { externalToolElement, updater };
};
Expand All @@ -178,8 +216,7 @@ describe(ContentElementUpdateVisitor.name, () => {
const setup = () => {
const externalToolElement = externalToolElementFactory.build();
const content = new ExternalToolContentBody();
const openGraphProxyService = new OpenGraphProxyService();
const updater = new ContentElementUpdateVisitor(content, openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);

return { externalToolElement, updater };
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ import {
RichTextContentBody,
SubmissionContainerContentBody,
} from '../controller/dto';
import { OpenGraphProxyService } from './open-graph-proxy.service';

@Injectable()
export class ContentElementUpdateVisitor implements BoardCompositeVisitorAsync {
private readonly content: AnyElementContentBody;

constructor(content: AnyElementContentBody, private readonly openGraphProxyService: OpenGraphProxyService) {
constructor(content: AnyElementContentBody) {
this.content = content;
}

Expand All @@ -55,13 +54,19 @@ export class ContentElementUpdateVisitor implements BoardCompositeVisitorAsync {

async visitLinkElementAsync(linkElement: LinkElement): Promise<void> {
if (this.content instanceof LinkContentBody) {
const urlWithProtocol = /:\/\//.test(this.content.url) ? this.content.url : `https://${this.content.url}`;
linkElement.url = new URL(urlWithProtocol).toString();
const openGraphData = await this.openGraphProxyService.fetchOpenGraphData(linkElement.url);
linkElement.title = openGraphData.title;
linkElement.description = openGraphData.description;
if (openGraphData.image) {
linkElement.imageUrl = openGraphData.image.url;
linkElement.url = new URL(this.content.url).toString();
linkElement.title = this.content.title ?? '';
linkElement.description = this.content.description ?? '';
if (this.content.imageUrl) {
const isRelativeUrl = (url: string) => {
const fallbackHostname = 'https://www.fallback-url-if-url-is-relative.org';
const imageUrlObject = new URL(url, fallbackHostname);
return imageUrlObject.origin === fallbackHostname;
};

if (isRelativeUrl(this.content.imageUrl)) {
linkElement.imageUrl = this.content.imageUrl;
}
}
return Promise.resolve();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,13 @@ import {
import { BoardDoRepo } from '../repo';
import { BoardDoService } from './board-do.service';
import { ContentElementService } from './content-element.service';
import { OpenGraphProxyService } from './open-graph-proxy.service';

describe(ContentElementService.name, () => {
let module: TestingModule;
let service: ContentElementService;
let boardDoRepo: DeepMocked<BoardDoRepo>;
let boardDoService: DeepMocked<BoardDoService>;
let contentElementFactory: DeepMocked<ContentElementFactory>;
let openGraphProxyService: DeepMocked<OpenGraphProxyService>;

beforeAll(async () => {
module = await Test.createTestingModule({
Expand All @@ -52,18 +50,13 @@ describe(ContentElementService.name, () => {
provide: ContentElementFactory,
useValue: createMock<ContentElementFactory>(),
},
{
provide: OpenGraphProxyService,
useValue: createMock<OpenGraphProxyService>(),
},
],
}).compile();

service = module.get(ContentElementService);
boardDoRepo = module.get(BoardDoRepo);
boardDoService = module.get(BoardDoService);
contentElementFactory = module.get(ContentElementFactory);
openGraphProxyService = module.get(OpenGraphProxyService);

await setupEntities();
});
Expand Down Expand Up @@ -302,8 +295,6 @@ describe(ContentElementService.name, () => {
image: { url: 'https://my-open-graph-proxy.scvs.de/image/adefcb12ed3a' },
};

openGraphProxyService.fetchOpenGraphData.mockResolvedValueOnce(imageResponse);

return { linkElement, content, card, imageResponse };
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ import { AnyElementContentBody } from '../controller/dto';
import { BoardDoRepo } from '../repo';
import { BoardDoService } from './board-do.service';
import { ContentElementUpdateVisitor } from './content-element-update.visitor';
import { OpenGraphProxyService } from './open-graph-proxy.service';

@Injectable()
export class ContentElementService {
constructor(
private readonly boardDoRepo: BoardDoRepo,
private readonly boardDoService: BoardDoService,
private readonly contentElementFactory: ContentElementFactory,
private readonly openGraphProxyService: OpenGraphProxyService
private readonly contentElementFactory: ContentElementFactory
) {}

async findById(elementId: EntityId): Promise<AnyContentElementDo> {
Expand Down Expand Up @@ -58,7 +56,7 @@ export class ContentElementService {
}

async update(element: AnyContentElementDo, content: AnyElementContentBody): Promise<AnyContentElementDo> {
const updater = new ContentElementUpdateVisitor(content, this.openGraphProxyService);
const updater = new ContentElementUpdateVisitor(content);
await element.acceptAsync(updater);

const parent = await this.boardDoRepo.findParentOfId(element.id);
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/modules/board/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ export * from './card.service';
export * from './column-board.service';
export * from './column.service';
export * from './content-element.service';
export * from './open-graph-proxy.service';
export * from './submission-item.service';
Loading

0 comments on commit a3fd43f

Please sign in to comment.