feat: use resource identifiers
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import type { Ref, ComputedRef } from 'vue'
|
|||||||
import { useProvidersStore } from '@/stores/providersStore'
|
import { useProvidersStore } from '@/stores/providersStore'
|
||||||
import { useServicesStore } from '@/stores/servicesStore'
|
import { useServicesStore } from '@/stores/servicesStore'
|
||||||
import { useNodesStore, ROOT_ID } from '@/stores/nodesStore'
|
import { useNodesStore, ROOT_ID } from '@/stores/nodesStore'
|
||||||
import type { ListFilter, ListSort, ListRange } from '@/types/common'
|
import type { ListFilter, ListSort, ListRange, ServiceIdentifier, CollectionIdentifier } from '@/types/common'
|
||||||
import type { DocumentInterface } from '@/types/document'
|
import type { DocumentInterface } from '@/types/document'
|
||||||
import { CollectionObject } from '@/models/collection'
|
import { CollectionObject } from '@/models/collection'
|
||||||
import { EntityObject } from '@/models/entity'
|
import { EntityObject } from '@/models/entity'
|
||||||
@@ -16,6 +16,9 @@ import { EntityObject } from '@/models/entity'
|
|||||||
// Base URL for file manager transfer endpoints
|
// Base URL for file manager transfer endpoints
|
||||||
const TRANSFER_BASE_URL = '/m/documents_manager'
|
const TRANSFER_BASE_URL = '/m/documents_manager'
|
||||||
|
|
||||||
|
// Number of collections+entities requested per page
|
||||||
|
const DEFAULT_PAGE_TALLY = 64
|
||||||
|
|
||||||
export interface UseDocumentManagerOptions {
|
export interface UseDocumentManagerOptions {
|
||||||
providerId: string | Ref<string> | ComputedRef<string>
|
providerId: string | Ref<string> | ComputedRef<string>
|
||||||
serviceId: string | Ref<string> | ComputedRef<string>
|
serviceId: string | Ref<string> | ComputedRef<string>
|
||||||
@@ -32,6 +35,12 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
const currentProviderId = () => unref(providerId)
|
const currentProviderId = () => unref(providerId)
|
||||||
const currentServiceId = () => unref(serviceId)
|
const currentServiceId = () => unref(serviceId)
|
||||||
|
|
||||||
|
const currentSources = (): (ServiceIdentifier | CollectionIdentifier)[] => [
|
||||||
|
currentLocation.value === ROOT_ID
|
||||||
|
? `${currentProviderId()}:${currentServiceId()}`
|
||||||
|
: `${currentProviderId()}:${currentServiceId()}:${currentLocation.value}`,
|
||||||
|
]
|
||||||
|
|
||||||
// Current location (folder being viewed)
|
// Current location (folder being viewed)
|
||||||
const currentLocation: Ref<string> = ref(ROOT_ID)
|
const currentLocation: Ref<string> = ref(ROOT_ID)
|
||||||
|
|
||||||
@@ -39,6 +48,13 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
const isLoading = computed(() => nodesStore.transceiving)
|
const isLoading = computed(() => nodesStore.transceiving)
|
||||||
const error = computed(() => nodesStore.error)
|
const error = computed(() => nodesStore.error)
|
||||||
|
|
||||||
|
// Pagination state for the current location + filter + sort
|
||||||
|
const currentFilter: Ref<ListFilter | undefined> = ref(undefined)
|
||||||
|
const currentSort: Ref<ListSort | undefined> = ref(undefined)
|
||||||
|
const loadedCount = ref(0)
|
||||||
|
const hasMore = ref(false)
|
||||||
|
const isLoadingMore = ref(false)
|
||||||
|
|
||||||
// Provider and service
|
// Provider and service
|
||||||
const provider = computed(() => providersStore.provider(currentProviderId()))
|
const provider = computed(() => providersStore.provider(currentProviderId()))
|
||||||
const service = computed(() => servicesStore.service(currentProviderId(), currentServiceId()))
|
const service = computed(() => servicesStore.service(currentProviderId(), currentServiceId()))
|
||||||
@@ -71,7 +87,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
// Navigate to a folder
|
// Navigate to a folder
|
||||||
const navigateTo = async (collectionId: string | null) => {
|
const navigateTo = async (collectionId: string | null) => {
|
||||||
currentLocation.value = collectionId || ROOT_ID
|
currentLocation.value = collectionId || ROOT_ID
|
||||||
await refresh()
|
await resetAndLoad(currentFilter.value, currentSort.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate up one level
|
// Navigate up one level
|
||||||
@@ -90,28 +106,55 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
await navigateTo(ROOT_ID)
|
await navigateTo(ROOT_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh current location
|
// Reset the current location's loaded items and (re)fetch the first page,
|
||||||
const refresh = async (
|
// optionally with a new filter/sort (e.g. search query or sort field change)
|
||||||
filter?: ListFilter,
|
const resetAndLoad = async (filter?: ListFilter, sort?: ListSort) => {
|
||||||
sort?: ListSort,
|
currentFilter.value = filter
|
||||||
range?: ListRange
|
currentSort.value = sort
|
||||||
) => {
|
loadedCount.value = 0
|
||||||
await nodesStore.fetchNodes(
|
hasMore.value = false
|
||||||
currentProviderId(),
|
|
||||||
currentServiceId(),
|
const range: ListRange = { type: 'tally', anchor: 'absolute', position: 0, tally: DEFAULT_PAGE_TALLY }
|
||||||
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
const result = await nodesStore.fetchNodes(
|
||||||
|
currentSources(),
|
||||||
filter,
|
filter,
|
||||||
sort,
|
sort,
|
||||||
range
|
range,
|
||||||
|
'reset'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
loadedCount.value = result.collections.length + result.entities.length
|
||||||
|
hasMore.value = !result.isLastPage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the next page of the current location (infinite scroll "load more")
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (!hasMore.value || isLoadingMore.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoadingMore.value = true
|
||||||
|
try {
|
||||||
|
const range: ListRange = { type: 'tally', anchor: 'absolute', position: loadedCount.value, tally: DEFAULT_PAGE_TALLY }
|
||||||
|
const result = await nodesStore.fetchNodes(
|
||||||
|
currentSources(),
|
||||||
|
currentFilter.value,
|
||||||
|
currentSort.value,
|
||||||
|
range,
|
||||||
|
'append'
|
||||||
|
)
|
||||||
|
|
||||||
|
loadedCount.value += result.collections.length + result.entities.length
|
||||||
|
hasMore.value = !result.isLastPage
|
||||||
|
} finally {
|
||||||
|
isLoadingMore.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new folder
|
// Create a new folder
|
||||||
const createFolder = async (label: string): Promise<CollectionObject> => {
|
const createFolder = async (label: string): Promise<CollectionObject> => {
|
||||||
return await nodesStore.createCollection(
|
return await nodesStore.createCollection(
|
||||||
currentProviderId(),
|
`${currentProviderId()}:${currentServiceId()}:${currentLocation.value}`,
|
||||||
currentServiceId(),
|
|
||||||
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
|
||||||
{ label, owner: '' }
|
{ label, owner: '' }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -132,9 +175,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await nodesStore.createEntity(
|
return await nodesStore.createEntity(
|
||||||
currentProviderId(),
|
`${currentProviderId()}:${currentServiceId()}:${currentLocation.value}`,
|
||||||
currentServiceId(),
|
|
||||||
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
|
||||||
properties
|
properties
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -147,7 +188,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node instanceof CollectionObject) {
|
if (node instanceof CollectionObject) {
|
||||||
return await nodesStore.updateCollection(currentProviderId(), currentServiceId(), nodeId, {
|
return await nodesStore.updateCollection(`${node.provider}:${node.service}:${nodeId}`, {
|
||||||
label: newLabel,
|
label: newLabel,
|
||||||
owner: node.properties.owner,
|
owner: node.properties.owner,
|
||||||
})
|
})
|
||||||
@@ -161,7 +202,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
format: node.properties.format,
|
format: node.properties.format,
|
||||||
encoding: node.properties.encoding,
|
encoding: node.properties.encoding,
|
||||||
}
|
}
|
||||||
return await nodesStore.updateEntity(currentProviderId(), currentServiceId(), node.collection, nodeId, properties)
|
return await nodesStore.updateEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${nodeId}`, properties)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +214,9 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node instanceof CollectionObject) {
|
if (node instanceof CollectionObject) {
|
||||||
return await nodesStore.deleteCollection(currentProviderId(), currentServiceId(), nodeId)
|
return await nodesStore.deleteCollection(`${node.provider}:${node.service}:${nodeId}`)
|
||||||
} else {
|
} else {
|
||||||
return await nodesStore.deleteEntity(currentProviderId(), currentServiceId(), node.collection, nodeId)
|
return await nodesStore.deleteEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${nodeId}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +226,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
if (!node || !(node instanceof EntityObject)) {
|
if (!node || !(node instanceof EntityObject)) {
|
||||||
throw new Error('Entity not found')
|
throw new Error('Entity not found')
|
||||||
}
|
}
|
||||||
return await nodesStore.readEntity(currentProviderId(), currentServiceId(), node.collection || ROOT_ID, entityId)
|
return await nodesStore.readEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${entityId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write file content
|
// Write file content
|
||||||
@@ -194,7 +235,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
if (!node || !(node instanceof EntityObject)) {
|
if (!node || !(node instanceof EntityObject)) {
|
||||||
throw new Error('Entity not found')
|
throw new Error('Entity not found')
|
||||||
}
|
}
|
||||||
return await nodesStore.writeEntity(currentProviderId(), currentServiceId(), node.collection, entityId, content)
|
return await nodesStore.writeEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${entityId}`, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a URL suitable for inline viewing (img src / video src)
|
// Get a URL suitable for inline viewing (img src / video src)
|
||||||
@@ -253,7 +294,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
await providersStore.list([currentProviderId()])
|
await providersStore.list([currentProviderId()])
|
||||||
await servicesStore.list()
|
await servicesStore.list()
|
||||||
if (autoFetch) {
|
if (autoFetch) {
|
||||||
await refresh()
|
await resetAndLoad()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,6 +303,8 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
currentLocation,
|
currentLocation,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
|
hasMore,
|
||||||
|
isLoadingMore,
|
||||||
|
|
||||||
// Provider/Service
|
// Provider/Service
|
||||||
provider,
|
provider,
|
||||||
@@ -279,7 +322,8 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|||||||
navigateTo,
|
navigateTo,
|
||||||
navigateUp,
|
navigateUp,
|
||||||
navigateToRoot,
|
navigateToRoot,
|
||||||
refresh,
|
resetAndLoad,
|
||||||
|
loadMore,
|
||||||
|
|
||||||
// Operations
|
// Operations
|
||||||
createFolder,
|
createFolder,
|
||||||
|
|||||||
@@ -25,23 +25,11 @@ import type {
|
|||||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||||
import { CollectionObject, CollectionPropertiesObject } from '../models/collection';
|
import { CollectionObject, CollectionPropertiesObject } from '../models/collection';
|
||||||
|
|
||||||
function isCollectionPayload(value: unknown): value is CollectionInterface {
|
|
||||||
if (!value || typeof value !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidate = value as Record<string, unknown>;
|
|
||||||
return (
|
|
||||||
('identifier' in candidate || 'provider' in candidate || 'service' in candidate)
|
|
||||||
&& 'properties' in candidate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to create the right collection model class based on provider identifier
|
* Helper to create the right collection model class based on provider identifier
|
||||||
* Uses provider-specific factory if available, otherwise returns base CollectionObject
|
* Uses provider-specific factory if available, otherwise returns base CollectionObject
|
||||||
*/
|
*/
|
||||||
function createCollectionObject(data: CollectionInterface): CollectionObject {
|
export function createCollectionObject(data: CollectionInterface): CollectionObject {
|
||||||
const integrationStore = useIntegrationStore();
|
const integrationStore = useIntegrationStore();
|
||||||
const factoryItem = integrationStore.getItemById('documents_collection_factory', data.provider) as any;
|
const factoryItem = integrationStore.getItemById('documents_collection_factory', data.provider) as any;
|
||||||
const factory = factoryItem?.factory;
|
const factory = factoryItem?.factory;
|
||||||
@@ -68,38 +56,27 @@ export const collectionService = {
|
|||||||
const serviceList: Record<string, Record<string, CollectionObject>> = {};
|
const serviceList: Record<string, Record<string, CollectionObject>> = {};
|
||||||
Object.entries(providerServices).forEach(([serviceId, serviceCollections]) => {
|
Object.entries(providerServices).forEach(([serviceId, serviceCollections]) => {
|
||||||
const collectionList: Record<string, CollectionObject> = {};
|
const collectionList: Record<string, CollectionObject> = {};
|
||||||
Object.entries(serviceCollections as Record<string, unknown>).forEach(([collectionId, collectionData]) => {
|
Object.entries(serviceCollections).forEach(([collectionId, collectionData]) => {
|
||||||
if (isCollectionPayload(collectionData)) {
|
collectionList[collectionId] = createCollectionObject(collectionData);
|
||||||
collectionList[collectionId] = createCollectionObject(collectionData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collectionData && typeof collectionData === 'object') {
|
|
||||||
Object.entries(collectionData as Record<string, unknown>).forEach(([nestedCollectionId, nestedCollectionData]) => {
|
|
||||||
if (isCollectionPayload(nestedCollectionData)) {
|
|
||||||
collectionList[nestedCollectionId] = createCollectionObject(nestedCollectionData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
serviceList[serviceId] = collectionList;
|
serviceList[serviceId] = collectionList;
|
||||||
});
|
});
|
||||||
providerList[providerId] = serviceList;
|
providerList[providerId] = serviceList;
|
||||||
});
|
});
|
||||||
|
|
||||||
return providerList;
|
return providerList;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a specific collection by provider and identifier
|
* Retrieve collections by resource identifier
|
||||||
*
|
*
|
||||||
* @param request - fetch request parameters
|
* @param request - fetch request parameters
|
||||||
*
|
*
|
||||||
* @returns Promise with collection object
|
* @returns Promise with collections keyed by resource identifier
|
||||||
*/
|
*/
|
||||||
async fetch(request: CollectionFetchRequest): Promise<CollectionObject> {
|
async fetch(request: CollectionFetchRequest): Promise<Record<string, CollectionObject>> {
|
||||||
const response = await transceivePost<CollectionFetchRequest, CollectionFetchResponse>('collection.fetch', request);
|
const response = await transceivePost<CollectionFetchRequest, CollectionFetchResponse>('collection.fetch', request);
|
||||||
return createCollectionObject(response);
|
return Object.fromEntries(Object.entries(response).map(([target, data]) => [target, createCollectionObject(data)]));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { EntityObject } from '../models';
|
|||||||
* Helper to create the right entity model class based on provider identifier
|
* Helper to create the right entity model class based on provider identifier
|
||||||
* Uses provider-specific factory if available, otherwise returns base EntityObject
|
* Uses provider-specific factory if available, otherwise returns base EntityObject
|
||||||
*/
|
*/
|
||||||
function createEntityObject(data: EntityInterface): EntityObject {
|
export function createEntityObject(data: EntityInterface): EntityObject {
|
||||||
const integrationStore = useIntegrationStore();
|
const integrationStore = useIntegrationStore();
|
||||||
const factoryItem = integrationStore.getItemById('documents_entity_factory', data.provider) as any;
|
const factoryItem = integrationStore.getItemById('documents_entity_factory', data.provider) as any;
|
||||||
const factory = factoryItem?.factory;
|
const factory = factoryItem?.factory;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { transceiveStream } from './transceive';
|
||||||
|
import type { NodeListRequest, NodeInterface } from '../types/node';
|
||||||
|
import { createCollectionObject } from './collectionService';
|
||||||
|
import { createEntityObject } from './entityService';
|
||||||
|
import type { CollectionObject } from '../models/collection';
|
||||||
|
import type { EntityObject } from '../models/entity';
|
||||||
|
|
||||||
|
export type NodeListResult = (CollectionObject | EntityObject)[];
|
||||||
|
|
||||||
|
export const nodeService = {
|
||||||
|
/** Hydrate a mixed page in the exact order returned by the provider. */
|
||||||
|
async list(request: NodeListRequest = {}): Promise<NodeListResult> {
|
||||||
|
const nodes: NodeListResult = [];
|
||||||
|
const { total } = await transceiveStream<NodeListRequest, NodeInterface>('node.list', request, node => {
|
||||||
|
switch (node['@type']) {
|
||||||
|
case 'document:collection':
|
||||||
|
nodes.push(createCollectionObject(node));
|
||||||
|
break;
|
||||||
|
case 'document:entity':
|
||||||
|
nodes.push(createEntityObject(node));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported node type in node.list response');
|
||||||
|
}
|
||||||
|
}, { requireEnd: true });
|
||||||
|
if (total !== nodes.length) {
|
||||||
|
throw new Error('Incomplete node.list stream');
|
||||||
|
}
|
||||||
|
// Publish only complete pages so a retry uses the same offset after failure.
|
||||||
|
return nodes;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nodeService;
|
||||||
@@ -62,7 +62,7 @@ export async function transceiveStream<TRequest, TData>(
|
|||||||
operation: string,
|
operation: string,
|
||||||
data: TRequest,
|
data: TRequest,
|
||||||
onData: (data: TData) => void,
|
onData: (data: TData) => void,
|
||||||
options?: { user?: string; onStart?: (expected?: number) => void }
|
options?: { user?: string; onStart?: (expected?: number) => void; requireEnd?: boolean }
|
||||||
): Promise<{ total: number }> {
|
): Promise<{ total: number }> {
|
||||||
const request: ApiRequest<TRequest> = {
|
const request: ApiRequest<TRequest> = {
|
||||||
version: API_VERSION,
|
version: API_VERSION,
|
||||||
@@ -73,6 +73,7 @@ export async function transceiveStream<TRequest, TData>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
let ended = false;
|
||||||
|
|
||||||
const dispatch = (line: string): void => {
|
const dispatch = (line: string): void => {
|
||||||
const message = JSON.parse(line) as ApiStreamResponse<TData>;
|
const message = JSON.parse(line) as ApiStreamResponse<TData>;
|
||||||
@@ -82,6 +83,7 @@ export async function transceiveStream<TRequest, TData>(
|
|||||||
options?.onStart?.(message.total);
|
options?.onStart?.(message.total);
|
||||||
} else if (message.status === 'end') {
|
} else if (message.status === 'end') {
|
||||||
total = message.total;
|
total = message.total;
|
||||||
|
ended = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -118,6 +120,7 @@ export async function transceiveStream<TRequest, TData>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buffer += decoder.decode();
|
||||||
if (buffer.trim()) dispatch(buffer);
|
if (buffer.trim()) dispatch(buffer);
|
||||||
} finally {
|
} finally {
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
@@ -125,5 +128,8 @@ export async function transceiveStream<TRequest, TData>(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (options?.requireEnd && !ended) {
|
||||||
|
throw new Error(`[${operation}] Stream ended without a completion frame`);
|
||||||
|
}
|
||||||
return { total };
|
return { total };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,25 +40,25 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
return `${provider}:${service ?? ''}:${identifier ?? ''}`
|
return `${provider}:${service ?? ''}:${identifier ?? ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function collection(provider: string, service: string | number, identifier: string | number, retrieve: boolean = false): CollectionObject | null {
|
function collection(target: CollectionIdentifier, retrieve: boolean = false): CollectionObject | null {
|
||||||
const key = identifierKey(provider, service, identifier)
|
const key = target
|
||||||
if (retrieve === true && !_collections.value[key]) {
|
if (retrieve === true && !_collections.value[key]) {
|
||||||
console.debug(`[Documents Manager][Store] - Force fetching collection "${key}"`)
|
console.debug(`[Documents Manager][Store] - Force fetching collection "${key}"`)
|
||||||
fetch(provider, service, identifier)
|
void fetch([target])
|
||||||
}
|
}
|
||||||
|
|
||||||
return _collections.value[key] || null
|
return _collections.value[key] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectionsForService(provider: string, service: string | number): CollectionObject[] {
|
function collectionsForService(target: ServiceIdentifier): CollectionObject[] {
|
||||||
const serviceKeyPrefix = `${provider}:${service}:`
|
const serviceKeyPrefix = `${target}:`
|
||||||
return Object.entries(_collections.value)
|
return Object.entries(_collections.value)
|
||||||
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
||||||
.map(([_, collectionObj]) => collectionObj)
|
.map(([_, collectionObj]) => collectionObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearService(provider: string, service: string | number): void {
|
function clearService(target: ServiceIdentifier): void {
|
||||||
const serviceKeyPrefix = `${provider}:${service}:`
|
const serviceKeyPrefix = `${target}:`
|
||||||
Object.keys(_collections.value)
|
Object.keys(_collections.value)
|
||||||
.filter((key) => key.startsWith(serviceKeyPrefix))
|
.filter((key) => key.startsWith(serviceKeyPrefix))
|
||||||
.forEach((key) => {
|
.forEach((key) => {
|
||||||
@@ -70,6 +70,27 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
_collections.value = {}
|
_collections.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge externally-hydrated collections into the store (used by paginated node fetches)
|
||||||
|
*/
|
||||||
|
function mergeMany(collections: CollectionObject[]): void {
|
||||||
|
const hydrated: Record<string, CollectionObject> = {}
|
||||||
|
collections.forEach((collectionObj) => {
|
||||||
|
const key = identifierKey(collectionObj.provider, collectionObj.service, collectionObj.identifier)
|
||||||
|
hydrated[key] = collectionObj
|
||||||
|
})
|
||||||
|
_collections.value = { ..._collections.value, ...hydrated }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove specific collections by their own identifiers (used to evict a stale page before a reset)
|
||||||
|
*/
|
||||||
|
function removeMany(targets: CollectionIdentifier[]): void {
|
||||||
|
targets.forEach((target) => {
|
||||||
|
delete _collections.value[target]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort): Promise<Record<string, CollectionObject>> {
|
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort): Promise<Record<string, CollectionObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
@@ -98,14 +119,11 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetch(provider: string, service: string | number, identifier: string | number): Promise<CollectionObject> {
|
async function fetch(targets: CollectionIdentifier[]): Promise<Record<string, CollectionObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.fetch({ targets: [`${provider}:${service}:${identifier}`] })
|
const response = await collectionService.fetch({ targets })
|
||||||
const key = identifierKey(response.provider, response.service, response.identifier)
|
_collections.value = { ..._collections.value, ...response }
|
||||||
_collections.value[key] = response
|
|
||||||
|
|
||||||
console.debug('[Documents Manager][Store] - Successfully fetched collection:', key)
|
|
||||||
return response
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to fetch collection:', error)
|
console.error('[Documents Manager][Store] - Failed to fetch collection:', error)
|
||||||
@@ -130,17 +148,12 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function create(
|
async function create(
|
||||||
provider: string,
|
target: ServiceIdentifier | CollectionIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number | null,
|
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const target: CollectionIdentifier | undefined = collection !== null && collection !== undefined && collection !== ''
|
const response = await collectionService.create({ target, properties })
|
||||||
? `${provider}:${service}:${collection}`
|
|
||||||
: undefined
|
|
||||||
const response = await collectionService.create({ provider, service: String(service), target, properties })
|
|
||||||
const key = identifierKey(response.provider, response.service, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.identifier)
|
||||||
_collections.value[key] = response
|
_collections.value[key] = response
|
||||||
|
|
||||||
@@ -155,14 +168,12 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function update(
|
async function update(
|
||||||
provider: string,
|
target: CollectionIdentifier,
|
||||||
service: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.update({ target: `${provider}:${service}:${identifier}`, properties })
|
const response = await collectionService.update({ target, properties })
|
||||||
const key = identifierKey(response.provider, response.service, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.identifier)
|
||||||
_collections.value[key] = response
|
_collections.value[key] = response
|
||||||
|
|
||||||
@@ -176,17 +187,16 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(provider: string, service: string | number, identifier: string | number): Promise<{ success: boolean }> {
|
async function remove(target: CollectionIdentifier): Promise<{ success: boolean }> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.delete({ target: `${provider}:${service}:${identifier}` })
|
const response = await collectionService.delete({ target })
|
||||||
const success = response.disposition === 'deleted' || response.disposition === 'moved'
|
const success = response.disposition === 'deleted' || response.disposition === 'moved'
|
||||||
if (success) {
|
if (success) {
|
||||||
const key = identifierKey(provider, service, identifier)
|
delete _collections.value[target]
|
||||||
delete _collections.value[key]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('[Documents Manager][Store] - Successfully deleted collection:', `${provider}:${service}:${identifier}`)
|
console.debug('[Documents Manager][Store] - Successfully deleted collection:', target)
|
||||||
return { success }
|
return { success }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to delete collection:', error)
|
console.error('[Documents Manager][Store] - Failed to delete collection:', error)
|
||||||
@@ -206,6 +216,8 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
collectionsForService,
|
collectionsForService,
|
||||||
clearService,
|
clearService,
|
||||||
clearAll,
|
clearAll,
|
||||||
|
mergeMany,
|
||||||
|
removeMany,
|
||||||
list,
|
list,
|
||||||
fetch,
|
fetch,
|
||||||
extant,
|
extant,
|
||||||
|
|||||||
+48
-62
@@ -49,37 +49,34 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function entity(
|
function entity(
|
||||||
provider: string,
|
target: EntityIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
retrieve: boolean = false,
|
retrieve: boolean = false,
|
||||||
): EntityObject | null {
|
): EntityObject | null {
|
||||||
const key = identifierKey(provider, service, collection, identifier)
|
const key = target
|
||||||
if (retrieve === true && !_entities.value[key]) {
|
if (retrieve === true && !_entities.value[key]) {
|
||||||
console.debug(`[Documents Manager][Store] - Force fetching entity "${key}"`)
|
console.debug(`[Documents Manager][Store] - Force fetching entity "${key}"`)
|
||||||
fetch(provider, service, collection, [identifier])
|
void fetch([target])
|
||||||
}
|
}
|
||||||
|
|
||||||
return _entities.value[key] || null
|
return _entities.value[key] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
function entitiesForService(provider: string, service: string | number): EntityObject[] {
|
function entitiesForService(target: ServiceIdentifier): EntityObject[] {
|
||||||
const serviceKeyPrefix = `${provider}:${service}:`
|
const serviceKeyPrefix = `${target}:`
|
||||||
return Object.entries(_entities.value)
|
return Object.entries(_entities.value)
|
||||||
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
||||||
.map(([_, entityObj]) => entityObj)
|
.map(([_, entityObj]) => entityObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function entitiesForCollection(provider: string, service: string | number, collection: string | number): EntityObject[] {
|
function entitiesForCollection(target: CollectionIdentifier): EntityObject[] {
|
||||||
const collectionKeyPrefix = `${provider}:${service}:${collection}:`
|
const collectionKeyPrefix = `${target}:`
|
||||||
return Object.entries(_entities.value)
|
return Object.entries(_entities.value)
|
||||||
.filter(([key]) => key.startsWith(collectionKeyPrefix))
|
.filter(([key]) => key.startsWith(collectionKeyPrefix))
|
||||||
.map(([_, entityObj]) => entityObj)
|
.map(([_, entityObj]) => entityObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearService(provider: string, service: string | number): void {
|
function clearService(target: ServiceIdentifier): void {
|
||||||
const serviceKeyPrefix = `${provider}:${service}:`
|
const serviceKeyPrefix = `${target}:`
|
||||||
Object.keys(_entities.value)
|
Object.keys(_entities.value)
|
||||||
.filter((key) => key.startsWith(serviceKeyPrefix))
|
.filter((key) => key.startsWith(serviceKeyPrefix))
|
||||||
.forEach((key) => {
|
.forEach((key) => {
|
||||||
@@ -87,8 +84,8 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearCollection(provider: string, service: string | number, collection: string | number): void {
|
function clearCollection(target: CollectionIdentifier): void {
|
||||||
const collectionKeyPrefix = `${provider}:${service}:${collection}:`
|
const collectionKeyPrefix = `${target}:`
|
||||||
Object.keys(_entities.value)
|
Object.keys(_entities.value)
|
||||||
.filter((key) => key.startsWith(collectionKeyPrefix))
|
.filter((key) => key.startsWith(collectionKeyPrefix))
|
||||||
.forEach((key) => {
|
.forEach((key) => {
|
||||||
@@ -100,6 +97,18 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
_entities.value = {}
|
_entities.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge externally-hydrated entities into the store (used by paginated node fetches)
|
||||||
|
*/
|
||||||
|
function mergeMany(entities: EntityObject[]): void {
|
||||||
|
const hydrated: Record<string, EntityObject> = {}
|
||||||
|
entities.forEach((entityObj) => {
|
||||||
|
const key = identifierKey(entityObj.provider, entityObj.service, entityObj.collection, entityObj.identifier)
|
||||||
|
hydrated[key] = entityObj
|
||||||
|
})
|
||||||
|
_entities.value = { ..._entities.value, ...hydrated }
|
||||||
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise<Record<string, EntityObject>> {
|
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise<Record<string, EntityObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
@@ -131,25 +140,13 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetch(
|
async function fetch(
|
||||||
provider: string,
|
targets: EntityIdentifier[],
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
identifiers: (string | number)[],
|
|
||||||
): Promise<Record<string, EntityObject>> {
|
): Promise<Record<string, EntityObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const targets: EntityIdentifier[] = identifiers.map((identifier) => `${provider}:${service}:${collection}:${identifier}` as EntityIdentifier)
|
|
||||||
const response = await entityService.fetch({ targets })
|
const response = await entityService.fetch({ targets })
|
||||||
|
_entities.value = { ..._entities.value, ...response }
|
||||||
const hydrated: Record<string, EntityObject> = {}
|
return response
|
||||||
Object.entries(response).forEach(([identifier, entityObj]) => {
|
|
||||||
const key = identifierKey(provider, service, collection, identifier)
|
|
||||||
hydrated[key] = entityObj
|
|
||||||
_entities.value[key] = entityObj
|
|
||||||
})
|
|
||||||
|
|
||||||
console.debug('[Documents Manager][Store] - Successfully fetched', Object.keys(hydrated).length, 'entities')
|
|
||||||
return hydrated
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to fetch entities:', error)
|
console.error('[Documents Manager][Store] - Failed to fetch entities:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -173,15 +170,13 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function create(
|
async function create(
|
||||||
provider: string,
|
target: CollectionIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
options?: Record<string, unknown>,
|
options?: Record<string, unknown>,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.create({ target: `${provider}:${service}:${collection}`, properties, options })
|
const response = await entityService.create({ target, properties, options })
|
||||||
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
||||||
_entities.value[key] = response
|
_entities.value[key] = response
|
||||||
|
|
||||||
@@ -196,15 +191,12 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function update(
|
async function update(
|
||||||
provider: string,
|
target: EntityIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.update({ target: `${provider}:${service}:${collection}:${identifier}`, properties })
|
const response = await entityService.update({ target, properties })
|
||||||
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
||||||
_entities.value[key] = response
|
_entities.value[key] = response
|
||||||
|
|
||||||
@@ -219,23 +211,22 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function remove(
|
async function remove(
|
||||||
provider: string,
|
targets: EntityIdentifier[],
|
||||||
service: string | number,
|
): Promise<{ successes: EntityIdentifier[]; failures: EntityIdentifier[] }> {
|
||||||
collection: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
): Promise<{ success: boolean }> {
|
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const target: EntityIdentifier = `${provider}:${service}:${collection}:${identifier}`
|
const response = await entityService.delete({ targets })
|
||||||
const response = await entityService.delete({ targets: [target] })
|
const successes: EntityIdentifier[] = []
|
||||||
const success = response[target]?.disposition === 'deleted'
|
const failures: EntityIdentifier[] = []
|
||||||
if (success) {
|
for (const target of targets) {
|
||||||
const key = identifierKey(provider, service, collection, identifier)
|
if (response[target]?.disposition === 'deleted') {
|
||||||
delete _entities.value[key]
|
delete _entities.value[target]
|
||||||
|
successes.push(target)
|
||||||
|
} else {
|
||||||
|
failures.push(target)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return { successes, failures }
|
||||||
console.debug('[Documents Manager][Store] - Successfully deleted entity:', target)
|
|
||||||
return { success }
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to delete entity:', error)
|
console.error('[Documents Manager][Store] - Failed to delete entity:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -277,14 +268,11 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function read(
|
async function read(
|
||||||
provider: string,
|
target: EntityIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.read({ target: `${provider}:${service}:${collection}:${identifier}` })
|
const response = await entityService.read({ target })
|
||||||
return response.content
|
return response.content
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to read entity:', error)
|
console.error('[Documents Manager][Store] - Failed to read entity:', error)
|
||||||
@@ -295,15 +283,12 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function write(
|
async function write(
|
||||||
provider: string,
|
target: EntityIdentifier,
|
||||||
service: string | number,
|
|
||||||
collection: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
content: string,
|
content: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.write({ target: `${provider}:${service}:${collection}:${identifier}`, content, encoding: 'base64' })
|
const response = await entityService.write({ target, content, encoding: 'base64' })
|
||||||
return response.bytesWritten
|
return response.bytesWritten
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to write entity:', error)
|
console.error('[Documents Manager][Store] - Failed to write entity:', error)
|
||||||
@@ -325,6 +310,7 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
clearService,
|
clearService,
|
||||||
clearCollection,
|
clearCollection,
|
||||||
clearAll,
|
clearAll,
|
||||||
|
mergeMany,
|
||||||
list,
|
list,
|
||||||
fetch,
|
fetch,
|
||||||
extant,
|
extant,
|
||||||
|
|||||||
+76
-68
@@ -6,6 +6,7 @@ import { computed, ref, readonly } from 'vue'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type {
|
import type {
|
||||||
CollectionIdentifier,
|
CollectionIdentifier,
|
||||||
|
EntityIdentifier,
|
||||||
ServiceIdentifier,
|
ServiceIdentifier,
|
||||||
ListFilter,
|
ListFilter,
|
||||||
ListSort,
|
ListSort,
|
||||||
@@ -16,6 +17,7 @@ import type {
|
|||||||
import { CollectionObject, EntityObject } from '../models'
|
import { CollectionObject, EntityObject } from '../models'
|
||||||
import { useCollectionsStore } from './collectionsStore'
|
import { useCollectionsStore } from './collectionsStore'
|
||||||
import { useEntitiesStore } from './entitiesStore'
|
import { useEntitiesStore } from './entitiesStore'
|
||||||
|
import { nodeService } from '../services/nodeService'
|
||||||
|
|
||||||
export const ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
export const ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
||||||
|
|
||||||
@@ -46,8 +48,8 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
|
|
||||||
function getServiceNodes(providerId: string, serviceId: string | number): NodeRecord[] {
|
function getServiceNodes(providerId: string, serviceId: string | number): NodeRecord[] {
|
||||||
return [
|
return [
|
||||||
...collectionsStore.collectionsForService(providerId, serviceId),
|
...collectionsStore.collectionsForService(`${providerId}:${serviceId}`),
|
||||||
...entitiesStore.entitiesForService(providerId, serviceId),
|
...entitiesStore.entitiesForService(`${providerId}:${serviceId}`),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,20 +107,13 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCollections(
|
async function fetchCollections(
|
||||||
providerId: string,
|
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number | null,
|
|
||||||
filter?: ListFilter,
|
filter?: ListFilter,
|
||||||
sort?: ListSort,
|
sort?: ListSort,
|
||||||
): Promise<CollectionObject[]> {
|
): Promise<CollectionObject[]> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const sources: (ServiceIdentifier | CollectionIdentifier)[] = collectionId === null
|
return Object.values(await collectionsStore.list(sources, filter, sort))
|
||||||
? [`${providerId}:${serviceId}`]
|
|
||||||
: [`${providerId}:${serviceId}:${collectionId}`]
|
|
||||||
|
|
||||||
await collectionsStore.list(sources, filter, sort)
|
|
||||||
return collectionsStore.collectionsForService(providerId, serviceId)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to fetch collections'
|
error.value = e instanceof Error ? e.message : 'Failed to fetch collections'
|
||||||
throw e
|
throw e
|
||||||
@@ -126,43 +121,76 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchEntities(
|
async function fetchEntities(
|
||||||
providerId: string,
|
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number | null,
|
|
||||||
filter?: ListFilter,
|
filter?: ListFilter,
|
||||||
sort?: ListSort,
|
sort?: ListSort,
|
||||||
range?: ListRange,
|
range?: ListRange,
|
||||||
): Promise<EntityObject[]> {
|
): Promise<EntityObject[]> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const sources: (ServiceIdentifier | CollectionIdentifier)[] = collectionId === null
|
return Object.values(await entitiesStore.list(sources, filter, sort, range))
|
||||||
? [`${providerId}:${serviceId}`]
|
|
||||||
: [`${providerId}:${serviceId}:${collectionId}`]
|
|
||||||
|
|
||||||
await entitiesStore.list(sources, filter, sort, range)
|
|
||||||
return entitiesStore.entitiesForCollection(providerId, serviceId, collectionId)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to fetch entities'
|
error.value = e instanceof Error ? e.message : 'Failed to fetch entities'
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a unified node listing, optionally limited to a page by range.
|
||||||
|
*/
|
||||||
async function fetchNodes(
|
async function fetchNodes(
|
||||||
providerId: string,
|
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
||||||
serviceId: string | number,
|
|
||||||
parentId: string | number | null = ROOT_ID,
|
|
||||||
filter?: ListFilter,
|
filter?: ListFilter,
|
||||||
sort?: ListSort,
|
sort?: ListSort,
|
||||||
range?: ListRange,
|
range?: ListRange,
|
||||||
): Promise<NodeRecord[]> {
|
mode: 'reset' | 'append' = 'reset',
|
||||||
|
): Promise<{ collections: CollectionObject[]; entities: EntityObject[]; isLastPage: boolean }> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
const result = await nodeService.list({ sources, filter, sort, range })
|
||||||
fetchCollections(providerId, serviceId, parentId, filter, sort),
|
|
||||||
fetchEntities(providerId, serviceId, parentId, filter, sort, range),
|
|
||||||
])
|
|
||||||
|
|
||||||
return getChildren(providerId, serviceId, parentId)
|
// Service selectors list the root; collection selectors list that folder.
|
||||||
|
// Omitted sources list roots across services, not every cached descendant.
|
||||||
|
const locations = new Map<string, { provider: string; service: string; parent: string }>()
|
||||||
|
if (sources?.length) {
|
||||||
|
for (const source of sources) {
|
||||||
|
const [provider, service, parent] = source.split(':')
|
||||||
|
const location = { provider: provider!, service: service!, parent: isRoot(parent) ? ROOT_ID : parent! }
|
||||||
|
locations.set(`${location.provider}:${location.service}:${location.parent}`, location)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const node of [...nodeList.value, ...result]) {
|
||||||
|
const location = { provider: node.provider, service: String(node.service), parent: ROOT_ID }
|
||||||
|
locations.set(`${location.provider}:${location.service}:${ROOT_ID}`, location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace only the selected locations, and only after the stream succeeds.
|
||||||
|
if (mode === 'reset') {
|
||||||
|
for (const { provider, service, parent } of locations.values()) {
|
||||||
|
const staleIds = getChildCollections(provider, service, parent)
|
||||||
|
.map((node): CollectionIdentifier => `${node.provider}:${node.service}:${node.identifier}`)
|
||||||
|
collectionsStore.removeMany(staleIds)
|
||||||
|
entitiesStore.clearCollection(`${provider}:${service}:${parent}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageCollections = result.filter((node): node is CollectionObject => node instanceof CollectionObject)
|
||||||
|
const pageEntities = result.filter((node): node is EntityObject => node instanceof EntityObject)
|
||||||
|
|
||||||
|
collectionsStore.mergeMany(pageCollections)
|
||||||
|
entitiesStore.mergeMany(pageEntities)
|
||||||
|
|
||||||
|
// The backend applies the range independently to each selected location.
|
||||||
|
const counts = new Map<string, number>()
|
||||||
|
for (const node of result) {
|
||||||
|
const parent = isRoot(node.collection) ? ROOT_ID : String(node.collection)
|
||||||
|
const key = `${node.provider}:${node.service}:${parent}`
|
||||||
|
counts.set(key, (counts.get(key) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
const isLastPage = range === undefined || [...counts.values()].every(count => count < range.tally)
|
||||||
|
|
||||||
|
return { collections: pageCollections, entities: pageEntities, isLastPage }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to fetch nodes'
|
error.value = e instanceof Error ? e.message : 'Failed to fetch nodes'
|
||||||
throw e
|
throw e
|
||||||
@@ -170,14 +198,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createCollection(
|
async function createCollection(
|
||||||
providerId: string,
|
target: ServiceIdentifier | CollectionIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
parentCollectionId: string | number | null,
|
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await collectionsStore.create(providerId, serviceId, parentCollectionId, properties)
|
return await collectionsStore.create(target, properties)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to create collection'
|
error.value = e instanceof Error ? e.message : 'Failed to create collection'
|
||||||
throw e
|
throw e
|
||||||
@@ -185,14 +211,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateCollection(
|
async function updateCollection(
|
||||||
providerId: string,
|
target: CollectionIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await collectionsStore.update(providerId, serviceId, identifier, properties)
|
return await collectionsStore.update(target, properties)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to update collection'
|
error.value = e instanceof Error ? e.message : 'Failed to update collection'
|
||||||
throw e
|
throw e
|
||||||
@@ -200,13 +224,11 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCollection(
|
async function deleteCollection(
|
||||||
providerId: string,
|
target: CollectionIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const response = await collectionsStore.delete(providerId, serviceId, identifier)
|
const response = await collectionsStore.delete(target)
|
||||||
return response.success
|
return response.success
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to delete collection'
|
error.value = e instanceof Error ? e.message : 'Failed to delete collection'
|
||||||
@@ -215,15 +237,13 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createEntity(
|
async function createEntity(
|
||||||
providerId: string,
|
target: CollectionIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number,
|
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
options?: Record<string, unknown>,
|
options?: Record<string, unknown>,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.create(providerId, serviceId, collectionId, properties, options)
|
return await entitiesStore.create(target, properties, options)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to create entity'
|
error.value = e instanceof Error ? e.message : 'Failed to create entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -231,15 +251,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateEntity(
|
async function updateEntity(
|
||||||
providerId: string,
|
target: EntityIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.update(providerId, serviceId, collectionId, identifier, properties)
|
return await entitiesStore.update(target, properties)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to update entity'
|
error.value = e instanceof Error ? e.message : 'Failed to update entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -247,15 +264,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteEntity(
|
async function deleteEntity(
|
||||||
providerId: string,
|
target: EntityIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const response = await entitiesStore.delete(providerId, serviceId, collectionId, identifier)
|
const response = await entitiesStore.delete([target])
|
||||||
return response.success
|
return response.successes.includes(target)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to delete entity'
|
error.value = e instanceof Error ? e.message : 'Failed to delete entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -263,14 +277,11 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function readEntity(
|
async function readEntity(
|
||||||
providerId: string,
|
target: EntityIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.read(providerId, serviceId, collectionId, identifier)
|
return await entitiesStore.read(target)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to read entity'
|
error.value = e instanceof Error ? e.message : 'Failed to read entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -278,15 +289,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function writeEntity(
|
async function writeEntity(
|
||||||
providerId: string,
|
target: EntityIdentifier,
|
||||||
serviceId: string | number,
|
|
||||||
collectionId: string | number,
|
|
||||||
identifier: string | number,
|
|
||||||
content: string,
|
content: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.write(providerId, serviceId, collectionId, identifier, content)
|
return await entitiesStore.write(target, content)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to write entity'
|
error.value = e instanceof Error ? e.message : 'Failed to write entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -294,8 +302,8 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearServiceNodes(providerId: string, serviceId: string | number): void {
|
function clearServiceNodes(providerId: string, serviceId: string | number): void {
|
||||||
collectionsStore.clearService(providerId, serviceId)
|
collectionsStore.clearService(`${providerId}:${serviceId}`)
|
||||||
entitiesStore.clearService(providerId, serviceId)
|
entitiesStore.clearService(`${providerId}:${serviceId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearNodes(): void {
|
function clearNodes(): void {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export interface CollectionFetchRequest {
|
|||||||
targets: CollectionIdentifier[];
|
targets: CollectionIdentifier[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionFetchResponse extends CollectionInterface {}
|
export type CollectionFetchResponse = Record<CollectionIdentifier, CollectionInterface>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection extant
|
* Collection extant
|
||||||
@@ -87,9 +87,7 @@ export interface CollectionExtantResponse {
|
|||||||
* Collection create
|
* Collection create
|
||||||
*/
|
*/
|
||||||
export interface CollectionCreateRequest {
|
export interface CollectionCreateRequest {
|
||||||
provider: string;
|
target: ServiceIdentifier | CollectionIdentifier; // Service root or parent collection
|
||||||
service: string | number;
|
|
||||||
target?: CollectionIdentifier | null; // Parent collection identifier (absent for root)
|
|
||||||
properties: CollectionMutableProperties;
|
properties: CollectionMutableProperties;
|
||||||
options?: Record<string, unknown>;
|
options?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
@@ -139,4 +137,4 @@ export interface CollectionMoveRequest {
|
|||||||
target?: CollectionIdentifier | null; // Destination parent (absent for root)
|
target?: CollectionIdentifier | null; // Destination parent (absent for root)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionMoveResponse extends CollectionInterface {}
|
export interface CollectionMoveResponse extends CollectionInterface {}
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export type * from './provider';
|
|||||||
export type * from './service';
|
export type * from './service';
|
||||||
export type * from './collection';
|
export type * from './collection';
|
||||||
export type * from './entity';
|
export type * from './entity';
|
||||||
|
export type * from './node';
|
||||||
export type * from './document';
|
export type * from './document';
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Node (unified collections + entities) type definitions
|
||||||
|
*/
|
||||||
|
import type { CollectionIdentifier, ListFilter, ListRange, ListSort, ServiceIdentifier } from './common';
|
||||||
|
import type { CollectionInterface } from './collection';
|
||||||
|
import type { EntityInterface } from './entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node list
|
||||||
|
*/
|
||||||
|
export interface NodeListRequest {
|
||||||
|
sources?: (ServiceIdentifier | CollectionIdentifier)[];
|
||||||
|
filter?: ListFilter;
|
||||||
|
sort?: ListSort;
|
||||||
|
range?: ListRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeInterface =
|
||||||
|
| (Omit<CollectionInterface, '@type'> & { '@type': 'document:collection' })
|
||||||
|
| (Omit<EntityInterface, '@type'> & { '@type': 'document:entity' });
|
||||||
|
|
||||||
|
/** Payload of each data frame in the node.list NDJSON stream. */
|
||||||
|
export type NodeListResponse = NodeInterface;
|
||||||
Reference in New Issue
Block a user