Skip to content

Commit

Permalink
Merge pull request #1952 from openkfw/1900-docs-metadata
Browse files Browse the repository at this point in the history
#1900 Additional metadata in WF documents
  • Loading branch information
MartinJurcoGlina authored Sep 20, 2024
2 parents 226b89d + 041f910 commit 97ce29d
Show file tree
Hide file tree
Showing 38 changed files with 587 additions and 213 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ yarn-error.log
.vs/
e2e-test/debug.log
scripts/development/.env
scripts/development/certs/*
scripts/development/certs
scripts/operation/.env
.idea/
docs/developer/api-docs
Expand Down
17 changes: 13 additions & 4 deletions api/src/service/Client_storage_service.h.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Result from "../result";
import { File } from "./domain/document/document_upload";

type Base64String = string;

Expand All @@ -12,6 +13,7 @@ export interface StorageObject {
id: string;
fileName: string;
base64: Base64String;
lastModified?: string;
}

export interface DeleteResponse {
Expand All @@ -33,14 +35,21 @@ export interface StorageServiceClientI {
*
*/
getVersion(): Promise<Version>;
/**
* @typedef {Object} File
* @property {string} id - The unique identifier for the file.
* @property {string} fileName - The name of the file.
* @property {string} documentBase64 - The base64 encoded content of the file.
* @property {string} [comment] - An optional comment about the file.
*/

