feat: implement entity patch
Some checks failed
Build Test / test (pull_request) Successful in 35s
JS Unit Tests / test (pull_request) Failing after 34s
PHP Unit Tests / test (pull_request) Successful in 1m9s

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-05-20 21:37:14 -04:00
parent aff17840ed
commit 453d720046
5 changed files with 117 additions and 43 deletions

View File

@@ -123,6 +123,11 @@ export class MessageObject implements MessageModelInterface {
return clonePlain(this._data.flags ?? {});
}
// this should be moved to a mutable object, but for now we can allow it here for convenience
set flags(value: { read?: boolean; flagged?: boolean; answered?: boolean; draft?: boolean }) {
this._data.flags = clonePlain(value);
}
/** Helper methods */
get isRead(): boolean {

View File

@@ -25,6 +25,8 @@ import type {
EntityListStreamRequest,
EntityListBulkResponse,
EntityListBulkRequest,
EntityPatchResponse,
EntityPatchRequest,
} from '../types/entity';
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
import { EntityObject } from '../models';
@@ -150,6 +152,27 @@ export const entityService = {
return createEntityObject(response);
},
/**
* Patch existing entities with new properties
*
* @param request - patch request parameters
*
* @returns Promise with patch results keyed by target entity identifier
*/
async patch(request: EntityPatchRequest): Promise<EntityPatchResponse> {
const properties = {
'@type': request.properties['@type'] ?? 'mail:message',
...(Object.prototype.hasOwnProperty.call(request.properties, 'flags')
? { flags: request.properties.flags }
: {}),
}
return await transceivePost<EntityPatchRequest, EntityPatchResponse>('entity.patch', {
...request,
properties,
});
},
/**
* Delete entities by their identifiers
*

View File

@@ -7,7 +7,7 @@ import { defineStore } from 'pinia'
import { entityService } from '../services'
import { EntityObject, MessageObject } from '../models'
import type {
EntityStreamRequest,
EntityListStreamRequest,
EntityTransmitRequest,
EntityTransmitResponse,
} from '../types/entity'
@@ -216,6 +216,15 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
}
}
/**
* Create a new empty entity object
*
* @returns New entity object instance
*/
function fresh(): EntityObject {
return new EntityObject()
}
/**
* Create a new entity with given collection identifier and properties
*
@@ -282,19 +291,19 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
*
* Removes successfully deleted entities from the local store.
*
* @param sources - entity identifiers to delete
* @param targets - entity identifiers to delete
*
* @returns Promise with deletion results keyed by source identifier
* @returns Promise with deletion results keyed by target identifier
*/
async function remove(sources: EntityIdentifier[]): Promise<{successes: EntityIdentifier[], failures: EntityIdentifier[]}> {
async function remove(targets: EntityIdentifier[]): Promise<{successes: EntityIdentifier[], failures: EntityIdentifier[]}> {
transceiving.value = true
try {
const response = await entityService.delete({ sources })
const response = await entityService.delete({ targets })
const successes: EntityIdentifier[] = []
const failures: EntityIdentifier[] = []
Object.entries(response).forEach(([sourceIdentifier, result]) => {
const originalIdentifier = sourceIdentifier as EntityIdentifier
Object.entries(response).forEach(([targetIdentifier, result]) => {
const originalIdentifier = targetIdentifier as EntityIdentifier
if (!result.disposition || result.disposition === 'error') {
console.warn(`[Mail Manager][Store] - Entity move on "${originalIdentifier}" returned an error: ${result.error})`)
failures.push(originalIdentifier)
@@ -335,6 +344,57 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
}
}
/**
* Patch existing entities with new properties
*/
async function patch(properties: MessageInterface | MessageObject, targets: EntityIdentifier[]) {
transceiving.value = true
try {
if (properties instanceof MessageObject) {
properties = properties.toJson()
}
const response = await entityService.patch({ properties, targets })
const successes: EntityIdentifier[] = []
const failures: EntityIdentifier[] = []
Object.entries(response).forEach(([targetIdentifier, result]) => {
const originalIdentifier = targetIdentifier as EntityIdentifier
if (!result.disposition || result.disposition === 'error') {
console.warn(`[Mail Manager][Store] - Entity patch on "${originalIdentifier}" returned an error: ${result.error})`)
failures.push(originalIdentifier)
return
}
const cachedEntity = _entities.value[originalIdentifier]
if (!cachedEntity) {
return
}
if (result.disposition === 'patched') {
const cachedEntityJson = cachedEntity.toJson()
const mutatedEntity = cachedEntity.clone().fromJson({
...cachedEntityJson,
properties: {
...cachedEntityJson.properties,
...properties,
},
})
_entities.value[originalIdentifier] = mutatedEntity
}
successes.push(originalIdentifier)
})
console.debug('[Mail Manager][Store] - Successfully patched', successes.length, 'entities')
return { successes, failures }
} catch (error: any) {
console.error('[Mail Manager][Store] - Failed to patch entities:', error)
throw error
} finally {
transceiving.value = false
}
}
/**
* Move entities to another collection.
*
@@ -414,36 +474,6 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
}
}
/**
* Stream entities progressively, merging each entity into the store as it arrives.
*
* Unlike list(), which waits for the full response before updating the store,
* stream() updates reactive state entity-by-entity so UI renders incrementally.
*
* @param sources - optional source selector
* @param filter - optional list filter
* @param sort - optional list sort
* @param range - optional list range
*
* @returns Promise resolving to { total } when the stream completes
*/
async function stream(sources?: CollectionIdentifier[], filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise<{ total: number }> {
transceiving.value = true
try {
const request: EntityStreamRequest = { sources, filter, sort, range }
const result = await entityService.stream(request, (entity: EntityObject) => {
_entities.value[entity.identifier] = entity
})
console.debug('[Mail Manager][Store] - Successfully streamed', result.total, 'entities')
return result
} catch (error: any) {
console.error('[Mail Manager][Store] - Failed to stream entities:', error)
throw error
} finally {
transceiving.value = false
}
}
// Return public API
return {
// State (readonly)
@@ -457,12 +487,13 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
list,
fetch,
extant,
fresh,
create,
update,
patch,
delete: remove,
delta,
move,
transmit,
stream,
}
})

View File

@@ -128,15 +128,30 @@ export interface EntityUpdateRequest<T = MessageInterface> {
export interface EntityUpdateResponse<T = MessageInterface> extends EntityInterface<T> {}
/**
* Entity patch
*/
export interface EntityPatchRequest<T = MessageInterface> {
properties: T;
targets: EntityIdentifier[];
}
export interface EntityPatchResponse{
[targetIdentifier: EntityIdentifier]: {
disposition: 'patched' | 'error';
error?: string;
};
}
/**
* Entity delete
*/
export interface EntityDeleteRequest {
sources: EntityIdentifier[];
targets: EntityIdentifier[];
}
export interface EntityDeleteResponse {
[sourceIdentifier: EntityIdentifier]: {
[targetIdentifier: EntityIdentifier]: {
disposition: 'deleted' | 'moved' | 'error';
destination: CollectionIdentifier | null;
mutation: EntityIdentifier | null;