feat: implement node list

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-10-01 21:10:42 -04:00
parent de71f95e3d
commit dbfb5f8b37
4 changed files with 182 additions and 16 deletions
+128 -1
View File
@@ -17,7 +17,9 @@ use KTXF\Documents\Service\ServiceBaseInterface;
use KTXF\Documents\Service\ServiceCollectionMutableInterface;
use KTXF\Documents\Service\ServiceConfigurableInterface;
use KTXF\Documents\Service\ServiceMutableInterface;
use KTXF\Documents\Service\ServiceNodeListInterface;
use KTXF\Resource\Delta\Delta;
use KTXF\Resource\Exceptions\InvalidParameterException;
use KTXF\Resource\Filter\Filter;
use KTXF\Resource\Filter\IFilter;
use KTXF\Resource\Identifier\CollectionIdentifierInterface;
@@ -26,6 +28,8 @@ use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
use KTXF\Resource\Range\IRange;
use KTXF\Resource\Range\Range;
use KTXF\Resource\Range\RangeAnchorType;
use KTXF\Resource\Range\RangeTally;
use KTXF\Resource\Range\RangeType;
use KTXF\Resource\Sort\ISort;
use KTXF\Resource\Sort\Sort;
@@ -34,7 +38,7 @@ use KTXM\ProviderJmapc\Providers\ServiceLocation;
use KTXM\ProviderJmapc\Service\Remote\RemoteFilesService;
use KTXM\ProviderJmapc\Service\Remote\RemoteService;
class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface
class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface, ServiceNodeListInterface
{
public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE;
@@ -76,6 +80,22 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
self::CAPABILITY_ENTITY_DELTA => true,
self::CAPABILITY_ENTITY_EXTANT => true,
self::CAPABILITY_ENTITY_FETCH => true,
self::CAPABILITY_NODE_LIST => true,
self::CAPABILITY_NODE_LIST_FILTER => [
self::CAPABILITY_NODE_FILTER_LABEL => 's:100:256:256',
],
self::CAPABILITY_NODE_LIST_SORT => [
self::CAPABILITY_NODE_SORT_LABEL,
self::CAPABILITY_NODE_SORT_SIZE,
self::CAPABILITY_NODE_SORT_CREATED_ON,
self::CAPABILITY_NODE_SORT_MODIFIED_ON,
],
self::CAPABILITY_NODE_LIST_RANGE => [
self::CAPABILITY_NODE_RANGE_TALLY => [
self::CAPABILITY_NODE_RANGE_TALLY_ABSOLUTE,
self::CAPABILITY_NODE_RANGE_TALLY_RELATIVE,
],
],
];
private readonly RemoteFilesService $remoteService;
@@ -499,4 +519,111 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
return null;
}
// Node operations (unified collections + entities)
public function nodeList(string|int|null $location, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): Generator
{
$this->initialize();
$result = $this->remoteService->nodeList($location !== null ? (string)$location : null);
// convert and filter, keeping folders and files apart so folders always come first
$label = null;
if ($filter !== null && isset($filter->conditions()[self::CAPABILITY_NODE_FILTER_LABEL])) {
$label = (string)$filter->conditions()[self::CAPABILITY_NODE_FILTER_LABEL]['value'];
}
$collections = [];
$entities = [];
foreach ($result['list'] as $index => $entry) {
unset($result['list'][$index]);
if (!is_array($entry) || !isset($entry['id'])) {
continue;
}
// folders carry no blob
$object = empty($entry['blobId'])
? new CollectionResource(provider: $this->provider(), service: $this->identifier())
: new EntityResource(provider: $this->provider(), service: $this->identifier());
$object->fromJmap($entry);
if ($label !== null && $label !== '' && mb_stripos($object->getProperties()->getLabel(), $label) === false) {
continue;
}
if ($object instanceof CollectionResource) {
$collections[] = $object;
} else {
$entities[] = $object;
}
}
// sort each group, defaulting to label ascending
$conditions = $sort?->conditions() ?? [];
if ($conditions === []) {
$conditions = [['attribute' => self::CAPABILITY_NODE_SORT_LABEL, 'direction' => true]];
}
$comparator = function (CollectionResource|EntityResource $a, CollectionResource|EntityResource $b) use ($conditions): int {
foreach ($conditions as $condition) {
$order = match ($condition['attribute']) {
self::CAPABILITY_NODE_SORT_LABEL => strnatcasecmp($a->getProperties()->getLabel(), $b->getProperties()->getLabel()),
self::CAPABILITY_NODE_SORT_SIZE => $this->nodeSize($a) <=> $this->nodeSize($b),
self::CAPABILITY_NODE_SORT_CREATED_ON => ($a->created()?->getTimestamp() ?? 0) <=> ($b->created()?->getTimestamp() ?? 0),
self::CAPABILITY_NODE_SORT_MODIFIED_ON => ($a->modified()?->getTimestamp() ?? 0) <=> ($b->modified()?->getTimestamp() ?? 0),
default => 0,
};
if ($order !== 0) {
return $condition['direction'] ? $order : -$order;
}
}
return strnatcasecmp($a->getProperties()->getLabel(), $b->getProperties()->getLabel());
};
usort($collections, $comparator);
usort($entities, $comparator);
$nodes = array_merge($collections, $entities);
unset($collections, $entities);
// apply range
if ($range instanceof RangeTally) {
$offset = 0;
if ($range->getAnchor() === RangeAnchorType::RELATIVE) {
// relative ranges start after the anchor node
foreach ($nodes as $index => $node) {
if ((string)$node->identifier() === (string)$range->getPosition()) {
$offset = $index + 1;
break;
}
}
} else {
$offset = max(0, (int)$range->getPosition());
}
$nodes = array_slice($nodes, $offset, $range->getTally());
}
foreach ($nodes as $index => $node) {
unset($nodes[$index]);
yield $node->identifier() => $node;
}
}
public function nodeListFilter(): IFilter
{
return new Filter($this->serviceAbilities[self::CAPABILITY_NODE_LIST_FILTER] ?? []);
}
public function nodeListSort(): ISort
{
// sort attributes are matched by key
return new Sort(array_fill_keys($this->serviceAbilities[self::CAPABILITY_NODE_LIST_SORT] ?? [], true));
}
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();
}
private function nodeSize(CollectionResource|EntityResource $node): int
{
return $node instanceof EntityResource ? $node->getProperties()->size() : 0;
}
}