/**
* Upload an object using the
*
* @param id id of object
* @param name name of object
* @param data content of uploaded object base64 encoded
* @param {File} file - File object containing id, fileName, documentBase64, and an optional comment.
* @returns {Promise<Result.Type<UploadResponse>>} - A promise that resolves to the upload response.
*/
uploadObject(id: string, name: string, data: Base64String): Promise<Result.Type<UploadResponse>>;
uploadObject(file: File): Promise<Result.Type<UploadResponse>>;
/**
* Download an object using the matching secret
*
Expand Down
25 changes: 14 additions & 11 deletions api/src/service/Client_storage_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import {
UploadResponse,
Version,
} from "./Client_storage_service.h";
import { File } from "./domain/document/document_upload";

interface UploadRequest {
fileName: string;
content: string;
comment?: string;
}

export default class StorageServiceClient implements StorageServiceClientI {
Expand Down Expand Up @@ -66,28 +68,28 @@ export default class StorageServiceClient implements StorageServiceClientI {
return result?.data as Version;
}

public async uploadObject(
id: string,
name: string,
data: string,
): Promise<Result.Type<UploadResponse>> {
logger.debug(`Uploading Object "${name}"`);
public async uploadObject(file: File): Promise<Result.Type<UploadResponse>> {
logger.debug(`Uploading Object "${file.fileName}"`);

let requestData: UploadRequest = {
fileName: encodeURIComponent(name),
content: data,
fileName: encodeURIComponent(file.fileName || ""),
content: file.documentBase64,
comment: encodeURIComponent(file.comment || ""),
};
if (config.encryptionPassword) {
requestData.fileName = encrypt(config.encryptionPassword, requestData.fileName);
requestData.content = encrypt(config.encryptionPassword, requestData.content);
requestData.comment = requestData.comment
? encrypt(config.encryptionPassword, requestData.comment)
: undefined;
}
const url = `/upload?docId=${id}`;
const url = `/upload?docId=${file.id}`;
const uploadResponse = await this.axiosInstance.post(url, requestData);
if (Result.isErr(uploadResponse)) {
logger.error(`Error while uploading document ${id} to storage service.`);
logger.error(`Error while uploading document ${file.id} to storage service.`);
return new VError(uploadResponse, "Uploading the object failed");
} else if (uploadResponse.status !== 200) {
logger.error(`Error while uploading document ${id} to storage service.`);
logger.error(`Error while uploading document ${file.id} to storage service.`);
return new VError("Uploading the object failed");
}
return uploadResponse.data;
Expand Down Expand Up @@ -115,6 +117,7 @@ export default class StorageServiceClient implements StorageServiceClientI {
id: downloadResponse.data.meta.docid,
fileName: decodeURIComponent(downloadResponse.data.meta.filename),
base64: downloadResponse.data.data,
lastModified: downloadResponse.data.meta.lastModified,
};

if (config.encryptionPassword) {
Expand Down
4 changes: 2 additions & 2 deletions api/src/service/document_upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export async function documentUpload(
getPublicKey: async (organization) => {
return PublicKeyGet.getPublicKey(conn, ctx, organization);
},
storeDocument: async (id, name, hash) => {
return storageServiceClient.uploadObject(id, name, hash);
storeDocument: async (file: DocumentUpload.File) => {
return storageServiceClient.uploadObject(file);
},
encryptWithKey: async (secret, publicKey) => {
return encryptWithKey(secret, publicKey);
Expand Down
16 changes: 15 additions & 1 deletion api/src/service/domain/document/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export interface DocumentReference {
fileName: string;
hash: string;
available?: boolean;
comment?: string;
lastModified?: string;
}

export interface DeleteDocumentResponse {
Expand All @@ -42,13 +44,16 @@ export interface ExternalLinkReference {
link: string;
available?: boolean;
linkedFileHash?: string;
lastModified?: string;
comment?: string;
}

export type DocumentOrExternalLinkReference = DocumentReference | ExternalLinkReference;

export type DocumentWithAvailability = DocumentOrExternalLinkReference & {
isValidHash?: boolean;
message?: string;
comment?: string;
lastModified?: string;
};

export const documentReferenceSchema = Joi.alternatives([
Expand All @@ -57,20 +62,26 @@ export const documentReferenceSchema = Joi.alternatives([
fileName: Joi.string().required(),
hash: Joi.string().required(),
available: Joi.boolean(),
comment: Joi.string().optional().allow(""),
lastModified: Joi.date().iso().optional(),
}),
Joi.object({
id: Joi.string().required(),
fileName: Joi.string().required(),
link: Joi.string().required(),
linkedFileHash: Joi.string(),
available: Joi.boolean(),
comment: Joi.string().optional().allow(""),
lastModified: Joi.date().iso().optional(),
}),
]);

export interface UploadedDocument extends GenericDocument {
id: string;
base64: string;
fileName: string;
comment?: string;
lastModified?: string;
}

export interface DocumentLink extends GenericDocument {
Expand All @@ -90,6 +101,7 @@ export const uploadedDocumentSchema = Joi.alternatives([
.max(MAX_DOCUMENT_SIZE_BASE64)
.error(() => new Error("Document is not valid")),
fileName: Joi.string(),
comment: Joi.string().optional().allow(""),
}),
Joi.object({
id: Joi.string(),
Expand All @@ -98,6 +110,7 @@ export const uploadedDocumentSchema = Joi.alternatives([
.required()
.error(() => new Error("Link is not valid")),
fileName: Joi.string(),
comment: Joi.string().optional().allow(""),
linkedFileHash: Joi.string(),
}),
]);
Expand All @@ -114,6 +127,7 @@ export async function hashDocument(
id: document.id,
hash: hashValue,
fileName: document.fileName,
comment: document.comment,
}));
}

Expand Down
6 changes: 3 additions & 3 deletions api/src/service/domain/document/document_upload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const repository = {
getAllDocumentInfos: (): Promise<StoredDocument[]> => Promise.resolve(existingDocuments),
getAllDocumentReferences: (): Promise<DocumentOrExternalLinkReference[]> =>
Promise.resolve(documentReferences),
storeDocument: (_id, _name, _hash): Promise<any> =>
storeDocument: (_file): Promise<any> =>
Promise.resolve({
id: "1",
secret: "secret",
Expand Down Expand Up @@ -120,15 +120,15 @@ describe("Storage Service: Upload a document", async () => {
it("Uploading document fails if storing the document failed", async () => {
const result = await uploadDocument(ctx, alice, requestData, {
...repository,
storeDocument: (_id, _name, _hash) => Promise.resolve(new VError("failed to store document")),
storeDocument: (_file) => Promise.resolve(new VError("failed to store document")),
});
assert.isTrue(Result.isErr(result));
});

it("Uploading document fails if no secret is returned from storage service", async () => {
const result = await uploadDocument(ctx, alice, requestData, {
...repository,
storeDocument: (id, _name, _hash) => Promise.resolve({ id, secret: undefined } as any),
storeDocument: (_file) => Promise.resolve({ id, secret: undefined } as any),
});
assert.isTrue(Result.isErr(result));
});
Expand Down
20 changes: 12 additions & 8 deletions api/src/service/domain/document/document_upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,22 @@ export interface RequestData {
id: string;
fileName: string;
documentBase64: string;
comment?: string;
}

export interface File {
id: string;
fileName: string;
documentBase64: string;
comment?: string;
}

type Base64String = string;

interface Repository {
getAllDocumentReferences(): Promise<Result.Type<GenericDocument[]>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
storeDocument(id, name, hash): Promise<any>;
storeDocument(file: File): Promise<any>;
encryptWithKey(secret, publicKey): Promise<Result.Type<string>>;
getPublicKey(organization): Promise<Result.Type<Base64String>>;
getUser(userId: string): Promise<Result.Type<UserRecord.UserRecord>>;
Expand All @@ -36,10 +44,10 @@ function docIdAlreadyExists(existingDocuments: GenericDocument[], docId: string)
export async function uploadDocument(
ctx: Ctx,
issuer: ServiceUser,
requestData: RequestData,
file: File,
repository: Repository,
): Promise<Result.Type<BusinessEvent[]>> {
const { id, documentBase64, fileName } = requestData;
const { id, documentBase64, fileName } = file;

logger.trace("Getting all documents from repository");
const existingDocuments = await repository.getAllDocumentReferences();
Expand All @@ -57,11 +65,7 @@ export async function uploadDocument(
}

logger.trace("Storing document in storage");
const documentStorageServiceResponseResult = await repository.storeDocument(
id,
fileName,
documentBase64,
);
const documentStorageServiceResponseResult = await repository.storeDocument(file);

if (Result.isErr(documentStorageServiceResponseResult)) {
return new VError(documentStorageServiceResponseResult, "failed to store document");
Expand Down
11 changes: 9 additions & 2 deletions api/src/service/domain/document/workflowitem_document_delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ import { NotFound } from "../errors/not_found";
import { ServiceUser } from "../organization/service_user";
import * as Workflowitem from "../workflow/workflowitem";
import * as WorkflowitemUpdated from "../workflow/workflowitem_updated";
import { DocumentOrExternalLinkReference, ExternalLinkReference, StoredDocument } from "./document";
import {
DocumentOrExternalLinkReference,
ExternalLinkReference,
StoredDocument,
UploadedDocument,
} from "./document";
import * as DocumentDeleted from "./document_deleted";
import * as DocumentShared from "./document_shared";
import VError = require("verror");
import { BusinessEvent } from "../business_event";
import * as WorkflowitemEventSourcing from "../workflow/workflowitem_eventsourcing";

function isDocumentLink(obj: DocumentOrExternalLinkReference): obj is ExternalLinkReference {
export function isDocumentLink(
obj: DocumentOrExternalLinkReference | UploadedDocument,
): obj is ExternalLinkReference {
return "link" in obj;
}

Expand Down
22 changes: 11 additions & 11 deletions api/src/service/domain/workflow/workflowitem_create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import * as Subproject from "./subproject";
import * as Workflowitem from "./workflowitem";
import * as WorkflowitemCreated from "./workflowitem_created";
import uuid = require("uuid");
import { File } from "../document/document_upload";
import { isDocumentLink } from "../document/workflowitem_document_delete";

export interface RequestData {
projectId: Project.Id;
Expand Down Expand Up @@ -91,11 +93,7 @@ interface Repository {
event: BusinessEvent,
workflowitem: Workflowitem.Workflowitem,
): Result.Type<BusinessEvent[]>;
uploadDocumentToStorageService(
fileName: string,
documentBase64: string,
docId: string,
): Promise<Result.Type<BusinessEvent[]>>;
uploadDocumentToStorageService(file: File): Promise<Result.Type<BusinessEvent[]>>;
getAllDocumentReferences(): Promise<Result.Type<GenericDocument[]>>;
}

Expand Down Expand Up @@ -187,13 +185,14 @@ export async function createWorkflowitem(
// preparation for workflowitem_created event
for (const doc of reqData.documents || []) {
doc.id = generateUniqueDocId(existingDocuments);
if ("base64" in doc) {
if (!isDocumentLink(doc)) {
const hashedDocumentResult = await hashDocument(doc);
if (Result.isErr(hashedDocumentResult)) {
return new VError(hashedDocumentResult, `cannot hash document ${doc.id} `);
}
documents.push(hashedDocumentResult);
} else {
doc.lastModified = new Date().toISOString();
documents.push(doc);
}
}
Expand All @@ -204,11 +203,12 @@ export async function createWorkflowitem(
.filter((document) => "base64" in document)
.map(async (document) => {
logger.trace({ document }, "Trying to upload document to storage service");
return repository.uploadDocumentToStorageService(
document.fileName || "",
document.base64,
document.id,
);
return repository.uploadDocumentToStorageService({
id: document.id,
fileName: document.fileName || "",
documentBase64: document.base64,
comment: document.comment,
});
}),
);
for (const result of documentUploadedEventsResults) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ async function setDocumentAvailability(
...doc,
available: Result.isOk(result),
isValidHash: isIdentical,
lastModified: result.lastModified,
});
} else {
docsWithAvailability.push({ ...doc, available: Result.isOk(result) });
Expand Down
Loading

0 comments on commit 97ce29d

Please sign in to comment.