/** * Collections Store */ import { ref, computed, readonly } from 'vue' import { defineStore } from 'pinia' import { collectionService } from '../services' import { CollectionObject, CollectionPropertiesObject } from '../models/collection' import type { SourceSelector, ListFilter, ListSort } from '../types' export const useCollectionsStore = defineStore('mailCollectionsStore', () => { const ROOT_IDENTIFIER = '__root__' const SERVICE_INDEX_IDENTIFIER = '__service__' // State const _collections = ref>({}) const _collectionsByServiceIndex = ref>({}) const _collectionsByParentIndex = ref>({}) const transceiving = ref(false) /** * Get count of collections in store */ const count = computed(() => Object.keys(_collections.value).length) /** * Check if any collections are present in store */ const has = computed(() => count.value > 0) /** * Get all collections present in store */ const collections = computed(() => Object.values(_collections.value)) /** * Get all collections present in store grouped by service */ const collectionsByService = computed(() => { const groups: Record = {} Object.keys(_collectionsByServiceIndex.value).forEach(serviceIndexKey => { const collectionKeys = _collectionsByServiceIndex.value[serviceIndexKey] ?? [] const collectionsForKey = collectionKeys .map(collectionKey => _collections.value[collectionKey]) .filter((collection): collection is CollectionObject => collection !== undefined) if (collectionsForKey.length === 0) { return } const firstCollection = collectionsForKey[0] const serviceKey = `${firstCollection.provider}:${firstCollection.service}` groups[serviceKey] = collectionsForKey }) return groups }) /** * Get a specific collection from store, with optional retrieval * * @param provider - provider identifier * @param service - service identifier * @param identifier - collection identifier * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only * * @returns Collection object or null */ function collection(provider: string, service: string | number, identifier: string | number, retrieve: boolean = false): CollectionObject | null { const key = identifierKey(provider, service, identifier) if (retrieve === true && !_collections.value[key]) { console.debug(`[Mail Manager][Store] - Force fetching collection "${key}"`) fetch(provider, service, identifier) } return _collections.value[key] || null } /** * Get all collections for a specific service * * @param provider - provider identifier * @param service - service identifier * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only * * @returns Array of collection objects */ function collectionsForService(provider: string, service: string | number, retrieve: boolean = false): CollectionObject[] { const serviceCollections = collectionObjectsForKeys( _collectionsByServiceIndex.value[identifierKey(provider, service, SERVICE_INDEX_IDENTIFIER)] ?? [], ) if (retrieve === true && serviceCollections.length === 0) { console.debug(`[Mail Manager][Store] - Force fetching collections for service "${provider}:${service}"`) const sources: SourceSelector = { [provider]: { [String(service)]: true } } list(sources) } return serviceCollections } /** * Get direct child collections for a parent collection, or root collections when parent is null. * * @param provider - provider identifier * @param service - service identifier * @param collectionId - parent collection identifier, or null for root-level collections * @param retrieve - Retrieve behavior: true = fetch service collections if missing, false = cache only * * @returns Array of direct child collection objects */ function collectionsInCollection(provider: string, service: string | number, collectionId: string | number | null, retrieve: boolean = false): CollectionObject[] { const nestedCollections = collectionObjectsForKeys( _collectionsByParentIndex.value[identifierKey(provider, service, collectionId)] ?? [], ) if (retrieve === true && nestedCollections.length === 0) { console.debug(`[Mail Manager][Store] - Force fetching collections in collection "${provider}:${service}:${collectionId}"`) const sources: SourceSelector = { [provider]: { [String(service)]: true } } list(sources) } return nestedCollections } function hasChildrenInCollection(provider: string, service: string | number, collectionId: string | number | null): boolean { return (_collectionsByParentIndex.value[identifierKey(provider, service, collectionId)]?.length ?? 0) > 0 } /** * Create unique key for a collection */ function identifierKey(provider: string, service: string | number | null, identifier: string | number | null): string { return `${provider}:${String(service ?? ROOT_IDENTIFIER)}:${String(identifier ?? ROOT_IDENTIFIER)}` } function collectionObjectsForKeys(collectionKeys: string[]): CollectionObject[] { return collectionKeys .map(collectionKey => _collections.value[collectionKey]) .filter((collection): collection is CollectionObject => collection !== undefined) } function addIndexEntry(index: Record, indexKey: string, collectionKey: string) { const existing = index[indexKey] ?? [] if (existing.includes(collectionKey)) { return } index[indexKey] = [...existing, collectionKey] } function removeIndexEntry(index: Record, indexKey: string, collectionKey: string) { const existing = index[indexKey] if (!existing) { return } const filtered = existing.filter(existingKey => existingKey !== collectionKey) if (filtered.length === 0) { delete index[indexKey] return } index[indexKey] = filtered } function indexCollection(collection: CollectionObject) { const collectionKey = identifierKey(collection.provider, collection.service, collection.identifier) const serviceIndexKey = identifierKey(collection.provider, collection.service, SERVICE_INDEX_IDENTIFIER) const parentIndexKey = identifierKey(collection.provider, collection.service, collection.collection) addIndexEntry(_collectionsByServiceIndex.value, serviceIndexKey, collectionKey) addIndexEntry(_collectionsByParentIndex.value, parentIndexKey, collectionKey) } function deindexCollection(collection: CollectionObject) { const collectionKey = identifierKey(collection.provider, collection.service, collection.identifier) const serviceIndexKey = identifierKey(collection.provider, collection.service, SERVICE_INDEX_IDENTIFIER) const parentIndexKey = identifierKey(collection.provider, collection.service, collection.collection) removeIndexEntry(_collectionsByServiceIndex.value, serviceIndexKey, collectionKey) removeIndexEntry(_collectionsByParentIndex.value, parentIndexKey, collectionKey) } // Actions /** * Retrieve all or specific collections, optionally filtered by source selector * * @param sources - optional source selector * @param filter - optional list filter * @param sort - optional list sort * * @returns Promise with collection object list keyed by provider, service, and collection identifier */ async function list(sources?: SourceSelector, filter?: ListFilter, sort?: ListSort): Promise> { transceiving.value = true try { const response = await collectionService.list({ sources, filter, sort }) // Flatten nested structure: provider:service:collection -> "provider:service:collection": object const collections: Record = {} Object.entries(response).forEach(([_providerId, providerServices]) => { Object.entries(providerServices).forEach(([_serviceId, serviceCollections]) => { Object.entries(serviceCollections).forEach(([_collectionId, collectionObj]) => { const key = identifierKey(collectionObj.provider, collectionObj.service, collectionObj.identifier) const previousCollection = _collections.value[key] if (previousCollection) { deindexCollection(previousCollection) } collections[key] = collectionObj }) }) }) // Merge retrieved collections into state _collections.value = { ..._collections.value, ...collections } Object.values(collections).forEach(collectionObj => { indexCollection(collectionObj) }) console.debug('[Mail Manager][Store] - Successfully retrieved', Object.keys(collections).length, 'collections') return collections } catch (error: any) { console.error('[Mail Manager][Store] - Failed to retrieve collections:', error) throw error } finally { transceiving.value = false } } /** * Retrieve a specific collection by provider, service, and identifier * * @param provider - provider identifier * @param service - service identifier * @param identifier - collection identifier * * @returns Promise with collection object */ async function fetch(provider: string, service: string | number, identifier: string | number): Promise { transceiving.value = true try { const response = await collectionService.fetch({ provider, service, collection: identifier }) // Merge fetched collection into state const key = identifierKey(response.provider, response.service, response.identifier) const previousCollection = _collections.value[key] if (previousCollection) { deindexCollection(previousCollection) } _collections.value[key] = response indexCollection(response) console.debug('[Mail Manager][Store] - Successfully fetched collection:', key) return response } catch (error: any) { console.error('[Mail Manager][Store] - Failed to fetch collection:', error) throw error } finally { transceiving.value = false } } /** * Retrieve collection availability status for a given source selector * * @param sources - source selector to check availability for * * @returns Promise with collection availability status */ async function extant(sources: SourceSelector) { transceiving.value = true try { const response = await collectionService.extant({ sources }) console.debug('[Mail Manager][Store] - Successfully checked', sources ? Object.keys(sources).length : 0, 'collections') return response } catch (error: any) { console.error('[Mail Manager][Store] - Failed to check collections:', error) throw error } finally { transceiving.value = false } } /** * Create a new collection with given provider, service, and data * * @param provider - provider identifier for the new collection * @param service - service identifier for the new collection * @param collection - optional parent collection identifier * @param data - collection properties for creation * * @returns Promise with created collection object */ async function create(provider: string, service: string | number, collection: string | number | null, data: CollectionPropertiesObject): Promise { transceiving.value = true try { const response = await collectionService.create({ provider, service, collection, properties: data }) // Merge created collection into state const key = identifierKey(response.provider, response.service, response.identifier) _collections.value[key] = response indexCollection(response) console.debug('[Mail Manager][Store] - Successfully created collection:', key) return response } catch (error: any) { console.error('[Mail Manager][Store] - Failed to create collection:', error) throw error } finally { transceiving.value = false } } /** * Update an existing collection with given provider, service, identifier, and data * * @param provider - provider identifier for the collection to update * @param service - service identifier for the collection to update * @param identifier - collection identifier for the collection to update * @param data - collection properties for update * * @returns Promise with updated collection object */ async function update(provider: string, service: string | number, identifier: string | number, data: CollectionPropertiesObject): Promise { transceiving.value = true try { const response = await collectionService.update({ provider, service, identifier, properties: data }) // Merge updated collection into state const key = identifierKey(response.provider, response.service, response.identifier) const previousCollection = _collections.value[key] if (previousCollection) { deindexCollection(previousCollection) } _collections.value[key] = response indexCollection(response) console.debug('[Mail Manager][Store] - Successfully updated collection:', key) return response } catch (error: any) { console.error('[Mail Manager][Store] - Failed to update collection:', error) throw error } finally { transceiving.value = false } } /** * Delete a collection by provider, service, and identifier * * @param provider - provider identifier for the collection to delete * @param service - service identifier for the collection to delete * @param identifier - collection identifier for the collection to delete * * @returns Promise with deletion result */ async function remove(provider: string, service: string | number, identifier: string | number): Promise { transceiving.value = true try { await collectionService.delete({ provider, service, identifier }) // Remove deleted collection from state const key = identifierKey(provider, service, identifier) const previousCollection = _collections.value[key] if (previousCollection) { deindexCollection(previousCollection) } delete _collections.value[key] console.debug('[Mail Manager][Store] - Successfully deleted collection:', key) } catch (error: any) { console.error('[Mail Manager][Store] - Failed to delete collection:', error) throw error } finally { transceiving.value = false } } // Return public API return { // State (readonly) transceiving: readonly(transceiving), // Getters count, has, collections, collectionsByService, collectionsForService, collectionsInCollection, hasChildrenInCollection, // Actions collection, list, fetch, extant, create, update, delete: remove, } })