/** * Nodes Store (thin wrapper over collections/entities stores) */ import { computed, ref, readonly } from 'vue' import { defineStore } from 'pinia' import type { CollectionIdentifier, EntityIdentifier, ServiceIdentifier, ListFilter, ListSort, ListRange, CollectionMutableProperties, DocumentInterface, } from '../types' import { CollectionObject, EntityObject } from '../models' import { useCollectionsStore } from './collectionsStore' import { useEntitiesStore } from './entitiesStore' import { nodeService } from '../services/nodeService' export const ROOT_ID = '00000000-0000-0000-0000-000000000000' type NodeRecord = CollectionObject | EntityObject export const useNodesStore = defineStore('documentsNodesStore', () => { const collectionsStore = useCollectionsStore() const entitiesStore = useEntitiesStore() const error = ref(null) const transceiving = computed(() => collectionsStore.transceiving || entitiesStore.transceiving) const nodeList = computed(() => { return [...collectionsStore.collections, ...entitiesStore.entities] }) const collectionList = computed(() => collectionsStore.collections) const entityList = computed(() => entitiesStore.entities) function toId(value: string | number | null | undefined): string | null { if (value === null || value === undefined || value === '') { return null } return String(value) } function getServiceNodes(providerId: string, serviceId: string | number): NodeRecord[] { return [ ...collectionsStore.collectionsForService(`${providerId}:${serviceId}`), ...entitiesStore.entitiesForService(`${providerId}:${serviceId}`), ] } function getNode(providerId: string, serviceId: string | number, nodeId: string | number): NodeRecord | undefined { const targetId = String(nodeId) return getServiceNodes(providerId, serviceId) .find((node) => String(node.identifier) === targetId) } function isRoot(nodeId: string | number | null | undefined): boolean { const normalized = toId(nodeId) return normalized === null || normalized === ROOT_ID } function getChildren(providerId: string, serviceId: string | number, parentId: string | number | null): NodeRecord[] { const serviceNodes = getServiceNodes(providerId, serviceId) if (isRoot(parentId)) { return serviceNodes.filter((node) => { const parent = toId(node.collection) return parent === null || parent === ROOT_ID }) } const targetParent = String(parentId) return serviceNodes.filter((node) => toId(node.collection) === targetParent) } function getChildCollections(providerId: string, serviceId: string | number, parentId: string | number | null): CollectionObject[] { return getChildren(providerId, serviceId, parentId) .filter((node): node is CollectionObject => node instanceof CollectionObject) } function getChildEntities(providerId: string, serviceId: string | number, parentId: string | number | null): EntityObject[] { return getChildren(providerId, serviceId, parentId) .filter((node): node is EntityObject => node instanceof EntityObject) } function getPath(providerId: string, serviceId: string | number, nodeId: string | number): NodeRecord[] { const path: NodeRecord[] = [] let current = getNode(providerId, serviceId, nodeId) while (current) { path.unshift(current) const parentId = toId(current.collection) if (parentId === null || parentId === ROOT_ID) { break } current = getNode(providerId, serviceId, parentId) } return path } async function fetchCollections( sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort, ): Promise { error.value = null try { return Object.values(await collectionsStore.list(sources, filter, sort)) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to fetch collections' throw e } } async function fetchEntities( sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort, range?: ListRange, ): Promise { error.value = null try { return Object.values(await entitiesStore.list(sources, filter, sort, range)) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to fetch entities' throw e } } /** * Fetch a unified node listing, optionally limited to a page by range. */ async function fetchNodes( sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort, range?: ListRange, mode: 'reset' | 'append' = 'reset', ): Promise<{ collections: CollectionObject[]; entities: EntityObject[]; isLastPage: boolean }> { error.value = null try { const result = await nodeService.list({ sources, filter, sort, range }) // Service selectors list the root; collection selectors list that folder. // Omitted sources list roots across services, not every cached descendant. const locations = new Map() 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() 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) { error.value = e instanceof Error ? e.message : 'Failed to fetch nodes' throw e } } async function createCollection( target: ServiceIdentifier | CollectionIdentifier, properties: CollectionMutableProperties, ): Promise { error.value = null try { return await collectionsStore.create(target, properties) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to create collection' throw e } } async function updateCollection( target: CollectionIdentifier, properties: CollectionMutableProperties, ): Promise { error.value = null try { return await collectionsStore.update(target, properties) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to update collection' throw e } } async function deleteCollection( target: CollectionIdentifier, ): Promise { error.value = null try { const response = await collectionsStore.delete(target) return response.success } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to delete collection' throw e } } async function createEntity( target: CollectionIdentifier, properties: DocumentInterface, options?: Record, ): Promise { error.value = null try { return await entitiesStore.create(target, properties, options) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to create entity' throw e } } async function updateEntity( target: EntityIdentifier, properties: DocumentInterface, ): Promise { error.value = null try { return await entitiesStore.update(target, properties) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to update entity' throw e } } async function deleteEntity( target: EntityIdentifier, ): Promise { error.value = null try { const response = await entitiesStore.delete([target]) return response.successes.includes(target) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to delete entity' throw e } } async function readEntity( target: EntityIdentifier, ): Promise { error.value = null try { return await entitiesStore.read(target) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to read entity' throw e } } async function writeEntity( target: EntityIdentifier, content: string, ): Promise { error.value = null try { return await entitiesStore.write(target, content) } catch (e) { error.value = e instanceof Error ? e.message : 'Failed to write entity' throw e } } function clearServiceNodes(providerId: string, serviceId: string | number): void { collectionsStore.clearService(`${providerId}:${serviceId}`) entitiesStore.clearService(`${providerId}:${serviceId}`) } function clearNodes(): void { collectionsStore.clearAll() entitiesStore.clearAll() } return { transceiving: readonly(transceiving), error: readonly(error), ROOT_ID, nodeList, collectionList, entityList, getNode, getServiceNodes, getChildren, getChildCollections, getChildEntities, getPath, isRoot, fetchNodes, fetchCollections, fetchEntities, createCollection, updateCollection, deleteCollection, createEntity, updateEntity, deleteEntity, readEntity, writeEntity, clearServiceNodes, clearNodes, } })