From 7e5884a82f41fc95ae5ce62617227f849e37c5ec Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Tue, 19 Nov 2024 12:29:56 +0100 Subject: [PATCH] fix: content vs element --- api/src/channel/lib/__test__/common.mock.ts | 2 - api/src/chat/schemas/types/message.ts | 8 ++- api/src/chat/services/block.service.spec.ts | 8 +-- api/src/cms/schemas/content.schema.ts | 25 +++++--- api/src/cms/services/content.service.spec.ts | 49 +++++++-------- api/src/cms/services/content.service.ts | 62 +++++++++---------- .../channels/web/base-web-channel.ts | 6 +- 7 files changed, 79 insertions(+), 81 deletions(-) diff --git a/api/src/channel/lib/__test__/common.mock.ts b/api/src/channel/lib/__test__/common.mock.ts index 9acc3c37..0794fa92 100644 --- a/api/src/channel/lib/__test__/common.mock.ts +++ b/api/src/channel/lib/__test__/common.mock.ts @@ -120,7 +120,6 @@ export const contentMessage: StdOutgoingListMessage = { id: '1', entity: 'rank', title: 'First', - // @ts-expect-error Necessary workaround desc: 'About being first', thumbnail: { payload: attachmentWithUrl, @@ -136,7 +135,6 @@ export const contentMessage: StdOutgoingListMessage = { id: '2', entity: 'rank', title: 'Second', - // @ts-expect-error Necessary workaround desc: 'About being second', thumbnail: { payload: attachmentWithUrl, diff --git a/api/src/chat/schemas/types/message.ts b/api/src/chat/schemas/types/message.ts index 8d1edbcd..5a49a4ba 100644 --- a/api/src/chat/schemas/types/message.ts +++ b/api/src/chat/schemas/types/message.ts @@ -7,7 +7,6 @@ */ import { Attachment } from '@/attachment/schemas/attachment.schema'; -import { Content } from '@/cms/schemas/content.schema'; import { Message } from '../message.schema'; @@ -84,9 +83,14 @@ export type StdOutgoingButtonsMessage = { buttons: Button[]; }; +export type ContentElement = { id: string; title: string } & Record< + string, + any +>; + export type StdOutgoingListMessage = { options: ContentOptions; - elements: Content[]; + elements: ContentElement[]; pagination: { total: number; skip: number; diff --git a/api/src/chat/services/block.service.spec.ts b/api/src/chat/services/block.service.spec.ts index 066e6fee..9d66bdfc 100644 --- a/api/src/chat/services/block.service.spec.ts +++ b/api/src/chat/services/block.service.spec.ts @@ -442,9 +442,7 @@ describe('BlockService', () => { { status: true, entity: contentType.id }, { skip: 0, limit: 2, sort: ['createdAt', 'desc'] }, ); - const flattenedElements = elements.map((element) => - Content.flatDynamicFields(element), - ); + const flattenedElements = elements.map(Content.toElement); expect(result.format).toEqualPayload( blockProductListMock.options.content?.display, ); @@ -476,9 +474,7 @@ describe('BlockService', () => { { status: true, entity: contentType.id }, { skip: 2, limit: 2, sort: ['createdAt', 'desc'] }, ); - const flattenedElements = elements.map((element) => - Content.flatDynamicFields(element), - ); + const flattenedElements = elements.map(Content.toElement); expect(result.format).toEqual( blockProductListMock.options.content?.display, ); diff --git a/api/src/cms/schemas/content.schema.ts b/api/src/cms/schemas/content.schema.ts index c06594df..173f84b5 100644 --- a/api/src/cms/schemas/content.schema.ts +++ b/api/src/cms/schemas/content.schema.ts @@ -6,10 +6,11 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose'; +import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Transform, Type } from 'class-transformer'; import mongoose, { Document } from 'mongoose'; +import { ContentElement } from '@/chat/schemas/types/message'; import { config } from '@/config'; import { BaseSchema } from '@/utils/generics/base-schema'; import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager'; @@ -51,14 +52,14 @@ export class ContentStub extends BaseSchema { /** * Helper to return the internal url of this content. */ - static getUrl(item: Content): string { + static getUrl(item: ContentElement): string { return new URL('/content/view/' + item.id, config.apiPath).toString(); } /** * Helper that returns the relative chatbot payload for this content. */ - static getPayload(item: Content): string { + static getPayload(item: ContentElement): string { return 'postback' in item ? (item.postback as string) : item.title; } } @@ -68,12 +69,18 @@ export class Content extends ContentStub { @Transform(({ obj }) => obj.entity.toString()) entity: string; - static flatDynamicFields(element: Content) { - Object.entries(element.dynamicFields).forEach(([key, value]) => { - element[key] = value; - }); - element.dynamicFields = undefined; - return element; + /** + * Converts a content object to an element (A flat representation of a content) + * + * @param content + * @returns An object that has all dynamic fields accessible at top level + */ + static toElement(content: Content): ContentElement { + return { + id: content.id, + title: content.title, + ...content.dynamicFields, + }; } } diff --git a/api/src/cms/services/content.service.spec.ts b/api/src/cms/services/content.service.spec.ts index 3844bedd..94a056b3 100644 --- a/api/src/cms/services/content.service.spec.ts +++ b/api/src/cms/services/content.service.spec.ts @@ -14,7 +14,10 @@ import { AttachmentRepository } from '@/attachment/repositories/attachment.repos import { AttachmentModel } from '@/attachment/schemas/attachment.schema'; import { AttachmentService } from '@/attachment/services/attachment.service'; import { FileType } from '@/chat/schemas/types/attachment'; -import { OutgoingMessageFormat } from '@/chat/schemas/types/message'; +import { + ContentElement, + OutgoingMessageFormat, +} from '@/chat/schemas/types/message'; import { ContentOptions } from '@/chat/schemas/types/options'; import { LoggerService } from '@/logger/logger.service'; import { IGNORED_TEST_FIELDS } from '@/utils/test/constants'; @@ -159,7 +162,10 @@ describe('ContentService', () => { }, ]; it('should return all content attachment ids', () => { - const result = contentService.getAttachmentIds(contents, 'image'); + const result = contentService.getAttachmentIds( + contents.map(Content.toElement), + 'image', + ); expect(result).toEqual(['123', '456']); }); @@ -172,22 +178,19 @@ describe('ContentService', () => { describe('populateAttachments', () => { it('should return populated content', async () => { const storeContents = await contentService.find({ title: /^store/ }); - const populatedStoreContents: Content[] = await Promise.all( - storeContents.map(async (store) => { - const attachmentId = store.dynamicFields.image.payload.attachment_id; + const elements: ContentElement[] = await Promise.all( + storeContents.map(Content.toElement).map(async (store) => { + const attachmentId = store.image.payload.attachment_id; if (attachmentId) { const attachment = await attachmentService.findOne(attachmentId); if (attachment) { return { ...store, - dynamicFields: { - ...store.dynamicFields, - image: { - type: 'image', - payload: { - ...attachment, - url: `http://localhost:4000/attachment/download/${attachment.id}/${attachment.name}`, - }, + image: { + type: 'image', + payload: { + ...attachment, + url: `http://localhost:4000/attachment/download/${attachment.id}/${attachment.name}`, }, }, }; @@ -197,10 +200,10 @@ describe('ContentService', () => { }), ); const result = await contentService.populateAttachments( - storeContents, + storeContents.map(Content.toElement), 'image', ); - expect(result).toEqualPayload(populatedStoreContents); + expect(result).toEqualPayload(elements); }); }); @@ -221,9 +224,7 @@ describe('ContentService', () => { { status: true }, { skip: 0, limit: 10, sort: ['createdAt', 'desc'] }, ); - const flattenedElements = actualData.map((content) => - Content.flatDynamicFields(content), - ); + const flattenedElements = actualData.map(Content.toElement); const content = await contentService.getContent(contentOptions, 0); expect(content?.elements).toEqualPayload(flattenedElements, [ ...IGNORED_TEST_FIELDS, @@ -237,9 +238,7 @@ describe('ContentService', () => { { status: true, entity: contentType.id }, { skip: 0, limit: 10, sort: ['createdAt', 'desc'] }, ); - const flattenedElements = actualData.map((content) => - Content.flatDynamicFields(content), - ); + const flattenedElements = actualData.map(Content.toElement); const content = await contentService.getContent( { ...contentOptions, @@ -257,9 +256,7 @@ describe('ContentService', () => { { status: true, entity: contentType.id, title: /^Jean/ }, { skip: 0, limit: 10, sort: ['createdAt', 'desc'] }, ); - const flattenedElements = actualData.map((content) => - Content.flatDynamicFields(content), - ); + const flattenedElements = actualData.map(Content.toElement); const content = await contentService.getContent( { ...contentOptions, @@ -276,9 +273,7 @@ describe('ContentService', () => { { status: true }, { skip: 2, limit: 2, sort: ['createdAt', 'desc'] }, ); - const flattenedElements = actualData.map((content) => - Content.flatDynamicFields(content), - ); + const flattenedElements = actualData.map(Content.toElement); const content = await contentService.getContent( { ...contentOptions, diff --git a/api/src/cms/services/content.service.ts b/api/src/cms/services/content.service.ts index 331736b4..bdcac780 100644 --- a/api/src/cms/services/content.service.ts +++ b/api/src/cms/services/content.service.ts @@ -11,7 +11,10 @@ import { Injectable } from '@nestjs/common'; import { Attachment } from '@/attachment/schemas/attachment.schema'; import { AttachmentService } from '@/attachment/services/attachment.service'; import { WithUrl } from '@/chat/schemas/types/attachment'; -import { StdOutgoingListMessage } from '@/chat/schemas/types/message'; +import { + ContentElement, + StdOutgoingListMessage, +} from '@/chat/schemas/types/message'; import { ContentOptions } from '@/chat/schemas/types/options'; import { LoggerService } from '@/logger/logger.service'; import { BaseService } from '@/utils/generics/base-service'; @@ -57,10 +60,10 @@ export class ContentService extends BaseService< * * @return A list of attachment IDs. */ - getAttachmentIds(contents: Content[], attachmentFieldName: string) { + getAttachmentIds(contents: ContentElement[], attachmentFieldName: string) { return contents.reduce((acc, content) => { - if (attachmentFieldName in content.dynamicFields) { - const attachment = content.dynamicFields[attachmentFieldName]; + if (attachmentFieldName in content) { + const attachment = content[attachmentFieldName]; if ( typeof attachment === 'object' && @@ -84,16 +87,16 @@ export class ContentService extends BaseService< /** * Populates attachment fields within content entities with detailed attachment information. * - * @param contents - An array of content entities. + * @param elements - An array of content entities. * @param attachmentFieldName - The name of the attachment field to populate. * * @return A list of content with populated attachment data. */ async populateAttachments( - contents: Content[], + elements: ContentElement[], attachmentFieldName: string, - ): Promise { - const attachmentIds = this.getAttachmentIds(contents, attachmentFieldName); + ): Promise { + const attachmentIds = this.getAttachmentIds(elements, attachmentFieldName); if (attachmentIds.length > 0) { const attachments = await this.attachmentService.find({ @@ -107,8 +110,8 @@ export class ContentService extends BaseService< }, {} as { [key: string]: WithUrl }, ); - const populatedContents = contents.map((content) => { - const attachmentField = content.dynamicFields[attachmentFieldName]; + const populatedContents = elements.map((content) => { + const attachmentField = content[attachmentFieldName]; if ( typeof attachmentField === 'object' && 'attachment_id' in attachmentField.payload @@ -116,17 +119,14 @@ export class ContentService extends BaseService< const attachmentId = attachmentField?.payload?.attachment_id; return { ...content, - dynamicFields: { - ...content.dynamicFields, - [attachmentFieldName]: { - type: attachmentField.type, - payload: { - ...(attachmentsById[attachmentId] || attachmentField.payload), - url: Attachment.getAttachmentUrl( - attachmentId, - attachmentsById[attachmentId].name, - ), - }, + [attachmentFieldName]: { + type: attachmentField.type, + payload: { + ...(attachmentsById[attachmentId] || attachmentField.payload), + url: Attachment.getAttachmentUrl( + attachmentId, + attachmentsById[attachmentId].name, + ), }, }, }; @@ -136,7 +136,7 @@ export class ContentService extends BaseService< }); return populatedContents; } - return contents; + return elements; } /** @@ -175,21 +175,15 @@ export class ContentService extends BaseService< limit, sort: ['createdAt', 'desc'], }); + const elements = contents.map(Content.toElement); const attachmentFieldName = options.fields.image_url; if (attachmentFieldName) { // Populate attachment when there's an image field - const populatedContents = await this.populateAttachments( - contents, - attachmentFieldName, - ); - const flatContent = populatedContents.map((content) => ({ - ...content, - ...content.dynamicFields, - dynamicFields: undefined, - })); - return { - elements: flatContent, + elements: await this.populateAttachments( + elements, + attachmentFieldName, + ), pagination: { total, skip, @@ -198,7 +192,7 @@ export class ContentService extends BaseService< }; } return { - elements: contents, + elements, pagination: { total, skip, diff --git a/api/src/extensions/channels/web/base-web-channel.ts b/api/src/extensions/channels/web/base-web-channel.ts index cb8907a6..b1544ca3 100644 --- a/api/src/extensions/channels/web/base-web-channel.ts +++ b/api/src/extensions/channels/web/base-web-channel.ts @@ -31,6 +31,7 @@ import { WithUrl } from '@/chat/schemas/types/attachment'; import { Button, ButtonType } from '@/chat/schemas/types/button'; import { AnyMessage, + ContentElement, FileType, IncomingMessage, OutgoingMessage, @@ -963,7 +964,10 @@ export default abstract class BaseWebChannelHandler< * * @returns An array of elements object */ - _formatElements(data: any[], options: BlockOptions): Web.MessageElement[] { + _formatElements( + data: ContentElement[], + options: BlockOptions, + ): Web.MessageElement[] { if (!options.content || !options.content.fields) { throw new Error('Content options are missing the fields'); }