diff --git a/lib/Providers/Personal/PersonalService.php b/lib/Providers/Personal/PersonalService.php index 26c6255..9182e2a 100644 --- a/lib/Providers/Personal/PersonalService.php +++ b/lib/Providers/Personal/PersonalService.php @@ -17,6 +17,7 @@ use KTXF\Documents\Entity\EntityPropertiesMutableInterface; use KTXF\Documents\Service\ServiceBaseInterface; use KTXF\Documents\Service\ServiceCollectionMutableInterface; use KTXF\Documents\Service\ServiceEntityMutableInterface; +use KTXF\Documents\Service\ServiceNodeListInterface; use KTXF\Files\Node\NodeType; use KTXF\Resource\Delta\Delta; use KTXF\Resource\Exceptions\InvalidParameterException; @@ -34,7 +35,7 @@ use KTXF\Resource\Sort\Sort; use KTXM\ProviderLocalDocuments\Store\BlobStore; use KTXM\ProviderLocalDocuments\Store\MetaStore; -class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface { +class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface, ServiceNodeListInterface { public const ROOT_ID = '00000000-0000-0000-0000-000000000000'; public const ROOT_LABEL = 'Personal Local Documents'; @@ -75,6 +76,9 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI self::CAPABILITY_ENTITY_LIST_SORT => [ self::CAPABILITY_ENTITY_SORT_ID, self::CAPABILITY_ENTITY_SORT_LABEL, + ServiceNodeListInterface::CAPABILITY_NODE_SORT_SIZE, + ServiceNodeListInterface::CAPABILITY_NODE_SORT_CREATED_ON, + ServiceNodeListInterface::CAPABILITY_NODE_SORT_MODIFIED_ON, ], self::CAPABILITY_ENTITY_LIST_RANGE => [ self::CAPABILITY_ENTITY_RANGE_TALLY => [ @@ -92,6 +96,22 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI self::CAPABILITY_ENTITY_COPY => true, self::CAPABILITY_ENTITY_READ => true, self::CAPABILITY_ENTITY_WRITE => true, + ServiceNodeListInterface::CAPABILITY_NODE_LIST => true, + ServiceNodeListInterface::CAPABILITY_NODE_LIST_FILTER => [ + ServiceNodeListInterface::CAPABILITY_NODE_FILTER_LABEL => 's:100:256:256', + ], + ServiceNodeListInterface::CAPABILITY_NODE_LIST_SORT => [ + ServiceNodeListInterface::CAPABILITY_NODE_SORT_LABEL, + ServiceNodeListInterface::CAPABILITY_NODE_SORT_SIZE, + ServiceNodeListInterface::CAPABILITY_NODE_SORT_CREATED_ON, + ServiceNodeListInterface::CAPABILITY_NODE_SORT_MODIFIED_ON, + ], + ServiceNodeListInterface::CAPABILITY_NODE_LIST_RANGE => [ + ServiceNodeListInterface::CAPABILITY_NODE_RANGE_TALLY => [ + ServiceNodeListInterface::CAPABILITY_NODE_RANGE_TALLY_ABSOLUTE, + ServiceNodeListInterface::CAPABILITY_NODE_RANGE_TALLY_RELATIVE, + ], + ], ]; public function __construct( @@ -785,6 +805,37 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI return $bytes; } + + // Node operations (unified collections + entities) + + public function nodeList(string|int|null $location, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): array { + $location = $this->normalizeLocation($location); + $entries = $this->metaStore->nodeList($this->serviceTenantId, $this->serviceUserId, $location, false, $filter, $sort, $range); + // cache nodes + foreach ($entries as $id => $node) { + if ($node instanceof CollectionResource) { + $this->serviceCollectionCache[$id] = $node; + } elseif ($node instanceof EntityResource) { + $this->serviceEntityCache[$id] = $node; + } + } + return $entries; + } + + public function nodeListFilter(): IFilter { + return new Filter($this->serviceAbilities[self::CAPABILITY_NODE_LIST_FILTER] ?? []); + } + + public function nodeListSort(): ISort { + return new Sort($this->serviceAbilities[self::CAPABILITY_NODE_LIST_SORT] ?? []); + } + + public function nodeListRange(RangeType $type): IRange { + if ($type !== RangeType::TALLY) { + throw new InvalidParameterException("Invalid: Node range of type '{$type->value}' is not supported"); + } + return new RangeTally(); + } /** * Build metadata array for an entity to store in .meta file diff --git a/lib/Store/MetaStore.php b/lib/Store/MetaStore.php index 29073b0..9a69d80 100644 --- a/lib/Store/MetaStore.php +++ b/lib/Store/MetaStore.php @@ -31,29 +31,28 @@ class MetaStore { protected array $_CollectionFilterAttributeMap = [ 'id' => 'nid', - 'label' => 'name', + 'label' => 'properties.label', 'parent' => 'pid', ]; protected array $_EntityFilterAttributeMap = [ 'id' => 'nid', - 'label' => 'name', + 'label' => 'properties.label', ]; protected array $_NodeFilterAttributeMap = [ 'id' => 'nid', - 'label' => 'name', + 'label' => 'properties.label', 'type' => 'type', 'parent' => 'pid', ]; protected array $_SortAttributeMap = [ - 'label' => 'name', + 'label' => 'properties.label', 'type' => 'type', - 'size' => 'size', - 'type' => 'type', - 'createdOn' => 'createdOn', - 'modifiedOn' => 'modifiedOn', + 'size' => 'properties.size', + 'createdOn' => 'created', + 'modifiedOn' => 'modified', ]; public function __construct( @@ -151,9 +150,9 @@ class MetaStore { $sortConditions = $this->constructSort($this->_SortAttributeMap, $sort); $options['sort'] = $sortConditions; } else { - $options['sort'] = ['name' => 1]; + $options['sort'] = ['properties.label' => 1]; } - + $cursor = $this->_store->selectCollection($this->_NodeTable)->find($query, $options); $list = []; foreach ($cursor as $entry) { @@ -358,14 +357,14 @@ class MetaStore { $sortConditions = $this->constructSort($this->_SortAttributeMap, $sort); $options['sort'] = $sortConditions; } else { - $options['sort'] = ['name' => 1]; + $options['sort'] = ['properties.label' => 1]; } - + if ($range !== null && $range->type() === RangeType::TALLY && $range instanceof IRangeTally) { $options['skip'] = $range->getPosition(); $options['limit'] = $range->getTally(); } - + $cursor = $this->_store->selectCollection($this->_NodeTable)->find($query, $options); $list = []; foreach ($cursor as $entry) { @@ -562,11 +561,12 @@ class MetaStore { $options = []; if ($sort !== null) { $sortConditions = $this->constructSort($this->_SortAttributeMap, $sort); - $options['sort'] = $sortConditions; + // folders must always sort before files, regardless of the caller's chosen sort field + $options['sort'] = ['type' => 1] + $sortConditions; } else { - $options['sort'] = ['type' => -1, 'name' => 1]; // folders first + $options['sort'] = ['type' => 1, 'properties.label' => 1]; // folders first, then alphabetical } - + if ($range !== null && $range->type() === RangeType::TALLY && $range instanceof IRangeTally) { $options['skip'] = $range->getPosition(); $options['limit'] = $range->getTally(); @@ -586,6 +586,13 @@ class MetaStore { return $list; } + /** + * Node delta (changes since signature, optionally recursive) + */ + public function nodeDelta(string $tenantId, string $userId, string|int|null $location, string $signature, bool $recursive = false, string $detail = 'ids'): array { + return $this->internalDelta($tenantId, $userId, $location, $signature, $recursive, $detail); + } + /** * Get all descendant collection IDs (helper for recursive operations) */ @@ -607,13 +614,6 @@ class MetaStore { return $ids; } - /** - * Node delta (changes since signature, optionally recursive) - */ - public function nodeDelta(string $tenantId, string $userId, string|int|null $location, string $signature, bool $recursive = false, string $detail = 'ids'): array { - return $this->internalDelta($tenantId, $userId, $location, $signature, $recursive, $detail); - } - /** * Internal delta implementation */