Initial commit

This commit is contained in:
root
2025-12-21 09:57:43 -05:00
committed by Sebastian Krupinski
commit db42b6699c
35 changed files with 6458 additions and 0 deletions
+687
View File
@@ -0,0 +1,687 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Ref, ComputedRef } from 'vue'
import type { FileNode, FileCollection, FileEntity } from '../types/node'
import type { FilterCondition, SortCondition, RangeCondition } from '../types/common'
import { isFileCollection } from '../types/node'
import { collectionService } from '../services/collectionService'
import { entityService } from '../services/entityService'
import { nodeService } from '../services/nodeService'
import { FileCollectionObject } from '../models/collection'
import { FileEntityObject } from '../models/entity'
// Root collection constant
export const ROOT_ID = '00000000-0000-0000-0000-000000000000'
// Store structure: provider -> service -> nodeId -> node (either collection or entity object)
type NodeRecord = FileCollectionObject | FileEntityObject
type ServiceNodeStore = Record<string, NodeRecord>
type ProviderNodeStore = Record<string, ServiceNodeStore>
type NodeStore = Record<string, ProviderNodeStore>
export const useNodesStore = defineStore('fileNodes', () => {
const nodes: Ref<NodeStore> = ref({})
const syncTokens: Ref<Record<string, Record<string, string>>> = ref({}) // provider -> service -> token
const loading = ref(false)
const error: Ref<string | null> = ref(null)
// Computed: flat list of all nodes
const nodeList: ComputedRef<NodeRecord[]> = computed(() => {
const result: NodeRecord[] = []
Object.values(nodes.value).forEach(providerNodes => {
Object.values(providerNodes).forEach(serviceNodes => {
result.push(...Object.values(serviceNodes))
})
})
return result
})
// Computed: all collections (folders)
const collectionList: ComputedRef<FileCollectionObject[]> = computed(() => {
return nodeList.value.filter(
(node): node is FileCollectionObject => node['@type'] === 'files.collection'
)
})
// Computed: all entities (files)
const entityList: ComputedRef<FileEntityObject[]> = computed(() => {
return nodeList.value.filter(
(node): node is FileEntityObject => node['@type'] === 'files.entity'
)
})
// Get a specific node
const getNode = (
providerId: string,
serviceId: string,
nodeId: string
): NodeRecord | undefined => {
return nodes.value[providerId]?.[serviceId]?.[nodeId]
}
// Get all nodes for a service
const getServiceNodes = (
providerId: string,
serviceId: string
): NodeRecord[] => {
return Object.values(nodes.value[providerId]?.[serviceId] || {})
}
// Get children of a parent node (or root nodes if parentId is null/ROOT_ID)
const getChildren = (
providerId: string,
serviceId: string,
parentId: string | null
): NodeRecord[] => {
const serviceNodes = nodes.value[providerId]?.[serviceId] || {}
const targetParent = parentId === ROOT_ID ? ROOT_ID : parentId
return Object.values(serviceNodes).filter(node => node.in === targetParent)
}
// Get child collections (folders)
const getChildCollections = (
providerId: string,
serviceId: string,
parentId: string | null
): FileCollectionObject[] => {
return getChildren(providerId, serviceId, parentId).filter(
(node): node is FileCollectionObject => node['@type'] === 'files.collection'
)
}
// Get child entities (files)
const getChildEntities = (
providerId: string,
serviceId: string,
parentId: string | null
): FileEntityObject[] => {
return getChildren(providerId, serviceId, parentId).filter(
(node): node is FileEntityObject => node['@type'] === 'files.entity'
)
}
// Get path to root (ancestors)
const getPath = (
providerId: string,
serviceId: string,
nodeId: string
): NodeRecord[] => {
const path: NodeRecord[] = []
let currentNode = getNode(providerId, serviceId, nodeId)
while (currentNode) {
path.unshift(currentNode)
if (currentNode.in === null || currentNode.in === ROOT_ID || currentNode.id === ROOT_ID) {
break
}
currentNode = getNode(providerId, serviceId, currentNode.in)
}
return path
}
// Check if a node is the root
const isRoot = (nodeId: string): boolean => {
return nodeId === ROOT_ID
}
// Helper to hydrate a node based on its type
const hydrateNode = (data: FileNode): NodeRecord => {
if (isFileCollection(data)) {
return new FileCollectionObject().fromJson(data)
} else {
return new FileEntityObject().fromJson(data as FileEntity)
}
}
// Set all nodes for a provider/service
const setNodes = (
providerId: string,
serviceId: string,
data: FileNode[]
) => {
if (!nodes.value[providerId]) {
nodes.value[providerId] = {}
}
const hydrated: ServiceNodeStore = {}
for (const nodeData of data) {
hydrated[nodeData.id] = hydrateNode(nodeData)
}
nodes.value[providerId][serviceId] = hydrated
}
// Add/update a single node
const addNode = (
providerId: string,
serviceId: string,
node: FileNode
) => {
if (!nodes.value[providerId]) {
nodes.value[providerId] = {}
}
if (!nodes.value[providerId][serviceId]) {
nodes.value[providerId][serviceId] = {}
}
nodes.value[providerId][serviceId][node.id] = hydrateNode(node)
}
// Add multiple nodes (handles both array and object formats from API)
const addNodes = (
providerId: string,
serviceId: string,
data: FileNode[] | Record<string, FileNode>
) => {
if (!nodes.value[providerId]) {
nodes.value[providerId] = {}
}
if (!nodes.value[providerId][serviceId]) {
nodes.value[providerId][serviceId] = {}
}
// Handle both array and object (keyed by ID) formats
const nodeArray = Array.isArray(data) ? data : Object.values(data)
for (const nodeData of nodeArray) {
nodes.value[providerId][serviceId][nodeData.id] = hydrateNode(nodeData)
}
}
// Remove a node
const removeNode = (
providerId: string,
serviceId: string,
nodeId: string
) => {
if (nodes.value[providerId]?.[serviceId]) {
delete nodes.value[providerId][serviceId][nodeId]
}
}
// Remove multiple nodes
const removeNodes = (
providerId: string,
serviceId: string,
nodeIds: string[]
) => {
if (nodes.value[providerId]?.[serviceId]) {
for (const id of nodeIds) {
delete nodes.value[providerId][serviceId][id]
}
}
}
// Clear all nodes for a service
const clearServiceNodes = (
providerId: string,
serviceId: string
) => {
if (nodes.value[providerId]) {
delete nodes.value[providerId][serviceId]
}
}
// Clear all nodes
const clearNodes = () => {
nodes.value = {}
}
// Sync token management
const getSyncToken = (
providerId: string,
serviceId: string
): string | undefined => {
return syncTokens.value[providerId]?.[serviceId]
}
const setSyncToken = (
providerId: string,
serviceId: string,
token: string
) => {
if (!syncTokens.value[providerId]) {
syncTokens.value[providerId] = {}
}
syncTokens.value[providerId][serviceId] = token
}
// ==================== API Actions ====================
// Fetch nodes (collections and entities) for a location
const fetchNodes = async (
providerId: string,
serviceId: string,
location?: string | null,
recursive: boolean = false,
filter?: FilterCondition[] | null,
sort?: SortCondition[] | null,
range?: RangeCondition | null
): Promise<NodeRecord[]> => {
loading.value = true
error.value = null
try {
const data = await nodeService.list(
providerId,
serviceId,
location,
recursive,
filter,
sort,
range
)
// API returns object keyed by ID, convert to array
const nodeArray = Array.isArray(data) ? data : Object.values(data)
addNodes(providerId, serviceId, nodeArray)
return nodeArray.map(hydrateNode)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to fetch nodes'
throw e
} finally {
loading.value = false
}
}
// Fetch collections (folders) for a location
const fetchCollections = async (
providerId: string,
serviceId: string,
location?: string | null,
filter?: FilterCondition[] | null,
sort?: SortCondition[] | null
): Promise<FileCollectionObject[]> => {
loading.value = true
error.value = null
try {
const data = await collectionService.list(providerId, serviceId, location, filter, sort)
// API returns object keyed by ID, convert to array
const collectionArray = Array.isArray(data) ? data : Object.values(data)
addNodes(providerId, serviceId, collectionArray)
return collectionArray.map(c => new FileCollectionObject().fromJson(c))
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to fetch collections'
throw e
} finally {
loading.value = false
}
}
// Fetch entities (files) for a collection
const fetchEntities = async (
providerId: string,
serviceId: string,
collection: string,
filter?: FilterCondition[] | null,
sort?: SortCondition[] | null,
range?: RangeCondition | null
): Promise<FileEntityObject[]> => {
loading.value = true
error.value = null
try {
const data = await entityService.list(providerId, serviceId, collection, filter, sort, range)
// API returns object keyed by ID, convert to array
const entityArray = Array.isArray(data) ? data : Object.values(data)
addNodes(providerId, serviceId, entityArray)
return entityArray.map(e => new FileEntityObject().fromJson(e))
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to fetch entities'
throw e
} finally {
loading.value = false
}
}
// Create a collection (folder)
const createCollection = async (
providerId: string,
serviceId: string,
location: string | null,
data: Partial<FileCollection>,
options?: Record<string, unknown>
): Promise<FileCollectionObject> => {
loading.value = true
error.value = null
try {
const created = await collectionService.create(providerId, serviceId, location, data, options)
addNode(providerId, serviceId, created)
return new FileCollectionObject().fromJson(created)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to create collection'
throw e
} finally {
loading.value = false
}
}
// Create an entity (file)
const createEntity = async (
providerId: string,
serviceId: string,
collection: string | null,
data: Partial<FileEntity>,
options?: Record<string, unknown>
): Promise<FileEntityObject> => {
loading.value = true
error.value = null
try {
const created = await entityService.create(providerId, serviceId, collection, data, options)
addNode(providerId, serviceId, created)
return new FileEntityObject().fromJson(created)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to create entity'
throw e
} finally {
loading.value = false
}
}
// Modify a collection
const modifyCollection = async (
providerId: string,
serviceId: string,
identifier: string,
data: Partial<FileCollection>
): Promise<FileCollectionObject> => {
loading.value = true
error.value = null
try {
const modified = await collectionService.modify(providerId, serviceId, identifier, data)
addNode(providerId, serviceId, modified)
return new FileCollectionObject().fromJson(modified)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to modify collection'
throw e
} finally {
loading.value = false
}
}
// Modify an entity
const modifyEntity = async (
providerId: string,
serviceId: string,
collection: string | null,
identifier: string,
data: Partial<FileEntity>
): Promise<FileEntityObject> => {
loading.value = true
error.value = null
try {
const modified = await entityService.modify(providerId, serviceId, collection, identifier, data)
addNode(providerId, serviceId, modified)
return new FileEntityObject().fromJson(modified)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to modify entity'
throw e
} finally {
loading.value = false
}
}
// Destroy a collection
const destroyCollection = async (
providerId: string,
serviceId: string,
identifier: string
): Promise<boolean> => {
loading.value = true
error.value = null
try {
const success = await collectionService.destroy(providerId, serviceId, identifier)
if (success) {
removeNode(providerId, serviceId, identifier)
}
return success
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to destroy collection'
throw e
} finally {
loading.value = false
}
}
// Destroy an entity
const destroyEntity = async (
providerId: string,
serviceId: string,
collection: string | null,
identifier: string
): Promise<boolean> => {
loading.value = true
error.value = null
try {
const success = await entityService.destroy(providerId, serviceId, collection, identifier)
if (success) {
removeNode(providerId, serviceId, identifier)
}
return success
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to destroy entity'
throw e
} finally {
loading.value = false
}
}
// Copy a collection
const copyCollection = async (
providerId: string,
serviceId: string,
identifier: string,
location?: string | null
): Promise<FileCollectionObject> => {
loading.value = true
error.value = null
try {
const copied = await collectionService.copy(providerId, serviceId, identifier, location)
addNode(providerId, serviceId, copied)
return new FileCollectionObject().fromJson(copied)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to copy collection'
throw e
} finally {
loading.value = false
}
}
// Copy an entity
const copyEntity = async (
providerId: string,
serviceId: string,
collection: string | null,
identifier: string,
destination?: string | null
): Promise<FileEntityObject> => {
loading.value = true
error.value = null
try {
const copied = await entityService.copy(providerId, serviceId, collection, identifier, destination)
addNode(providerId, serviceId, copied)
return new FileEntityObject().fromJson(copied)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to copy entity'
throw e
} finally {
loading.value = false
}
}
// Move a collection
const moveCollection = async (
providerId: string,
serviceId: string,
identifier: string,
location?: string | null
): Promise<FileCollectionObject> => {
loading.value = true
error.value = null
try {
const moved = await collectionService.move(providerId, serviceId, identifier, location)
addNode(providerId, serviceId, moved)
return new FileCollectionObject().fromJson(moved)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to move collection'
throw e
} finally {
loading.value = false
}
}
// Move an entity
const moveEntity = async (
providerId: string,
serviceId: string,
collection: string | null,
identifier: string,
destination?: string | null
): Promise<FileEntityObject> => {
loading.value = true
error.value = null
try {
const moved = await entityService.move(providerId, serviceId, collection, identifier, destination)
addNode(providerId, serviceId, moved)
return new FileEntityObject().fromJson(moved)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to move entity'
throw e
} finally {
loading.value = false
}
}
// Read entity content
const readEntity = async (
providerId: string,
serviceId: string,
collection: string,
identifier: string
): Promise<string | null> => {
loading.value = true
error.value = null
try {
const result = await entityService.read(providerId, serviceId, collection, identifier)
return result.content
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to read entity'
throw e
} finally {
loading.value = false
}
}
// Write entity content
const writeEntity = async (
providerId: string,
serviceId: string,
collection: string | null,
identifier: string,
content: string
): Promise<number> => {
loading.value = true
error.value = null
try {
return await entityService.write(providerId, serviceId, collection, identifier, content)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to write entity'
throw e
} finally {
loading.value = false
}
}
// Sync delta changes
const syncDelta = async (
providerId: string,
serviceId: string,
location: string | null,
signature: string,
recursive: boolean = false,
detail: 'ids' | 'full' = 'full'
): Promise<void> => {
loading.value = true
error.value = null
try {
const delta = await nodeService.delta(providerId, serviceId, location, signature, recursive, detail)
// Handle removed nodes
if (delta.removed.length > 0) {
removeNodes(providerId, serviceId, delta.removed)
}
// Handle added/modified nodes
if (detail === 'full') {
const addedNodes = delta.added as FileNode[]
const modifiedNodes = delta.modified as FileNode[]
if (addedNodes.length > 0) {
addNodes(providerId, serviceId, addedNodes)
}
if (modifiedNodes.length > 0) {
addNodes(providerId, serviceId, modifiedNodes)
}
}
// Update sync token
if (delta.signature) {
setSyncToken(providerId, serviceId, delta.signature)
}
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to sync delta'
throw e
} finally {
loading.value = false
}
}
return {
// State
nodes,
syncTokens,
loading,
error,
// Constants
ROOT_ID,
// Computed
nodeList,
collectionList,
entityList,
// Getters
getNode,
getServiceNodes,
getChildren,
getChildCollections,
getChildEntities,
getPath,
isRoot,
// Setters
setNodes,
addNode,
addNodes,
removeNode,
removeNodes,
clearServiceNodes,
clearNodes,
// Sync
getSyncToken,
setSyncToken,
// API Actions - Fetch
fetchNodes,
fetchCollections,
fetchEntities,
// API Actions - Create
createCollection,
createEntity,
// API Actions - Modify
modifyCollection,
modifyEntity,
// API Actions - Destroy
destroyCollection,
destroyEntity,
// API Actions - Copy
copyCollection,
copyEntity,
// API Actions - Move
moveCollection,
moveEntity,
// API Actions - Content
readEntity,
writeEntity,
// API Actions - Sync
syncDelta,
}
})
+104
View File
@@ -0,0 +1,104 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Ref, ComputedRef } from 'vue'
import type { ProviderInterface, ProviderRecord, ProviderCapabilitiesInterface } from '../types/provider'
import type { SourceSelector } from '../types/common'
import { providerService } from '../services/providerService'
import { ProviderObject } from '../models/provider'
export const useProvidersStore = defineStore('fileProviders', () => {
const providers: Ref<Record<string, ProviderObject>> = ref({})
const loading = ref(false)
const error: Ref<string | null> = ref(null)
const initialized = ref(false)
const providerList: ComputedRef<ProviderObject[]> = computed(() =>
Object.values(providers.value)
)
const providerIds: ComputedRef<string[]> = computed(() =>
Object.keys(providers.value)
)
const getProvider = (id: string): ProviderObject | undefined => {
return providers.value[id]
}
const hasProvider = (id: string): boolean => {
return id in providers.value
}
const isCapable = (providerId: string, capability: keyof ProviderCapabilitiesInterface): boolean => {
const provider = providers.value[providerId]
return provider ? provider.capable(capability) : false
}
const setProviders = (data: ProviderRecord) => {
const hydrated: Record<string, ProviderObject> = {}
for (const [id, providerData] of Object.entries(data)) {
hydrated[id] = new ProviderObject().fromJson(providerData)
}
providers.value = hydrated
initialized.value = true
}
const addProvider = (id: string, provider: ProviderInterface) => {
providers.value[id] = new ProviderObject().fromJson(provider)
}
const removeProvider = (id: string) => {
delete providers.value[id]
}
const clearProviders = () => {
providers.value = {}
initialized.value = false
}
// API actions
const fetchProviders = async (sources?: SourceSelector): Promise<void> => {
loading.value = true
error.value = null
try {
const data = await providerService.list(sources)
setProviders(data)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to fetch providers'
throw e
} finally {
loading.value = false
}
}
const checkProviderExtant = async (sources: SourceSelector): Promise<Record<string, boolean>> => {
try {
return await providerService.extant(sources)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to check providers'
throw e
}
}
return {
// State
providers,
loading,
error,
initialized,
// Computed
providerList,
providerIds,
// Getters
getProvider,
hasProvider,
isCapable,
// Setters
setProviders,
addProvider,
removeProvider,
clearProviders,
// Actions
fetchProviders,
checkProviderExtant,
}
})
+131
View File
@@ -0,0 +1,131 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Ref, ComputedRef } from 'vue'
import type { ServiceInterface, ServiceRecord } from '../types/service'
import type { SourceSelector } from '../types/common'
import { serviceService } from '../services/serviceService'
import { ServiceObject } from '../models/service'
// Nested structure: provider -> service -> ServiceObject
type ServiceStore = Record<string, Record<string, ServiceObject>>
export const useServicesStore = defineStore('fileServices', () => {
const services: Ref<ServiceStore> = ref({})
const loading = ref(false)
const error: Ref<string | null> = ref(null)
const initialized = ref(false)
const serviceList: ComputedRef<ServiceObject[]> = computed(() => {
const result: ServiceObject[] = []
Object.values(services.value).forEach(providerServices => {
result.push(...Object.values(providerServices))
})
return result
})
const getService = (providerId: string, serviceId: string): ServiceObject | undefined => {
return services.value[providerId]?.[serviceId]
}
const hasService = (providerId: string, serviceId: string): boolean => {
return !!services.value[providerId]?.[serviceId]
}
const getProviderServices = (providerId: string): ServiceObject[] => {
return Object.values(services.value[providerId] || {})
}
const getRootId = (providerId: string, serviceId: string): string | undefined => {
return services.value[providerId]?.[serviceId]?.rootId
}
const setServices = (data: ServiceRecord) => {
const hydrated: ServiceStore = {}
for (const [id, serviceData] of Object.entries(data)) {
const providerId = serviceData.provider
if (!hydrated[providerId]) {
hydrated[providerId] = {}
}
hydrated[providerId][id] = new ServiceObject().fromJson(serviceData)
}
services.value = hydrated
initialized.value = true
}
const addService = (providerId: string, serviceId: string, service: ServiceInterface) => {
if (!services.value[providerId]) {
services.value[providerId] = {}
}
services.value[providerId][serviceId] = new ServiceObject().fromJson(service)
}
const removeService = (providerId: string, serviceId: string) => {
if (services.value[providerId]) {
delete services.value[providerId][serviceId]
}
}
const clearServices = () => {
services.value = {}
initialized.value = false
}
// API actions
const fetchServices = async (sources?: SourceSelector): Promise<void> => {
loading.value = true
error.value = null
try {
const data = await serviceService.list(sources)
setServices(data)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to fetch services'
throw e
} finally {
loading.value = false
}
}
const checkServiceExtant = async (sources: SourceSelector): Promise<Record<string, boolean>> => {
try {
return await serviceService.extant(sources)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to check services'
throw e
}
}
const fetchService = async (providerId: string, serviceId: string): Promise<ServiceObject> => {
try {
const data = await serviceService.fetch(providerId, serviceId)
addService(providerId, serviceId, data)
return services.value[providerId][serviceId]
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to fetch service'
throw e
}
}
return {
// State
services,
loading,
error,
initialized,
// Computed
serviceList,
// Getters
getService,
hasService,
getProviderServices,
getRootId,
// Setters
setServices,
addService,
removeService,
clearServices,
// Actions
fetchServices,
checkServiceExtant,
fetchService,
}
})