feat: node list back end

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-09-04 22:12:27 -04:00
parent 93aa1b0f24
commit 7d18bcab1a
2 changed files with 214 additions and 98 deletions
+80
View File
@@ -20,6 +20,7 @@ use KTXF\Documents\Service\ServiceBaseInterface;
use KTXF\Documents\Service\ServiceCollectionMutableInterface;
use KTXF\Documents\Service\ServiceEntityMutableInterface;
use KTXF\Documents\Service\ServiceMutableInterface;
use KTXF\Documents\Service\ServiceNodeListInterface;
use KTXF\Resource\Filter\IFilter;
use KTXF\Preview\PreviewSource;
use KTXF\Resource\BinaryResource;
@@ -1404,6 +1405,85 @@ class Manager {
return $this->previewManager->fetch($tenantId, $source, $variant);
}
/**
* List collections and entities within a location as one unified, paginated set
*
* Folders are ordered before files within the returned range. Unlike
* {@see collectionList} and {@see entityListBulk}, this requires the target
* service to natively implement {@see ServiceNodeListInterface} — there is
* intentionally no fallback for services that only support the separate
* collection/entity listing operations, since every current caller targets a
* single provider/service/location per request.
*
* @since 2026.09.01
*
* @param string $tenantId Tenant identifier
* @param string $userId User identifier
* @param ResourceIdentifiers|null $targets Node sources with collection identifiers
* @param array|null $filter Node filter
* @param array|null $sort Node sort
* @param array|null $range Node range/pagination
*
* @return \Generator<int,CollectionBaseInterface|EntityBaseInterface> Nodes in provider order
*
* @throws InvalidArgumentException If the resolved service does not support unified node listing
*/
public function nodeList(string $tenantId, string $userId, ?ResourceIdentifiers $targets = null, array|null $filter = null, array|null $sort = null, array|null $range = null): \Generator {
// confirm that sources are provided
if ($targets === null) {
$targets = new ResourceIdentifiers([]);
}
// retrieve services for each provider
$aggregateServices = $this->serviceList($tenantId, $userId, $targets);
// retrieve nodes for each service
foreach ($aggregateServices as $services) {
/** @var ServiceBaseInterface $service */
foreach ($services as $service) {
// omit disabled services
if ($service->getEnabled() === false) {
continue;
}
if ($service instanceof ServiceNodeListInterface === false) {
throw new InvalidArgumentException("Service '{$service->identifier()}' does not support paginated node listing");
}
// retrieve collections for each service
$collectionSelected = $targets->byProvider($service->provider())->byService($service->identifier())->collections();
if ($collectionSelected === []) {
// documents are hierarchical: service level selection lists the root collection
$collectionSelected = [''];
}
// construct filter for nodes
$nodeFilter = null;
if ($filter !== null && $filter !== []) {
$nodeFilter = $service->nodeListFilter();
foreach ($filter as $attribute => $value) {
$nodeFilter->condition($attribute, $value);
}
}
// construct sort for nodes
$nodeSort = null;
if ($sort !== null && $sort !== []) {
$nodeSort = $service->nodeListSort();
foreach ($sort as $attribute => $direction) {
$nodeSort->condition($attribute, $direction);
}
}
// construct range for nodes
$nodeRange = null;
if ($range !== null && $range !== [] && isset($range['type'])) {
$nodeRange = $service->nodeListRange(RangeType::from($range['type']))->jsonDeserialize($range);
}
// Preserve provider order without grouping or buffering nodes.
foreach ($collectionSelected as $collectionId) {
$nodes = $service->nodeList($collectionId, $nodeFilter, $nodeSort, $nodeRange);
foreach ($nodes as $node) {
yield $node;
}
}
}
}
}
}