From 7f4f92f0e845062a40684283b5982c1bb912dfd5 Mon Sep 17 00:00:00 2001 From: Santiago Rojas <126520289+Santirv17@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:10:13 +0200 Subject: [PATCH] feat: add optional file URL field to packed file responses --- .../item/item.controller.read.test.ts | 57 +++++++++++++++++++ src/services/item/item.service.ts | 12 +++- src/services/item/item.ts | 1 + .../item/plugins/file/itemFile.schema.ts | 1 + 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/services/item/item.controller.read.test.ts b/src/services/item/item.controller.read.test.ts index 26e4204d2..e70e4c68a 100644 --- a/src/services/item/item.controller.read.test.ts +++ b/src/services/item/item.controller.read.test.ts @@ -947,6 +947,63 @@ describe('Item routes tests', () => { data.forEach((i) => expectThumbnails(i, MOCK_SIGNED_URL, false)); }); + it('Returns file URL only for file children', async () => { + const { + actor, + items: [parentItem], + } = await seedFromJson({ + items: [ + { + memberships: [{ account: 'actor', permission: 'admin' }], + children: [ + { + type: 'file', + extra: { + file: { + name: 'file.pdf', + path: 'files/file.pdf', + mimetype: 'application/pdf', + size: 123, + content: '', + }, + }, + }, + { + type: 'document', + extra: { + document: { + content: 'Some document content', + }, + }, + }, + {}, + ], + }, + ], + }); + assertIsDefined(actor); + assertIsMemberForTest(actor); + mockAuthenticate(actor); + + const response = await app.inject({ + method: HttpMethod.Get, + url: `/api/items/${parentItem.id}/children`, + }); + + const data = response.json(); + expect(response.statusCode).toBe(StatusCodes.OK); + + const fileChild = data.find( + (item): item is Extract => item.type === 'file', + ); + const documentChild = data.find((item) => item.type === 'document'); + const folderChild = data.find((item) => item.type === 'folder'); + + expect(fileChild?.extra.file.url).toEqual(MOCK_SIGNED_URL); + expect(documentChild?.extra).not.toHaveProperty('file'); + expect(folderChild?.extra).not.toHaveProperty('file'); + }); + it('Returns a child h5p successfully', async () => { const { actor, diff --git a/src/services/item/item.service.ts b/src/services/item/item.service.ts index cbd1d8104..af15250f7 100644 --- a/src/services/item/item.service.ts +++ b/src/services/item/item.service.ts @@ -18,6 +18,7 @@ import { getParentFromPath, } from '@graasp/sdk'; +import { resolveDependency } from '../../di/utils'; import { type DBConnection } from '../../drizzle/db'; import { type ItemGeolocationRaw, @@ -45,6 +46,7 @@ import { filterOutPackedItems, } from '../authorization.utils'; import { AuthorizedItemService } from '../authorizedItem.service'; +import FileService from '../file/file.service'; import { ItemMembershipRepository } from '../itemMembership/membership.repository'; import { ThumbnailService } from '../thumbnail/thumbnail.service'; import { DEFAULT_ORDER, IS_COPY_REGEX, MAX_COPY_SUFFIX_LENGTH } from './constants'; @@ -540,7 +542,7 @@ export class ItemService { children, thumbnails, ); - return filteredChildren.map((children) => this.transformItemByType(children)); + return Promise.all(filteredChildren.map((child) => this.transformItemByType(child))); } async getDescendants( @@ -1050,7 +1052,8 @@ export class ItemService { } } - private transformItemByType(item: PackedItem) { + // Add response-only data that depends on the item type + private async transformItemByType(item: PackedItem): Promise { switch (item.type) { case 'h5p': { const { h5p: h5pExtraProperties } = item.extra as H5PItemExtra; @@ -1065,6 +1068,11 @@ export class ItemService { }; return { ...item, extra: newExtra }; } + case 'file': { + const fileService = resolveDependency(FileService); + const url = await fileService.getUrl({ path: item.extra.file.path }); + return { ...item, extra: { ...item.extra, file: { ...item.extra.file, url } } }; + } default: return item; } diff --git a/src/services/item/item.ts b/src/services/item/item.ts index 7caef14a9..7282f8e78 100644 --- a/src/services/item/item.ts +++ b/src/services/item/item.ts @@ -54,6 +54,7 @@ export type FileItem = Omit & { path: string; mimetype: string; size: number; + url?: string; altText?: string; content?: string; /** @deprecated */ diff --git a/src/services/item/plugins/file/itemFile.schema.ts b/src/services/item/plugins/file/itemFile.schema.ts index 8609390be..ffb54edef 100644 --- a/src/services/item/plugins/file/itemFile.schema.ts +++ b/src/services/item/plugins/file/itemFile.schema.ts @@ -16,6 +16,7 @@ const fileItemSchema = Type.Composite( path: Type.String(), mimetype: Type.String(), size: Type.Integer({ minimum: 0 }), + url: Type.Optional(Type.String()), altText: Type.Optional( Type.String({ description: 'alternative text of the file if it is an image',