feat: implement entity patch #24
@@ -797,10 +797,10 @@ class DefaultController extends ControllerAbstract {
|
||||
if (!is_array($data['targets'])) {
|
||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
||||
}
|
||||
if (!isset($data['data'])) {
|
||||
if (!isset($data['properties'])) {
|
||||
throw new InvalidArgumentException(self::ERR_MISSING_DATA);
|
||||
}
|
||||
if (!is_array($data['data'])) {
|
||||
if (!is_array($data['properties'])) {
|
||||
throw new InvalidArgumentException(self::ERR_INVALID_DATA);
|
||||
}
|
||||
|
||||
@@ -811,7 +811,7 @@ class DefaultController extends ControllerAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
return $this->mailManager->entityPatch($tenantId, $userId, $targets, $data['data']);
|
||||
return $this->mailManager->entityPatch($tenantId, $userId, $data['properties'], ...$targets->all());
|
||||
}
|
||||
|
||||
private function entityMove(string $tenantId, string $userId, array $data): mixed {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user