feat: mail entity download
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* Entity management service
|
||||
*/
|
||||
|
||||
import { transceivePost, transceiveStream } from './transceive';
|
||||
import { transceivePost, transceiveStream, transceiveDownload } from './transceive';
|
||||
import type {
|
||||
EntityFetchRequest,
|
||||
EntityFetchResponse,
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
EntityListBulkRequest,
|
||||
EntityPatchResponse,
|
||||
EntityPatchRequest,
|
||||
EntityDownloadRequest,
|
||||
} from '../types/entity';
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||
import { EntityObject } from '../models';
|
||||
@@ -110,7 +111,7 @@ export const entityService = {
|
||||
|
||||
// Convert response to EntityObject instances
|
||||
const list: Record<string, EntityObject> = {};
|
||||
Object.entries(response).forEach(([identifier, entity]) => {
|
||||
Object.entries(response).forEach(([, entity]) => {
|
||||
list[entity.identifier] = createEntityObject(entity);
|
||||
});
|
||||
|
||||
@@ -216,6 +217,16 @@ export const entityService = {
|
||||
async transmit(request: EntityTransmitRequest): Promise<EntityTransmitResponse> {
|
||||
return await transceivePost<EntityTransmitRequest, EntityTransmitResponse>('entity.transmit', request);
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit a browser-native attachment download request.
|
||||
*
|
||||
* The backend download endpoint is expected to honor the supplied selector
|
||||
* and respond with an attachment payload rather than JSON.
|
||||
*/
|
||||
download(request: EntityDownloadRequest): { transaction: string } {
|
||||
return transceiveDownload<EntityDownloadRequest>('entity.download', request);
|
||||
},
|
||||
};
|
||||
|
||||
export default entityService;
|
||||
|
||||
@@ -141,3 +141,84 @@ export async function transceiveStream<TRequest, TData>(
|
||||
|
||||
return { total };
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a browser-native file download request via a top-level form POST.
|
||||
*
|
||||
* This avoids buffering the response body in application-managed JavaScript
|
||||
* memory. The backend is expected to accept form fields where `data` contains
|
||||
* the serialized operation payload and to return an attachment response.
|
||||
*/
|
||||
export function transceiveDownload<TRequest>(
|
||||
operation: string,
|
||||
data: TRequest,
|
||||
user?: string,
|
||||
): { transaction: string } {
|
||||
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
||||
throw new Error('Browser window is not available for download submission');
|
||||
}
|
||||
|
||||
const request: ApiRequest<TRequest> = {
|
||||
version: API_VERSION,
|
||||
transaction: generateTransactionId(),
|
||||
operation,
|
||||
data,
|
||||
user,
|
||||
};
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = API_URL;
|
||||
form.target = '_blank';
|
||||
form.style.display = 'none';
|
||||
|
||||
appendHiddenField(form, 'version', String(request.version));
|
||||
appendHiddenField(form, 'transaction', request.transaction);
|
||||
appendHiddenField(form, 'operation', request.operation);
|
||||
appendFormValue(form, 'data', request.data);
|
||||
|
||||
if (request.user) {
|
||||
appendHiddenField(form, 'user', request.user);
|
||||
}
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
form.remove();
|
||||
|
||||
return { transaction: request.transaction };
|
||||
}
|
||||
|
||||
function appendHiddenField(form: HTMLFormElement, name: string, value: string): void {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = name;
|
||||
input.value = value;
|
||||
form.appendChild(input);
|
||||
}
|
||||
|
||||
function appendFormValue(form: HTMLFormElement, name: string, value: unknown): void {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
appendHiddenField(form, name, '');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => {
|
||||
appendFormValue(form, `${name}[${index}]`, item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
Object.entries(value as Record<string, unknown>).forEach(([key, nestedValue]) => {
|
||||
appendFormValue(form, `${name}[${key}]`, nestedValue);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
appendHiddenField(form, name, String(value));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import { defineStore } from 'pinia'
|
||||
import { entityService } from '../services'
|
||||
import { EntityObject, MessageObject } from '../models'
|
||||
import type {
|
||||
EntityListStreamRequest,
|
||||
EntityBlobSelector,
|
||||
EntityDownloadRequest,
|
||||
EntityTransmitRequest,
|
||||
EntityTransmitResponse,
|
||||
} from '../types/entity'
|
||||
@@ -18,7 +19,7 @@ import type {
|
||||
ListRange,
|
||||
ListSort,
|
||||
} from '../types/common'
|
||||
import type { MessageInterface } from '@/types/message'
|
||||
import type { MessageInterface, MessagePartInterface } from '@/types/message'
|
||||
|
||||
export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
||||
// State
|
||||
@@ -184,15 +185,15 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
||||
const response = await entityService.delta({ sources })
|
||||
|
||||
// Process delta and update store
|
||||
Object.entries(response).forEach(([provider, providerData]) => {
|
||||
Object.entries(response).forEach(([, providerData]) => {
|
||||
// Skip if no changes for provider
|
||||
if (providerData === false) return
|
||||
|
||||
Object.entries(providerData).forEach(([service, serviceData]) => {
|
||||
Object.entries(providerData).forEach(([, serviceData]) => {
|
||||
// Skip if no changes for service
|
||||
if (serviceData === false) return
|
||||
|
||||
Object.entries(serviceData).forEach(([collection, collectionData]) => {
|
||||
Object.entries(serviceData).forEach(([, collectionData]) => {
|
||||
// Skip if no changes for collection
|
||||
if (collectionData === false) return
|
||||
|
||||
@@ -474,6 +475,39 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function download(target: EntityIdentifier, part?: Partial<MessagePartInterface>) {
|
||||
let targetPart: EntityBlobSelector | undefined = undefined
|
||||
if (part && (part.blobId || part.partId || part.cid)) {
|
||||
targetPart = {
|
||||
blobId: part.blobId ?? undefined,
|
||||
partId: part.partId ?? undefined,
|
||||
cid: part.cid ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
let filename: string
|
||||
if (part && part.name && part.name.trim().length > 0) {
|
||||
filename = part.name.trim()
|
||||
} else if (part) {
|
||||
filename = `attachment-${part.partId || part.blobId || part.cid || 'unknown'}`
|
||||
} else {
|
||||
filename = 'message.eml'
|
||||
}
|
||||
|
||||
const request: EntityDownloadRequest = {
|
||||
target,
|
||||
part: targetPart,
|
||||
filename,
|
||||
}
|
||||
|
||||
try {
|
||||
entityService.download(request)
|
||||
} catch (error: any) {
|
||||
console.error('[Mail Manager][Store] - Failed to submit attachment download:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Return public API
|
||||
return {
|
||||
// State (readonly)
|
||||
@@ -495,5 +529,6 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
||||
delta,
|
||||
move,
|
||||
transmit,
|
||||
download,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -205,4 +205,32 @@ export interface EntityTransmitRequest {
|
||||
export interface EntityTransmitResponse {
|
||||
id: string;
|
||||
status: 'queued' | 'sent';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity Blob Fetch
|
||||
*/
|
||||
export interface EntityBlobSelector {
|
||||
blobId?: string;
|
||||
partId?: string;
|
||||
cid?: string;
|
||||
}
|
||||
|
||||
export interface EntityDownloadRequest {
|
||||
target: EntityIdentifier;
|
||||
part?: EntityBlobSelector;
|
||||
filename?: string | null;
|
||||
}
|
||||
|
||||
export interface EntityBlobsRequest {
|
||||
target: EntityIdentifier;
|
||||
parts: EntityBlobSelector[];
|
||||
}
|
||||
|
||||
export interface EntityBlobsResult {
|
||||
source: EntityIdentifier;
|
||||
part: EntityBlobSelector;
|
||||
blob: Blob;
|
||||
}
|
||||
|
||||
export interface EntityBlobsResponse extends Array<EntityBlobsResult> {}
|
||||
Reference in New Issue
Block a user