refactor: front end code to unified manager api

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-20 18:56:35 -04:00
parent 053c272fe4
commit c4cb1512d2
21 changed files with 1484 additions and 927 deletions
+17 -4
View File
@@ -70,9 +70,16 @@ export const collectionService = {
*
* @returns Promise with collection object
*/
async fetch(request: CollectionFetchRequest): Promise<CollectionObject> {
async fetch(request: CollectionFetchRequest): Promise<Record<string, CollectionObject>> {
const response = await transceivePost<CollectionFetchRequest, CollectionFetchResponse>('collection.fetch', request);
return createCollectionObject(response);
// Convert response to CollectionObject instances
const list: Record<string, CollectionObject> = {};
Object.entries(response).forEach(([, collection]) => {
list[collection.identifier] = createCollectionObject(collection);
});
return list;
},
/**
@@ -123,8 +130,14 @@ export const collectionService = {
*
* @returns Promise with deletion result
*/
async delete(request: CollectionDeleteRequest): Promise<CollectionDeleteResponse> {
return await transceivePost<CollectionDeleteRequest, CollectionDeleteResponse>('collection.delete', request);
async delete(request: CollectionDeleteRequest): Promise<boolean | CollectionObject> {
const response = await transceivePost<CollectionDeleteRequest, CollectionDeleteResponse>('collection.delete', request);
if (response.disposition === 'moved' && response.mutation) {
return createCollectionObject(response.mutation);
}
return true;
},
};
+80 -31
View File
@@ -2,10 +2,12 @@
* Entity management service
*/
import { transceivePost } from './transceive';
import { transceivePost, transceiveStream } from './transceive';
import type {
EntityListRequest,
EntityListResponse,
EntityListBulkRequest,
EntityListBulkResponse,
EntityListStreamRequest,
EntityListStreamResponse,
EntityFetchRequest,
EntityFetchResponse,
EntityExtantRequest,
@@ -18,6 +20,10 @@ import type {
EntityDeleteResponse,
EntityDeltaRequest,
EntityDeltaResponse,
EntityMoveRequest,
EntityMoveResponse,
EntityCopyRequest,
EntityCopyResponse,
EntityInterface,
} from '../types/entity';
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
@@ -31,7 +37,7 @@ function createEntityObject(data: EntityInterface): EntityObject {
const integrationStore = useIntegrationStore();
const factoryItem = integrationStore.getItemById('people_entity_factory', data.provider) as any;
const factory = factoryItem?.factory;
// Use provider factory if available, otherwise base class
return factory ? factory(data) : new EntityObject().fromJson(data);
}
@@ -39,15 +45,15 @@ function createEntityObject(data: EntityInterface): EntityObject {
export const entityService = {
/**
* Retrieve list of entities, optionally filtered by source selector
*
* Retrieve list of entities, optionally filtered by source collection identifiers
*
* @param request - list request parameters
*
*
* @returns Promise with entity object list grouped by provider, service, collection, and entity identifier
*/
async list(request: EntityListRequest = {}): Promise<Record<string, Record<string, Record<string, Record<string, EntityObject>>>>> {
const response = await transceivePost<EntityListRequest, EntityListResponse>('entity.list', request);
async listBulk(request: EntityListBulkRequest = {}): Promise<Record<string, Record<string, Record<string, Record<string, EntityObject>>>>> {
const response = await transceivePost<EntityListBulkRequest, EntityListBulkResponse>('entity.listBulk', request);
// Convert nested response to EntityObject instances
const providerList: Record<string, Record<string, Record<string, Record<string, EntityObject>>>> = {};
Object.entries(response).forEach(([providerId, providerServices]) => {
@@ -65,34 +71,55 @@ export const entityService = {
});
providerList[providerId] = serviceList;
});
return providerList;
},
/**
* Stream entities as NDJSON, invoking onEntity for each entity as it arrives.
*
* The server emits one entity per line so the caller receives entities
* progressively rather than waiting for the full collection to load.
*
* @param request - stream request parameters (same shape as list)
* @param onEntity - called synchronously for each entity as it is received
*
* @returns Promise resolving to { total } when the stream completes
*/
async listStream(request: EntityListStreamRequest, onEntity: (entity: EntityObject) => void): Promise<{ total: number }> {
return await transceiveStream<EntityListStreamRequest, EntityListStreamResponse>(
'entity.listStream',
request,
(entity) => {
onEntity(createEntityObject(entity));
}
);
},
/**
* Retrieve a specific entity by provider and identifier
*
*
* @param request - fetch request parameters
*
*
* @returns Promise with entity objects keyed by identifier
*/
async fetch(request: EntityFetchRequest): Promise<Record<string, EntityObject>> {
const response = await transceivePost<EntityFetchRequest, EntityFetchResponse>('entity.fetch', request);
// Convert response to EntityObject instances
const list: Record<string, EntityObject> = {};
Object.entries(response).forEach(([identifier, entityData]) => {
list[identifier] = createEntityObject(entityData);
Object.entries(response).forEach(([, entity]) => {
list[entity.identifier] = createEntityObject(entity);
});
return list;
},
/**
* Retrieve entity availability status for a given source selector
*
* Retrieve entity availability status for a given set of entity identifiers
*
* @param request - extant request parameters
*
*
* @returns Promise with entity availability status
*/
async extant(request: EntityExtantRequest): Promise<EntityExtantResponse> {
@@ -101,9 +128,9 @@ export const entityService = {
/**
* Create a new entity
*
*
* @param request - create request parameters
*
*
* @returns Promise with created entity object
*/
async create(request: EntityCreateRequest): Promise<EntityObject> {
@@ -113,9 +140,9 @@ export const entityService = {
/**
* Update an existing entity
*
*
* @param request - update request parameters
*
*
* @returns Promise with updated entity object
*/
async update(request: EntityUpdateRequest): Promise<EntityObject> {
@@ -124,11 +151,11 @@ export const entityService = {
},
/**
* Delete an entity
*
* Delete entities by their identifiers
*
* @param request - delete request parameters
*
* @returns Promise with deletion result
*
* @returns Promise with deletion results keyed by source entity identifier
*/
async delete(request: EntityDeleteRequest): Promise<EntityDeleteResponse> {
return await transceivePost<EntityDeleteRequest, EntityDeleteResponse>('entity.delete', request);
@@ -136,15 +163,37 @@ export const entityService = {
/**
* Retrieve delta changes for entities
*
*
* @param request - delta request parameters
*
* @returns Promise with delta changes (created, modified, deleted)
*
* @returns Promise with delta changes (additions, modifications, deletions)
*/
async delta(request: EntityDeltaRequest): Promise<EntityDeltaResponse> {
return await transceivePost<EntityDeltaRequest, EntityDeltaResponse>('entity.delta', request);
},
/**
* Move entities to a target collection
*
* @param request - move request parameters
*
* @returns Promise with move results keyed by source entity identifier
*/
async move(request: EntityMoveRequest): Promise<EntityMoveResponse> {
return await transceivePost<EntityMoveRequest, EntityMoveResponse>('entity.move', request);
},
/**
* Copy entities to a target collection
*
* @param request - copy request parameters
*
* @returns Promise with copy results keyed by source entity identifier
*/
async copy(request: EntityCopyRequest): Promise<EntityCopyResponse> {
return await transceivePost<EntityCopyRequest, EntityCopyResponse>('entity.copy', request);
},
};
export default entityService;
+94 -2
View File
@@ -4,7 +4,7 @@
*/
import { createFetchWrapper } from '@KTXC';
import type { ApiRequest, ApiResponse } from '../types/common';
import type { ApiRequest, ApiResponse, ApiStreamResponse } from '../types/common';
const fetchWrapper = createFetchWrapper();
const API_URL = '/m/people_manager/v1';
@@ -45,6 +45,98 @@ export async function transceivePost<TRequest, TResponse>(
const errorMessage = `[${operation}] ${response.data.message}${response.data.code ? ` (code: ${response.data.code})` : ''}`;
throw new Error(errorMessage);
}
return response.data;
}
/**
* Stream an NDJSON API response, unwrapping data frames for the caller.
*
* The server emits one JSON object per line with a transport-level `type`
* discriminant. This helper consumes control and error frames, forwards only
* unwrapped `data` payloads to the caller, and returns the final stream total.
*
* @param operation - Operation name, e.g. 'entity.listStream'
* @param data - Operation-specific request data
* @param onData - Synchronous callback invoked for every unwrapped data payload.
* May throw to abort the stream.
* @param user - Optional user identifier override
* @returns Promise resolving to the final stream total from the control/end frame
*/
export async function transceiveStream<TRequest, TData>(
operation: string,
data: TRequest,
onData: (data: TData) => void,
user?: string
): Promise<{ total: number }> {
const request: ApiRequest<TRequest> = {
version: API_VERSION,
transaction: generateTransactionId(),
operation,
data,
user,
};
let total = 0;
await fetchWrapper.post(API_URL, request, {
headers: { 'Accept': 'application/json' },
onStream: async (response: Response) => {
if (!response.body) {
throw new Error(`[${operation}] Response body is not readable`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop()!; // retain any incomplete trailing chunk
for (const line of lines) {
if (!line.trim()) continue;
const message = JSON.parse(line) as ApiStreamResponse<TData>;
if (message.type === 'control') {
if (message.status === 'end') {
total = message.total;
}
continue;
}
if (message.type === 'error') {
throw new Error(`[${operation}] ${message.message}`);
}
onData(message.data);
}
}
// flush any remaining bytes still in the buffer
if (buffer.trim()) {
const message = JSON.parse(buffer) as ApiStreamResponse<TData>;
if (message.type === 'control') {
if (message.status === 'end') {
total = message.total;
}
} else if (message.type === 'error') {
throw new Error(`[${operation}] ${message.message}`);
} else {
onData(message.data);
}
}
} finally {
reader.releaseLock();
}
},
});
return { total };
}