From 4821dcc085040d002b565198d915dc5e90c9baaa Mon Sep 17 00:00:00 2001 From: Sebastian Mahr Date: Thu, 13 Feb 2025 10:17:06 +0100 Subject: [PATCH] Enrich metadata of File with tags (#419) * feat: add tags to files * fix: tests * test: add test to load file with tags * fix: add tags to FileDVO * fix: add tags to getFilesQuery * test: add test for custom query * chore: make tags optional in DTO and DVO --- .../src/dataViews/transport/FileDVO.ts | 1 + .../runtime/src/types/transport/FileDTO.ts | 1 + .../runtime/src/useCases/common/Schemas.ts | 25 +++++++++ .../useCases/transport/files/FileMapper.ts | 1 + .../src/useCases/transport/files/GetFiles.ts | 18 +++++++ .../useCases/transport/files/UploadOwnFile.ts | 4 +- packages/runtime/test/transport/files.test.ts | 52 +++++++++++++++++++ .../src/modules/files/FileController.ts | 4 +- .../src/modules/files/local/CachedFile.ts | 6 +++ .../modules/files/local/SendFileParameters.ts | 5 ++ .../files/transmission/FileMetadata.ts | 5 ++ .../test/modules/files/FileController.test.ts | 1 + .../transport/test/testHelpers/TestUtil.ts | 3 +- 13 files changed, 123 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/dataViews/transport/FileDVO.ts b/packages/runtime/src/dataViews/transport/FileDVO.ts index 15c5f6722..40e4fe32d 100644 --- a/packages/runtime/src/dataViews/transport/FileDVO.ts +++ b/packages/runtime/src/dataViews/transport/FileDVO.ts @@ -4,6 +4,7 @@ import { IdentityDVO } from "./IdentityDVO"; export interface FileDVO extends DataViewObject { type: "FileDVO"; filename: string; + tags?: string[]; filesize: number; createdAt: string; createdBy: IdentityDVO; diff --git a/packages/runtime/src/types/transport/FileDTO.ts b/packages/runtime/src/types/transport/FileDTO.ts index 7006b6498..448f70e27 100644 --- a/packages/runtime/src/types/transport/FileDTO.ts +++ b/packages/runtime/src/types/transport/FileDTO.ts @@ -1,6 +1,7 @@ export interface FileDTO { id: string; filename: string; + tags?: string[]; filesize: number; createdAt: string; createdBy: string; diff --git a/packages/runtime/src/useCases/common/Schemas.ts b/packages/runtime/src/useCases/common/Schemas.ts index 95416be78..13a4d977e 100644 --- a/packages/runtime/src/useCases/common/Schemas.ts +++ b/packages/runtime/src/useCases/common/Schemas.ts @@ -20436,6 +20436,19 @@ export const GetFilesRequest: any = { } } ] + }, + "tags": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] } }, "additionalProperties": false @@ -20548,6 +20561,12 @@ export const UploadOwnFileRequest: any = { }, "description": { "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ @@ -20587,6 +20606,12 @@ export const UploadOwnFileValidatableRequest: any = { "description": { "type": "string" }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, "content": { "type": "object" } diff --git a/packages/runtime/src/useCases/transport/files/FileMapper.ts b/packages/runtime/src/useCases/transport/files/FileMapper.ts index 463b6778d..5132f3150 100644 --- a/packages/runtime/src/useCases/transport/files/FileMapper.ts +++ b/packages/runtime/src/useCases/transport/files/FileMapper.ts @@ -24,6 +24,7 @@ export class FileMapper { return { id: file.id.toString(), filename: file.cache.filename, + tags: file.cache.tags, filesize: file.cache.filesize, createdAt: file.cache.createdAt.toString(), createdBy: file.cache.createdBy.toString(), diff --git a/packages/runtime/src/useCases/transport/files/GetFiles.ts b/packages/runtime/src/useCases/transport/files/GetFiles.ts index 8b15c6129..ff4d86ba8 100644 --- a/packages/runtime/src/useCases/transport/files/GetFiles.ts +++ b/packages/runtime/src/useCases/transport/files/GetFiles.ts @@ -18,6 +18,7 @@ export interface GetFilesQuery { mimetype?: string | string[]; title?: string | string[]; isOwn?: string | string[]; + tags?: string | string[]; } export interface GetFilesRequest { @@ -43,6 +44,7 @@ export class GetFilesUseCase extends UseCase { [nameof((c) => c.filesize)]: true, [nameof((c) => c.mimetype)]: true, [nameof((c) => c.title)]: true, + [nameof((c) => c.tags)]: true, [nameof((c) => c.isOwn)]: true }, alias: { @@ -55,7 +57,23 @@ export class GetFilesUseCase extends UseCase { [nameof((c) => c.filesize)]: `${nameof((f) => f.cache)}.${nameof((c) => c.filesize)}`, [nameof((c) => c.mimetype)]: `${nameof((f) => f.cache)}.${nameof((c) => c.mimetype)}`, [nameof((c) => c.title)]: `${nameof((f) => f.cache)}.${nameof((c) => c.title)}`, + [nameof((c) => c.tags)]: `${nameof((f) => f.cache)}.${nameof((c) => c.tags)}`, [nameof((c) => c.isOwn)]: nameof((f) => f.isOwn) + }, + custom: { + // content.tags + [`${nameof((x) => x.tags)}`]: (query: any, input: string | string[]) => { + if (typeof input === "string") { + query[`${nameof((x) => x.tags)}`] = { $contains: input }; + return; + } + const allowedTags = []; + for (const tag of input) { + const tagQuery = { [`${nameof((x) => x.tags)}`]: { $contains: tag } }; + allowedTags.push(tagQuery); + } + query["$or"] = allowedTags; + } } }); diff --git a/packages/runtime/src/useCases/transport/files/UploadOwnFile.ts b/packages/runtime/src/useCases/transport/files/UploadOwnFile.ts index da3ebc3f7..18eb91b23 100644 --- a/packages/runtime/src/useCases/transport/files/UploadOwnFile.ts +++ b/packages/runtime/src/useCases/transport/files/UploadOwnFile.ts @@ -15,6 +15,7 @@ export interface UploadOwnFileRequest { expiresAt?: ISO8601DateTimeString; title?: string; description?: string; + tags?: string[]; } export interface UploadOwnFileValidatableRequest extends Omit { @@ -74,7 +75,8 @@ export class UploadOwnFileUseCase extends UseCase description: request.description, filename: request.filename, mimetype: request.mimetype, - expiresAt: CoreDate.from(request.expiresAt ?? "9999-12-31T00:00:00.000Z") + expiresAt: CoreDate.from(request.expiresAt ?? "9999-12-31T00:00:00.000Z"), + tags: request.tags }); await this.accountController.syncDatawallet(); diff --git a/packages/runtime/test/transport/files.test.ts b/packages/runtime/test/transport/files.test.ts index 6d8fa60ab..769d19566 100644 --- a/packages/runtime/test/transport/files.test.ts +++ b/packages/runtime/test/transport/files.test.ts @@ -112,6 +112,13 @@ describe("File upload", () => { expect(CoreDate.from(file.expiresAt).isSame(defaultDate)).toBe(true); }); + test("can upload a file with tags", async () => { + const response = await transportServices1.files.uploadOwnFile(await makeUploadRequest({ tags: ["tag1", "tag2"] })); + expect(response).toBeSuccessful(); + + expect(response.value.tags).toStrictEqual(["tag1", "tag2"]); + }); + test("cannot upload a file with expiry date in the past", async () => { const response = await transportServices1.files.uploadOwnFile(await makeUploadRequest({ expiresAt: "1970" })); expect(response).toBeAnError("'expiresAt' must be in the future", "error.runtime.validation.invalidPropertyValue"); @@ -136,6 +143,39 @@ describe("Get file", () => { expect(response.value).toMatchObject(file); }); + test("can get file by tags", async () => { + const uploadFileResult = await transportServices1.files.uploadOwnFile( + await makeUploadRequest({ + tags: ["aTag", "anotherTag"] + }) + ); + const file = uploadFileResult.value; + + const uploadFileResult2 = await transportServices1.files.uploadOwnFile( + await makeUploadRequest({ + tags: ["aThirdTag", "aFourthTag"] + }) + ); + const file2 = uploadFileResult2.value; + + const getResult = await transportServices1.files.getFiles({ query: { tags: ["aTag"] } }); + + expect(getResult).toBeSuccessful(); + expect(getResult.value[0].id).toStrictEqual(file.id); + + const getResult2 = await transportServices1.files.getFiles({ query: { tags: ["aTag", "anotherTag"] } }); + + expect(getResult2).toBeSuccessful(); + expect(getResult2.value[0].id).toStrictEqual(file.id); + + const getResult3 = await transportServices1.files.getFiles({ query: { tags: ["aTag", "aThirdTag"] } }); + + expect(getResult3).toBeSuccessful(); + const result3Ids = getResult3.value.map((file) => file.id); + expect(result3Ids).toContain(file.id); + expect(result3Ids).toContain(file2.id); + }); + test("accessing not existing file id causes an error", async () => { const response = await transportServices1.files.getFile({ id: UNKNOWN_FILE_ID }); expect(response).toBeAnError("File not found. Make sure the ID exists and the record is not expired.", "error.runtime.recordNotFound"); @@ -295,6 +335,18 @@ describe("Load peer file with token reference", () => { expect(response.value).toContainEqual({ ...file, isOwn: false }); }); + test("should load a peer file with its tags", async () => { + const uploadOwnFileResult = await transportServices1.files.uploadOwnFile( + await makeUploadRequest({ + tags: ["tag1", "tag2"] + }) + ); + const token = (await transportServices1.files.createTokenForFile({ fileId: uploadOwnFileResult.value.id })).value; + const loadFileResult = await transportServices2.files.getOrLoadFile({ reference: token.truncatedReference }); + + expect(loadFileResult.value.tags).toStrictEqual(["tag1", "tag2"]); + }); + test("cannot pass token id as truncated token reference", async () => { const file = await uploadFile(transportServices1); const token = (await transportServices1.files.createTokenForFile({ fileId: file.id })).value; diff --git a/packages/transport/src/modules/files/FileController.ts b/packages/transport/src/modules/files/FileController.ts index 3691e1d99..394858ae0 100644 --- a/packages/transport/src/modules/files/FileController.ts +++ b/packages/transport/src/modules/files/FileController.ts @@ -192,7 +192,8 @@ export class FileController extends TransportController { plaintextHash: plaintextHash, secretKey: fileDownloadSecretKey, filemodified: input.filemodified, - mimetype: input.mimetype + mimetype: input.mimetype, + tags: input.tags }); const serializedMetadata = metadata.serialize(); @@ -218,6 +219,7 @@ export class FileController extends TransportController { title: input.title, description: input.description, filename: input.filename, + tags: input.tags, filesize: fileSize, filemodified: input.filemodified, cipherKey: fileDownloadSecretKey, diff --git a/packages/transport/src/modules/files/local/CachedFile.ts b/packages/transport/src/modules/files/local/CachedFile.ts index 6090fe589..d66024c68 100644 --- a/packages/transport/src/modules/files/local/CachedFile.ts +++ b/packages/transport/src/modules/files/local/CachedFile.ts @@ -8,6 +8,7 @@ import { FileMetadata } from "../transmission/FileMetadata"; export interface ICachedFile extends ISerializable { title?: string; filename: string; + tags?: string[]; filesize: number; filemodified?: CoreDate; mimetype: string; @@ -37,6 +38,10 @@ export class CachedFile extends Serializable implements ICachedFile { @serialize() public filename: string; + @validate({ nullable: true }) + @serialize({ type: String }) + public tags?: string[]; + @validate() @serialize() public filesize: number; @@ -112,6 +117,7 @@ export class CachedFile extends Serializable implements ICachedFile { cipherKey: metadata.secretKey, filemodified: metadata.filemodified, filename: metadata.filename, + tags: metadata.tags, filesize: metadata.filesize, plaintextHash: metadata.plaintextHash, deletedAt: backboneResponse.deletedAt ? CoreDate.from(backboneResponse.deletedAt) : undefined, diff --git a/packages/transport/src/modules/files/local/SendFileParameters.ts b/packages/transport/src/modules/files/local/SendFileParameters.ts index b4fbade67..9547e9e4f 100644 --- a/packages/transport/src/modules/files/local/SendFileParameters.ts +++ b/packages/transport/src/modules/files/local/SendFileParameters.ts @@ -10,6 +10,7 @@ export interface ISendFileParameters extends ISerializable { expiresAt: ICoreDate; filemodified?: ICoreDate; buffer: ICoreBuffer; + tags?: string[]; } @type("SendFileParameters") @@ -42,6 +43,10 @@ export class SendFileParameters extends Serializable implements ISendFileParamet @serialize() public buffer: CoreBuffer; + @validate({ nullable: true }) + @serialize({ type: String }) + public tags?: string[]; + public static from(value: ISendFileParameters): SendFileParameters { return this.fromAny(value); } diff --git a/packages/transport/src/modules/files/transmission/FileMetadata.ts b/packages/transport/src/modules/files/transmission/FileMetadata.ts index d215cbccf..3a764a9ca 100644 --- a/packages/transport/src/modules/files/transmission/FileMetadata.ts +++ b/packages/transport/src/modules/files/transmission/FileMetadata.ts @@ -7,6 +7,7 @@ export interface IFileMetadata extends ISerializable { title?: string; description?: string; filename: string; + tags?: string[]; plaintextHash: ICoreHash; secretKey: ICryptoSecretKey; @@ -30,6 +31,10 @@ export class FileMetadata extends Serializable implements IFileMetadata { @serialize() public filename: string; + @validate({ nullable: true }) + @serialize({ type: String }) + public tags?: string[]; + @validate() @serialize() public plaintextHash: CoreHash; diff --git a/packages/transport/test/modules/files/FileController.test.ts b/packages/transport/test/modules/files/FileController.test.ts index ab47eb89e..a49fe0506 100644 --- a/packages/transport/test/modules/files/FileController.test.ts +++ b/packages/transport/test/modules/files/FileController.test.ts @@ -29,6 +29,7 @@ describe("FileController", function () { expect(receivedFile.cache?.expiresAt).toStrictEqual(sentFile.cache!.expiresAt); expect(sentFile.cache!.description).toBe(receivedFile.cache!.description); expect(sentFile.cache!.title).toBe(receivedFile.cache!.title); + expect(sentFile.cache!.tags).toStrictEqual(receivedFile.cache!.tags); expect(sentFile.cache!.filemodified?.toString()).toBe(receivedFile.cache!.filemodified?.toString()); expect(sentFile.cache!.filename).toBe(receivedFile.cache!.filename); expect(sentFile.cache!.filesize).toBe(receivedFile.cache!.filesize); diff --git a/packages/transport/test/testHelpers/TestUtil.ts b/packages/transport/test/testHelpers/TestUtil.ts index 23f9c988b..bf94bfabc 100644 --- a/packages/transport/test/testHelpers/TestUtil.ts +++ b/packages/transport/test/testHelpers/TestUtil.ts @@ -638,7 +638,8 @@ export class TestUtil { filename: "Test.bin", filemodified: CoreDate.from("2019-09-30T00:00:00.000Z"), mimetype: "application/json", - expiresAt: CoreDate.utc().add({ minutes: 5 }) + expiresAt: CoreDate.utc().add({ minutes: 5 }), + tags: ["tag1", "tag2"] }; const file = await from.files.sendFile(params);