Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c97618bdd1 | |||
| 4b37be3e7a | |||
| 26ed0c8aad | |||
| 41de70da75 | |||
| e802c8fc0f | |||
| 7d18bcab1a | |||
| 93aa1b0f24 | |||
| cbcf2e5acb |
Generated
+46
-34
@@ -9,20 +9,20 @@
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "myclabs/deep-copy",
|
||||
"version": "1.13.4",
|
||||
"version": "1.14.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
|
||||
"reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||
"reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
|
||||
"reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
"php": "^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/collections": "<1.6.8",
|
||||
@@ -57,15 +57,15 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
||||
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
|
||||
"source": "https://github.com/myclabs/DeepCopy/tree/1.14.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
|
||||
"type": "tidelift"
|
||||
"url": "https://github.com/mnapoli",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-01T08:46:24+00:00"
|
||||
"time": "2026-08-11T10:17:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nikic/php-parser",
|
||||
@@ -332,23 +332,23 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
"version": "6.0.1",
|
||||
"version": "6.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
|
||||
"reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5"
|
||||
"reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5",
|
||||
"reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a248d1640ab059b075f53a2ef0f9856e864e06b5",
|
||||
"reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^12.0"
|
||||
"phpunit/phpunit": "^12.5.33"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -381,7 +381,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1"
|
||||
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -401,7 +401,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-02T14:04:18+00:00"
|
||||
"time": "2026-08-25T14:40:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-invoker",
|
||||
@@ -589,16 +589,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "12.5.31",
|
||||
"version": "12.5.35",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "0608d157a284f15cc73b99a3327eff06b66a176d"
|
||||
"reference": "345ac22e42cb14cdc7aa8428655d8bb8c82a4aab"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0608d157a284f15cc73b99a3327eff06b66a176d",
|
||||
"reference": "0608d157a284f15cc73b99a3327eff06b66a176d",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/345ac22e42cb14cdc7aa8428655d8bb8c82a4aab",
|
||||
"reference": "345ac22e42cb14cdc7aa8428655d8bb8c82a4aab",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -608,18 +608,18 @@
|
||||
"ext-libxml": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"myclabs/deep-copy": "^1.13.4",
|
||||
"myclabs/deep-copy": "^1.14.0",
|
||||
"phar-io/manifest": "^2.0.4",
|
||||
"phar-io/version": "^3.2.1",
|
||||
"php": ">=8.3",
|
||||
"phpunit/php-code-coverage": "^12.5.7",
|
||||
"phpunit/php-file-iterator": "^6.0.1",
|
||||
"phpunit/php-file-iterator": "^6.0.2",
|
||||
"phpunit/php-invoker": "^6.0.0",
|
||||
"phpunit/php-text-template": "^5.0.0",
|
||||
"phpunit/php-timer": "^8.0.0",
|
||||
"sebastian/cli-parser": "^4.2.1",
|
||||
"sebastian/comparator": "^7.1.8",
|
||||
"sebastian/diff": "^7.0.0",
|
||||
"sebastian/diff": "^7.0.1",
|
||||
"sebastian/environment": "^8.1.2",
|
||||
"sebastian/exporter": "^7.0.3",
|
||||
"sebastian/global-state": "^8.0.3",
|
||||
@@ -667,7 +667,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.31"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.35"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -675,7 +675,7 @@
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-06T14:54:16+00:00"
|
||||
"time": "2026-09-09T04:48:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
@@ -898,24 +898,24 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/diff",
|
||||
"version": "7.0.0",
|
||||
"version": "7.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/diff.git",
|
||||
"reference": "7ab1ea946c012266ca32390913653d844ecd085f"
|
||||
"reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f",
|
||||
"reference": "7ab1ea946c012266ca32390913653d844ecd085f",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6",
|
||||
"reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^12.0",
|
||||
"symfony/process": "^7.2"
|
||||
"phpunit/phpunit": "^12.5.33",
|
||||
"symfony/process": "^7.4.17"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -953,15 +953,27 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/diff/issues",
|
||||
"security": "https://github.com/sebastianbergmann/diff/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/diff/tree/7.0.0"
|
||||
"source": "https://github.com/sebastianbergmann/diff/tree/7.0.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sebastianbergmann",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://liberapay.com/sebastianbergmann",
|
||||
"type": "liberapay"
|
||||
},
|
||||
{
|
||||
"url": "https://thanks.dev/u/gh/sebastianbergmann",
|
||||
"type": "thanks_dev"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/sebastian/diff",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-07T04:55:46+00:00"
|
||||
"time": "2026-08-25T15:35:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/environment",
|
||||
@@ -1701,5 +1713,5 @@
|
||||
"platform-overrides": {
|
||||
"php": "8.3"
|
||||
},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62,14 +62,14 @@ class TransferController extends ControllerAbstract
|
||||
/** @var EntityBaseInterface[] $entities */
|
||||
$entities = $this->manager->entityFetchBulk($tenantId, $userId, $target);
|
||||
|
||||
if (empty($entities) || !isset($entities[$identifier])) {
|
||||
if (!isset($entities[(string) $target])) {
|
||||
return new JsonResponse([
|
||||
'status' => 'error',
|
||||
'error' => ['code' => 404, 'message' => 'File not found']
|
||||
], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$entity = $entities[$identifier];
|
||||
$entity = $entities[(string) $target];
|
||||
|
||||
// Get the stream
|
||||
$stream = $this->manager->entityReadStream($tenantId, $userId, $target);
|
||||
@@ -130,10 +130,7 @@ class TransferController extends ControllerAbstract
|
||||
$variant,
|
||||
);
|
||||
if ($preview === null) {
|
||||
return new JsonResponse([
|
||||
'status' => 'error',
|
||||
'error' => ['code' => 404, 'message' => 'Preview unavailable'],
|
||||
], Response::HTTP_NOT_FOUND);
|
||||
return new Response('', Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
$response = new StreamedResponse($preview->resource->stream());
|
||||
@@ -330,15 +327,16 @@ class TransferController extends ControllerAbstract
|
||||
foreach ($ids as $id) {
|
||||
// Try as entity first
|
||||
if ($collection !== null) {
|
||||
$target = new EntityIdentifier($provider, $service, $collection, (string) $id);
|
||||
/** @var EntityBaseInterface[] $entities */
|
||||
$entities = $this->manager->entityFetchBulk(
|
||||
$tenantId,
|
||||
$userId,
|
||||
new EntityIdentifier($provider, $service, $collection, (string)$id)
|
||||
$target
|
||||
);
|
||||
|
||||
if (!empty($entities) && isset($entities[$id])) {
|
||||
$entity = $entities[$id];
|
||||
if (isset($entities[(string) $target])) {
|
||||
$entity = $entities[(string) $target];
|
||||
$files[] = [
|
||||
'type' => 'file',
|
||||
'id' => $id,
|
||||
|
||||
+98
-12
@@ -20,10 +20,12 @@ 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;
|
||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||
use KTXF\Resource\Identifier\ServiceIdentifier;
|
||||
use KTXF\Resource\Identifier\EntityIdentifier;
|
||||
use KTXF\Resource\Identifier\ResourceIdentifiers;
|
||||
use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
|
||||
@@ -506,18 +508,16 @@ class Manager {
|
||||
*
|
||||
* @param string $tenantId tenant identifier
|
||||
* @param string $userId user identifier
|
||||
* @param string $provider provider identifier
|
||||
* @param string|int $service service identifier
|
||||
* @param CollectionIdentifier|null $target target parent collection identifier (null for root)
|
||||
* @param ServiceIdentifier|CollectionIdentifier $target service root or parent collection
|
||||
* @param CollectionPropertiesMutableInterface|array $properties properties for the new collection
|
||||
* @param array $options additional options for creation
|
||||
*
|
||||
* @return CollectionBaseInterface
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function collectionCreate(string $tenantId, string $userId, string $provider, string|int $service, CollectionIdentifier|null $target, CollectionPropertiesMutableInterface|array $properties, array $options = []): CollectionBaseInterface {
|
||||
public function collectionCreate(string $tenantId, string $userId, ServiceIdentifier|CollectionIdentifier $target, CollectionPropertiesMutableInterface|array $properties, array $options = []): CollectionBaseInterface {
|
||||
// retrieve service
|
||||
$service = $this->serviceFetch($tenantId, $userId, $provider, $service);
|
||||
$service = $this->serviceFetch($tenantId, $userId, $target->provider(), $target->service());
|
||||
// Check if service supports collection creation
|
||||
if ($service->getEnabled() === false) {
|
||||
throw new InvalidArgumentException("Service '{$service->identifier()}' not found or is disabled");
|
||||
@@ -533,7 +533,7 @@ class Manager {
|
||||
$properties = $service->collectionFresh()->getProperties()->jsonDeserialize($properties);
|
||||
}
|
||||
// Create collection
|
||||
return $service->collectionCreate($target, $properties, $options);
|
||||
return $service->collectionCreate($target instanceof CollectionIdentifier ? $target : null, $properties, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -811,21 +811,28 @@ class Manager {
|
||||
* @return array<string,EntityBaseInterface>
|
||||
*/
|
||||
public function entityFetchBulk(string $tenantId, ?string $userId, EntityIdentifier ...$identifiers): array {
|
||||
// group identifiers by provider/service
|
||||
// Keep locations separate: providers may return maps keyed by local ID.
|
||||
$groupedIdentifiers = [];
|
||||
foreach ($identifiers as $identifier) {
|
||||
$groupedIdentifiers[$identifier->provider()][$identifier->service()][] = $identifier;
|
||||
$groupedIdentifiers[$identifier->provider()][$identifier->service()][$identifier->collection()][] = $identifier;
|
||||
}
|
||||
// retrieve each service and fetch entities
|
||||
$list = [];
|
||||
foreach ($groupedIdentifiers as $providerId => $services) {
|
||||
foreach ($services as $serviceId => $entities) {
|
||||
foreach ($services as $serviceId => $collections) {
|
||||
$service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId);
|
||||
if ($service->getEnabled() === false) {
|
||||
throw new InvalidArgumentException("Service '{$providerId}:{$serviceId}' not found or is disabled");
|
||||
}
|
||||
// retrieve entities and merge into list
|
||||
$list = array_merge($list, $service->entityFetchBulk(...$entities));
|
||||
foreach ($collections as $entities) {
|
||||
$fetched = $service->entityFetchBulk(...$entities);
|
||||
foreach ($entities as $identifier) {
|
||||
$entity = $fetched[(string) $identifier] ?? $fetched[$identifier->entity()] ?? null;
|
||||
if ($entity !== null) {
|
||||
$list[(string) $identifier] = $entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $list;
|
||||
@@ -1353,7 +1360,7 @@ class Manager {
|
||||
*/
|
||||
public function entityPreview(string $tenantId, string $userId, EntityIdentifier $target, string $variant = 'inline'): ?Preview {
|
||||
$entities = $this->entityFetchBulk($tenantId, $userId, $target);
|
||||
$entity = $entities[$target->entity()] ?? reset($entities);
|
||||
$entity = $entities[(string) $target] ?? null;
|
||||
if (!$entity instanceof EntityBaseInterface) {
|
||||
return null;
|
||||
}
|
||||
@@ -1404,6 +1411,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-12
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace KTXM\DocumentsManager;
|
||||
|
||||
use KTXF\Module\ModuleBrowserInterface;
|
||||
use KTXF\Module\Configuration\BrowserModuleContextInterface;
|
||||
use KTXF\Module\Configuration\ModuleContextInterface;
|
||||
use KTXF\Module\ModuleInstanceAbstract;
|
||||
|
||||
/**
|
||||
* File Manager Module
|
||||
*/
|
||||
class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
||||
class Module extends ModuleInstanceAbstract
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
@@ -50,16 +51,11 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
||||
];
|
||||
}
|
||||
|
||||
public function registerBI(): array {
|
||||
return [
|
||||
'handle' => $this->handle(),
|
||||
'namespace' => 'DocumentsManager',
|
||||
'version' => $this->version(),
|
||||
'label' => $this->label(),
|
||||
'author' => $this->author(),
|
||||
'description' => $this->description(),
|
||||
'boot' => 'static/module.mjs',
|
||||
];
|
||||
public function configure(ModuleContextInterface $context): void
|
||||
{
|
||||
if ($context instanceof BrowserModuleContextInterface) {
|
||||
$context->registerModule($this, 'DocumentsManager', 'static/module.mjs');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Generated
+62
-62
@@ -95,12 +95,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
|
||||
"integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.8"
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -110,9 +110,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
|
||||
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
@@ -918,39 +918,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz",
|
||||
"integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==",
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz",
|
||||
"integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@vue/shared": "3.5.42",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@vue/shared": "3.5.40",
|
||||
"entities": "^7.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz",
|
||||
"integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==",
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz",
|
||||
"integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.42",
|
||||
"@vue/shared": "3.5.42"
|
||||
"@vue/compiler-core": "3.5.40",
|
||||
"@vue/shared": "3.5.40"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz",
|
||||
"integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==",
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz",
|
||||
"integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@vue/compiler-core": "3.5.42",
|
||||
"@vue/compiler-dom": "3.5.42",
|
||||
"@vue/compiler-ssr": "3.5.42",
|
||||
"@vue/shared": "3.5.42",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@vue/compiler-core": "3.5.40",
|
||||
"@vue/compiler-dom": "3.5.40",
|
||||
"@vue/compiler-ssr": "3.5.40",
|
||||
"@vue/shared": "3.5.40",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.19",
|
||||
@@ -958,13 +958,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz",
|
||||
"integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==",
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz",
|
||||
"integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.42",
|
||||
"@vue/shared": "3.5.42"
|
||||
"@vue/compiler-dom": "3.5.40",
|
||||
"@vue/shared": "3.5.40"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
@@ -1014,51 +1014,51 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz",
|
||||
"integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==",
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz",
|
||||
"integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.42"
|
||||
"@vue/shared": "3.5.40"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz",
|
||||
"integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==",
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz",
|
||||
"integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.42",
|
||||
"@vue/shared": "3.5.42"
|
||||
"@vue/reactivity": "3.5.40",
|
||||
"@vue/shared": "3.5.40"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz",
|
||||
"integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==",
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz",
|
||||
"integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.42",
|
||||
"@vue/runtime-core": "3.5.42",
|
||||
"@vue/shared": "3.5.42",
|
||||
"@vue/reactivity": "3.5.40",
|
||||
"@vue/runtime-core": "3.5.40",
|
||||
"@vue/shared": "3.5.40",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz",
|
||||
"integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==",
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz",
|
||||
"integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.42",
|
||||
"@vue/runtime-dom": "3.5.42",
|
||||
"@vue/shared": "3.5.42"
|
||||
"@vue/compiler-ssr": "3.5.40",
|
||||
"@vue/runtime-dom": "3.5.40",
|
||||
"@vue/shared": "3.5.40"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz",
|
||||
"integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==",
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz",
|
||||
"integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/tsconfig": {
|
||||
@@ -2320,16 +2320,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.42",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz",
|
||||
"integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==",
|
||||
"version": "3.5.40",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz",
|
||||
"integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.42",
|
||||
"@vue/compiler-sfc": "3.5.42",
|
||||
"@vue/runtime-dom": "3.5.42",
|
||||
"@vue/server-renderer": "3.5.42",
|
||||
"@vue/shared": "3.5.42"
|
||||
"@vue/compiler-dom": "3.5.40",
|
||||
"@vue/compiler-sfc": "3.5.40",
|
||||
"@vue/runtime-dom": "3.5.40",
|
||||
"@vue/server-renderer": "3.5.40",
|
||||
"@vue/shared": "3.5.40"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Ref, ComputedRef } from 'vue'
|
||||
import { useProvidersStore } from '@/stores/providersStore'
|
||||
import { useServicesStore } from '@/stores/servicesStore'
|
||||
import { useNodesStore, ROOT_ID } from '@/stores/nodesStore'
|
||||
import type { ListFilter, ListSort, ListRange } from '@/types/common'
|
||||
import type { ListFilter, ListSort, ListRange, ServiceIdentifier, CollectionIdentifier } from '@/types/common'
|
||||
import type { DocumentInterface } from '@/types/document'
|
||||
import { CollectionObject } from '@/models/collection'
|
||||
import { EntityObject } from '@/models/entity'
|
||||
@@ -16,6 +16,9 @@ import { EntityObject } from '@/models/entity'
|
||||
// Base URL for file manager transfer endpoints
|
||||
const TRANSFER_BASE_URL = '/m/documents_manager'
|
||||
|
||||
// Number of collections+entities requested per page
|
||||
const DEFAULT_PAGE_TALLY = 64
|
||||
|
||||
export interface UseDocumentManagerOptions {
|
||||
providerId: string | Ref<string> | ComputedRef<string>
|
||||
serviceId: string | Ref<string> | ComputedRef<string>
|
||||
@@ -32,6 +35,12 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
const currentProviderId = () => unref(providerId)
|
||||
const currentServiceId = () => unref(serviceId)
|
||||
|
||||
const currentSources = (): (ServiceIdentifier | CollectionIdentifier)[] => [
|
||||
currentLocation.value === ROOT_ID
|
||||
? `${currentProviderId()}:${currentServiceId()}`
|
||||
: `${currentProviderId()}:${currentServiceId()}:${currentLocation.value}`,
|
||||
]
|
||||
|
||||
// Current location (folder being viewed)
|
||||
const currentLocation: Ref<string> = ref(ROOT_ID)
|
||||
|
||||
@@ -39,6 +48,13 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
const isLoading = computed(() => nodesStore.transceiving)
|
||||
const error = computed(() => nodesStore.error)
|
||||
|
||||
// Pagination state for the current location + filter + sort
|
||||
const currentFilter: Ref<ListFilter | undefined> = ref(undefined)
|
||||
const currentSort: Ref<ListSort | undefined> = ref(undefined)
|
||||
const loadedCount = ref(0)
|
||||
const hasMore = ref(false)
|
||||
const isLoadingMore = ref(false)
|
||||
|
||||
// Provider and service
|
||||
const provider = computed(() => providersStore.provider(currentProviderId()))
|
||||
const service = computed(() => servicesStore.service(currentProviderId(), currentServiceId()))
|
||||
@@ -71,7 +87,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
// Navigate to a folder
|
||||
const navigateTo = async (collectionId: string | null) => {
|
||||
currentLocation.value = collectionId || ROOT_ID
|
||||
await refresh()
|
||||
await resetAndLoad(currentFilter.value, currentSort.value)
|
||||
}
|
||||
|
||||
// Navigate up one level
|
||||
@@ -90,28 +106,55 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
await navigateTo(ROOT_ID)
|
||||
}
|
||||
|
||||
// Refresh current location
|
||||
const refresh = async (
|
||||
filter?: ListFilter,
|
||||
sort?: ListSort,
|
||||
range?: ListRange
|
||||
) => {
|
||||
await nodesStore.fetchNodes(
|
||||
currentProviderId(),
|
||||
currentServiceId(),
|
||||
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
||||
// Reset the current location's loaded items and (re)fetch the first page,
|
||||
// optionally with a new filter/sort (e.g. search query or sort field change)
|
||||
const resetAndLoad = async (filter?: ListFilter, sort?: ListSort) => {
|
||||
currentFilter.value = filter
|
||||
currentSort.value = sort
|
||||
loadedCount.value = 0
|
||||
hasMore.value = false
|
||||
|
||||
const range: ListRange = { type: 'tally', anchor: 'absolute', position: 0, tally: DEFAULT_PAGE_TALLY }
|
||||
const result = await nodesStore.fetchNodes(
|
||||
currentSources(),
|
||||
filter,
|
||||
sort,
|
||||
range
|
||||
range,
|
||||
'reset'
|
||||
)
|
||||
|
||||
loadedCount.value = result.collections.length + result.entities.length
|
||||
hasMore.value = !result.isLastPage
|
||||
}
|
||||
|
||||
// Fetch the next page of the current location (infinite scroll "load more")
|
||||
const loadMore = async () => {
|
||||
if (!hasMore.value || isLoadingMore.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoadingMore.value = true
|
||||
try {
|
||||
const range: ListRange = { type: 'tally', anchor: 'absolute', position: loadedCount.value, tally: DEFAULT_PAGE_TALLY }
|
||||
const result = await nodesStore.fetchNodes(
|
||||
currentSources(),
|
||||
currentFilter.value,
|
||||
currentSort.value,
|
||||
range,
|
||||
'append'
|
||||
)
|
||||
|
||||
loadedCount.value += result.collections.length + result.entities.length
|
||||
hasMore.value = !result.isLastPage
|
||||
} finally {
|
||||
isLoadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new folder
|
||||
const createFolder = async (label: string): Promise<CollectionObject> => {
|
||||
return await nodesStore.createCollection(
|
||||
currentProviderId(),
|
||||
currentServiceId(),
|
||||
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
||||
`${currentProviderId()}:${currentServiceId()}:${currentLocation.value}`,
|
||||
{ label, owner: '' }
|
||||
)
|
||||
}
|
||||
@@ -132,9 +175,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
}
|
||||
|
||||
return await nodesStore.createEntity(
|
||||
currentProviderId(),
|
||||
currentServiceId(),
|
||||
currentLocation.value === ROOT_ID ? ROOT_ID : currentLocation.value,
|
||||
`${currentProviderId()}:${currentServiceId()}:${currentLocation.value}`,
|
||||
properties
|
||||
)
|
||||
}
|
||||
@@ -147,7 +188,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
}
|
||||
|
||||
if (node instanceof CollectionObject) {
|
||||
return await nodesStore.updateCollection(currentProviderId(), currentServiceId(), nodeId, {
|
||||
return await nodesStore.updateCollection(`${node.provider}:${node.service}:${nodeId}`, {
|
||||
label: newLabel,
|
||||
owner: node.properties.owner,
|
||||
})
|
||||
@@ -161,7 +202,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
format: node.properties.format,
|
||||
encoding: node.properties.encoding,
|
||||
}
|
||||
return await nodesStore.updateEntity(currentProviderId(), currentServiceId(), node.collection, nodeId, properties)
|
||||
return await nodesStore.updateEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${nodeId}`, properties)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,9 +214,9 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
}
|
||||
|
||||
if (node instanceof CollectionObject) {
|
||||
return await nodesStore.deleteCollection(currentProviderId(), currentServiceId(), nodeId)
|
||||
return await nodesStore.deleteCollection(`${node.provider}:${node.service}:${nodeId}`)
|
||||
} else {
|
||||
return await nodesStore.deleteEntity(currentProviderId(), currentServiceId(), node.collection, nodeId)
|
||||
return await nodesStore.deleteEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${nodeId}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +226,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
if (!node || !(node instanceof EntityObject)) {
|
||||
throw new Error('Entity not found')
|
||||
}
|
||||
return await nodesStore.readEntity(currentProviderId(), currentServiceId(), node.collection || ROOT_ID, entityId)
|
||||
return await nodesStore.readEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${entityId}`)
|
||||
}
|
||||
|
||||
// Write file content
|
||||
@@ -194,7 +235,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
if (!node || !(node instanceof EntityObject)) {
|
||||
throw new Error('Entity not found')
|
||||
}
|
||||
return await nodesStore.writeEntity(currentProviderId(), currentServiceId(), node.collection, entityId, content)
|
||||
return await nodesStore.writeEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${entityId}`, content)
|
||||
}
|
||||
|
||||
// Get a URL suitable for inline viewing (img src / video src)
|
||||
@@ -253,7 +294,7 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
await providersStore.list([currentProviderId()])
|
||||
await servicesStore.list()
|
||||
if (autoFetch) {
|
||||
await refresh()
|
||||
await resetAndLoad()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,6 +303,8 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
currentLocation,
|
||||
isLoading,
|
||||
error,
|
||||
hasMore,
|
||||
isLoadingMore,
|
||||
|
||||
// Provider/Service
|
||||
provider,
|
||||
@@ -279,7 +322,8 @@ export function useDocumentManager(options: UseDocumentManagerOptions) {
|
||||
navigateTo,
|
||||
navigateUp,
|
||||
navigateToRoot,
|
||||
refresh,
|
||||
resetAndLoad,
|
||||
loadMore,
|
||||
|
||||
// Operations
|
||||
createFolder,
|
||||
|
||||
@@ -25,23 +25,11 @@ import type {
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||
import { CollectionObject, CollectionPropertiesObject } from '../models/collection';
|
||||
|
||||
function isCollectionPayload(value: unknown): value is CollectionInterface {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidate = value as Record<string, unknown>;
|
||||
return (
|
||||
('identifier' in candidate || 'provider' in candidate || 'service' in candidate)
|
||||
&& 'properties' in candidate
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create the right collection model class based on provider identifier
|
||||
* Uses provider-specific factory if available, otherwise returns base CollectionObject
|
||||
*/
|
||||
function createCollectionObject(data: CollectionInterface): CollectionObject {
|
||||
export function createCollectionObject(data: CollectionInterface): CollectionObject {
|
||||
const integrationStore = useIntegrationStore();
|
||||
const factoryItem = integrationStore.getItemById('documents_collection_factory', data.provider) as any;
|
||||
const factory = factoryItem?.factory;
|
||||
@@ -68,38 +56,27 @@ export const collectionService = {
|
||||
const serviceList: Record<string, Record<string, CollectionObject>> = {};
|
||||
Object.entries(providerServices).forEach(([serviceId, serviceCollections]) => {
|
||||
const collectionList: Record<string, CollectionObject> = {};
|
||||
Object.entries(serviceCollections as Record<string, unknown>).forEach(([collectionId, collectionData]) => {
|
||||
if (isCollectionPayload(collectionData)) {
|
||||
collectionList[collectionId] = createCollectionObject(collectionData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (collectionData && typeof collectionData === 'object') {
|
||||
Object.entries(collectionData as Record<string, unknown>).forEach(([nestedCollectionId, nestedCollectionData]) => {
|
||||
if (isCollectionPayload(nestedCollectionData)) {
|
||||
collectionList[nestedCollectionId] = createCollectionObject(nestedCollectionData);
|
||||
}
|
||||
});
|
||||
}
|
||||
Object.entries(serviceCollections).forEach(([collectionId, collectionData]) => {
|
||||
collectionList[collectionId] = createCollectionObject(collectionData);
|
||||
});
|
||||
serviceList[serviceId] = collectionList;
|
||||
});
|
||||
providerList[providerId] = serviceList;
|
||||
});
|
||||
|
||||
|
||||
return providerList;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a specific collection by provider and identifier
|
||||
* Retrieve collections by resource identifier
|
||||
*
|
||||
* @param request - fetch request parameters
|
||||
*
|
||||
* @returns Promise with collection object
|
||||
* @returns Promise with collections keyed by resource identifier
|
||||
*/
|
||||
async fetch(request: CollectionFetchRequest): Promise<CollectionObject> {
|
||||
async fetch(request: CollectionFetchRequest): Promise<Record<string, CollectionObject>> {
|
||||
const response = await transceivePost<CollectionFetchRequest, CollectionFetchResponse>('collection.fetch', request);
|
||||
return createCollectionObject(response);
|
||||
return Object.fromEntries(Object.entries(response).map(([target, data]) => [target, createCollectionObject(data)]));
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,7 +35,7 @@ import { EntityObject } from '../models';
|
||||
* Helper to create the right entity model class based on provider identifier
|
||||
* Uses provider-specific factory if available, otherwise returns base EntityObject
|
||||
*/
|
||||
function createEntityObject(data: EntityInterface): EntityObject {
|
||||
export function createEntityObject(data: EntityInterface): EntityObject {
|
||||
const integrationStore = useIntegrationStore();
|
||||
const factoryItem = integrationStore.getItemById('documents_entity_factory', data.provider) as any;
|
||||
const factory = factoryItem?.factory;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { transceiveStream } from './transceive';
|
||||
import type { NodeListRequest, NodeInterface } from '../types/node';
|
||||
import { createCollectionObject } from './collectionService';
|
||||
import { createEntityObject } from './entityService';
|
||||
import type { CollectionObject } from '../models/collection';
|
||||
import type { EntityObject } from '../models/entity';
|
||||
|
||||
export type NodeListResult = (CollectionObject | EntityObject)[];
|
||||
|
||||
export const nodeService = {
|
||||
/** Hydrate a mixed page in the exact order returned by the provider. */
|
||||
async list(request: NodeListRequest = {}): Promise<NodeListResult> {
|
||||
const nodes: NodeListResult = [];
|
||||
const { total } = await transceiveStream<NodeListRequest, NodeInterface>('node.list', request, node => {
|
||||
switch (node['@type']) {
|
||||
case 'document:collection':
|
||||
nodes.push(createCollectionObject(node));
|
||||
break;
|
||||
case 'document:entity':
|
||||
nodes.push(createEntityObject(node));
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported node type in node.list response');
|
||||
}
|
||||
}, { requireEnd: true });
|
||||
if (total !== nodes.length) {
|
||||
throw new Error('Incomplete node.list stream');
|
||||
}
|
||||
// Publish only complete pages so a retry uses the same offset after failure.
|
||||
return nodes;
|
||||
},
|
||||
};
|
||||
|
||||
export default nodeService;
|
||||
@@ -62,7 +62,7 @@ export async function transceiveStream<TRequest, TData>(
|
||||
operation: string,
|
||||
data: TRequest,
|
||||
onData: (data: TData) => void,
|
||||
options?: { user?: string; onStart?: (expected?: number) => void }
|
||||
options?: { user?: string; onStart?: (expected?: number) => void; requireEnd?: boolean }
|
||||
): Promise<{ total: number }> {
|
||||
const request: ApiRequest<TRequest> = {
|
||||
version: API_VERSION,
|
||||
@@ -73,6 +73,7 @@ export async function transceiveStream<TRequest, TData>(
|
||||
};
|
||||
|
||||
let total = 0;
|
||||
let ended = false;
|
||||
|
||||
const dispatch = (line: string): void => {
|
||||
const message = JSON.parse(line) as ApiStreamResponse<TData>;
|
||||
@@ -82,6 +83,7 @@ export async function transceiveStream<TRequest, TData>(
|
||||
options?.onStart?.(message.total);
|
||||
} else if (message.status === 'end') {
|
||||
total = message.total;
|
||||
ended = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -118,6 +120,7 @@ export async function transceiveStream<TRequest, TData>(
|
||||
}
|
||||
}
|
||||
|
||||
buffer += decoder.decode();
|
||||
if (buffer.trim()) dispatch(buffer);
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
@@ -125,5 +128,8 @@ export async function transceiveStream<TRequest, TData>(
|
||||
},
|
||||
});
|
||||
|
||||
if (options?.requireEnd && !ended) {
|
||||
throw new Error(`[${operation}] Stream ended without a completion frame`);
|
||||
}
|
||||
return { total };
|
||||
}
|
||||
|
||||
@@ -40,25 +40,25 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
||||
return `${provider}:${service ?? ''}:${identifier ?? ''}`
|
||||
}
|
||||
|
||||
function collection(provider: string, service: string | number, identifier: string | number, retrieve: boolean = false): CollectionObject | null {
|
||||
const key = identifierKey(provider, service, identifier)
|
||||
function collection(target: CollectionIdentifier, retrieve: boolean = false): CollectionObject | null {
|
||||
const key = target
|
||||
if (retrieve === true && !_collections.value[key]) {
|
||||
console.debug(`[Documents Manager][Store] - Force fetching collection "${key}"`)
|
||||
fetch(provider, service, identifier)
|
||||
void fetch([target])
|
||||
}
|
||||
|
||||
return _collections.value[key] || null
|
||||
}
|
||||
|
||||
function collectionsForService(provider: string, service: string | number): CollectionObject[] {
|
||||
const serviceKeyPrefix = `${provider}:${service}:`
|
||||
function collectionsForService(target: ServiceIdentifier): CollectionObject[] {
|
||||
const serviceKeyPrefix = `${target}:`
|
||||
return Object.entries(_collections.value)
|
||||
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
||||
.map(([_, collectionObj]) => collectionObj)
|
||||
}
|
||||
|
||||
function clearService(provider: string, service: string | number): void {
|
||||
const serviceKeyPrefix = `${provider}:${service}:`
|
||||
function clearService(target: ServiceIdentifier): void {
|
||||
const serviceKeyPrefix = `${target}:`
|
||||
Object.keys(_collections.value)
|
||||
.filter((key) => key.startsWith(serviceKeyPrefix))
|
||||
.forEach((key) => {
|
||||
@@ -70,6 +70,27 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
||||
_collections.value = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge externally-hydrated collections into the store (used by paginated node fetches)
|
||||
*/
|
||||
function mergeMany(collections: CollectionObject[]): void {
|
||||
const hydrated: Record<string, CollectionObject> = {}
|
||||
collections.forEach((collectionObj) => {
|
||||
const key = identifierKey(collectionObj.provider, collectionObj.service, collectionObj.identifier)
|
||||
hydrated[key] = collectionObj
|
||||
})
|
||||
_collections.value = { ..._collections.value, ...hydrated }
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove specific collections by their own identifiers (used to evict a stale page before a reset)
|
||||
*/
|
||||
function removeMany(targets: CollectionIdentifier[]): void {
|
||||
targets.forEach((target) => {
|
||||
delete _collections.value[target]
|
||||
})
|
||||
}
|
||||
|
||||
// Actions
|
||||
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort): Promise<Record<string, CollectionObject>> {
|
||||
transceiving.value = true
|
||||
@@ -98,14 +119,11 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
||||
}
|
||||
}
|
||||
|
||||
async function fetch(provider: string, service: string | number, identifier: string | number): Promise<CollectionObject> {
|
||||
async function fetch(targets: CollectionIdentifier[]): Promise<Record<string, CollectionObject>> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const response = await collectionService.fetch({ targets: [`${provider}:${service}:${identifier}`] })
|
||||
const key = identifierKey(response.provider, response.service, response.identifier)
|
||||
_collections.value[key] = response
|
||||
|
||||
console.debug('[Documents Manager][Store] - Successfully fetched collection:', key)
|
||||
const response = await collectionService.fetch({ targets })
|
||||
_collections.value = { ..._collections.value, ...response }
|
||||
return response
|
||||
} catch (error: any) {
|
||||
console.error('[Documents Manager][Store] - Failed to fetch collection:', error)
|
||||
@@ -130,17 +148,12 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
||||
}
|
||||
|
||||
async function create(
|
||||
provider: string,
|
||||
service: string | number,
|
||||
collection: string | number | null,
|
||||
target: ServiceIdentifier | CollectionIdentifier,
|
||||
properties: CollectionMutableProperties,
|
||||
): Promise<CollectionObject> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const target: CollectionIdentifier | undefined = collection !== null && collection !== undefined && collection !== ''
|
||||
? `${provider}:${service}:${collection}`
|
||||
: undefined
|
||||
const response = await collectionService.create({ provider, service: String(service), target, properties })
|
||||
const response = await collectionService.create({ target, properties })
|
||||
const key = identifierKey(response.provider, response.service, response.identifier)
|
||||
_collections.value[key] = response
|
||||
|
||||
@@ -155,14 +168,12 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
||||
}
|
||||
|
||||
async function update(
|
||||
provider: string,
|
||||
service: string | number,
|
||||
identifier: string | number,
|
||||
target: CollectionIdentifier,
|
||||
properties: CollectionMutableProperties,
|
||||
): Promise<CollectionObject> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const response = await collectionService.update({ target: `${provider}:${service}:${identifier}`, properties })
|
||||
const response = await collectionService.update({ target, properties })
|
||||
const key = identifierKey(response.provider, response.service, response.identifier)
|
||||
_collections.value[key] = response
|
||||
|
||||
@@ -176,17 +187,16 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(provider: string, service: string | number, identifier: string | number): Promise<{ success: boolean }> {
|
||||
async function remove(target: CollectionIdentifier): Promise<{ success: boolean }> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const response = await collectionService.delete({ target: `${provider}:${service}:${identifier}` })
|
||||
const response = await collectionService.delete({ target })
|
||||
const success = response.disposition === 'deleted' || response.disposition === 'moved'
|
||||
if (success) {
|
||||
const key = identifierKey(provider, service, identifier)
|
||||
delete _collections.value[key]
|
||||
delete _collections.value[target]
|
||||
}
|
||||
|
||||
console.debug('[Documents Manager][Store] - Successfully deleted collection:', `${provider}:${service}:${identifier}`)
|
||||
console.debug('[Documents Manager][Store] - Successfully deleted collection:', target)
|
||||
return { success }
|
||||
} catch (error: any) {
|
||||
console.error('[Documents Manager][Store] - Failed to delete collection:', error)
|
||||
@@ -206,6 +216,8 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
||||
collectionsForService,
|
||||
clearService,
|
||||
clearAll,
|
||||
mergeMany,
|
||||
removeMany,
|
||||
list,
|
||||
fetch,
|
||||
extant,
|
||||
|
||||
+48
-62
@@ -49,37 +49,34 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
||||
}
|
||||
|
||||
function entity(
|
||||
provider: string,
|
||||
service: string | number,
|
||||
collection: string | number,
|
||||
identifier: string | number,
|
||||
target: EntityIdentifier,
|
||||
retrieve: boolean = false,
|
||||
): EntityObject | null {
|
||||
const key = identifierKey(provider, service, collection, identifier)
|
||||
const key = target
|
||||
if (retrieve === true && !_entities.value[key]) {
|
||||
console.debug(`[Documents Manager][Store] - Force fetching entity "${key}"`)
|
||||
fetch(provider, service, collection, [identifier])
|
||||
void fetch([target])
|
||||
}
|
||||
|
||||
return _entities.value[key] || null
|
||||
}
|
||||
|
||||
function entitiesForService(provider: string, service: string | number): EntityObject[] {
|
||||
const serviceKeyPrefix = `${provider}:${service}:`
|
||||
function entitiesForService(target: ServiceIdentifier): EntityObject[] {
|
||||
const serviceKeyPrefix = `${target}:`
|
||||
return Object.entries(_entities.value)
|
||||
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
||||
.map(([_, entityObj]) => entityObj)
|
||||
}
|
||||
|
||||
function entitiesForCollection(provider: string, service: string | number, collection: string | number): EntityObject[] {
|
||||
const collectionKeyPrefix = `${provider}:${service}:${collection}:`
|
||||
function entitiesForCollection(target: CollectionIdentifier): EntityObject[] {
|
||||
const collectionKeyPrefix = `${target}:`
|
||||
return Object.entries(_entities.value)
|
||||
.filter(([key]) => key.startsWith(collectionKeyPrefix))
|
||||
.map(([_, entityObj]) => entityObj)
|
||||
}
|
||||
|
||||
function clearService(provider: string, service: string | number): void {
|
||||
const serviceKeyPrefix = `${provider}:${service}:`
|
||||
function clearService(target: ServiceIdentifier): void {
|
||||
const serviceKeyPrefix = `${target}:`
|
||||
Object.keys(_entities.value)
|
||||
.filter((key) => key.startsWith(serviceKeyPrefix))
|
||||
.forEach((key) => {
|
||||
@@ -87,8 +84,8 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function clearCollection(provider: string, service: string | number, collection: string | number): void {
|
||||
const collectionKeyPrefix = `${provider}:${service}:${collection}:`
|
||||
function clearCollection(target: CollectionIdentifier): void {
|
||||
const collectionKeyPrefix = `${target}:`
|
||||
Object.keys(_entities.value)
|
||||
.filter((key) => key.startsWith(collectionKeyPrefix))
|
||||
.forEach((key) => {
|
||||
@@ -100,6 +97,18 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
||||
_entities.value = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge externally-hydrated entities into the store (used by paginated node fetches)
|
||||
*/
|
||||
function mergeMany(entities: EntityObject[]): void {
|
||||
const hydrated: Record<string, EntityObject> = {}
|
||||
entities.forEach((entityObj) => {
|
||||
const key = identifierKey(entityObj.provider, entityObj.service, entityObj.collection, entityObj.identifier)
|
||||
hydrated[key] = entityObj
|
||||
})
|
||||
_entities.value = { ..._entities.value, ...hydrated }
|
||||
}
|
||||
|
||||
// Actions
|
||||
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise<Record<string, EntityObject>> {
|
||||
transceiving.value = true
|
||||
@@ -131,25 +140,13 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
||||
}
|
||||
|
||||
async function fetch(
|
||||
provider: string,
|
||||
service: string | number,
|
||||
collection: string | number,
|
||||
identifiers: (string | number)[],
|
||||
targets: EntityIdentifier[],
|
||||
): Promise<Record<string, EntityObject>> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const targets: EntityIdentifier[] = identifiers.map((identifier) => `${provider}:${service}:${collection}:${identifier}` as EntityIdentifier)
|
||||
const response = await entityService.fetch({ targets })
|
||||
|
||||
const hydrated: Record<string, EntityObject> = {}
|
||||
Object.entries(response).forEach(([identifier, entityObj]) => {
|
||||
const key = identifierKey(provider, service, collection, identifier)
|
||||
hydrated[key] = entityObj
|
||||
_entities.value[key] = entityObj
|
||||
})
|
||||
|
||||
console.debug('[Documents Manager][Store] - Successfully fetched', Object.keys(hydrated).length, 'entities')
|
||||
return hydrated
|
||||
_entities.value = { ..._entities.value, ...response }
|
||||
return response
|
||||
} catch (error: any) {
|
||||
console.error('[Documents Manager][Store] - Failed to fetch entities:', error)
|
||||
throw error
|
||||
@@ -173,15 +170,13 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
||||
}
|
||||
|
||||
async function create(
|
||||
provider: string,
|
||||
service: string | number,
|
||||
collection: string | number,
|
||||
target: CollectionIdentifier,
|
||||
properties: DocumentInterface,
|
||||
options?: Record<string, unknown>,
|
||||
): Promise<EntityObject> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const response = await entityService.create({ target: `${provider}:${service}:${collection}`, properties, options })
|
||||
const response = await entityService.create({ target, properties, options })
|
||||
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
||||
_entities.value[key] = response
|
||||
|
||||
@@ -196,15 +191,12 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
||||
}
|
||||
|
||||
async function update(
|
||||
provider: string,
|
||||
service: string | number,
|
||||
collection: string | number,
|
||||
identifier: string | number,
|
||||
target: EntityIdentifier,
|
||||
properties: DocumentInterface,
|
||||
): Promise<EntityObject> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const response = await entityService.update({ target: `${provider}:${service}:${collection}:${identifier}`, properties })
|
||||
const response = await entityService.update({ target, properties })
|
||||
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
||||
_entities.value[key] = response
|
||||
|
||||
@@ -219,23 +211,22 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
||||
}
|
||||
|
||||
async function remove(
|
||||
provider: string,
|
||||
service: string | number,
|
||||
collection: string | number,
|
||||
identifier: string | number,
|
||||
): Promise<{ success: boolean }> {
|
||||
targets: EntityIdentifier[],
|
||||
): Promise<{ successes: EntityIdentifier[]; failures: EntityIdentifier[] }> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const target: EntityIdentifier = `${provider}:${service}:${collection}:${identifier}`
|
||||
const response = await entityService.delete({ targets: [target] })
|
||||
const success = response[target]?.disposition === 'deleted'
|
||||
if (success) {
|
||||
const key = identifierKey(provider, service, collection, identifier)
|
||||
delete _entities.value[key]
|
||||
const response = await entityService.delete({ targets })
|
||||
const successes: EntityIdentifier[] = []
|
||||
const failures: EntityIdentifier[] = []
|
||||
for (const target of targets) {
|
||||
if (response[target]?.disposition === 'deleted') {
|
||||
delete _entities.value[target]
|
||||
successes.push(target)
|
||||
} else {
|
||||
failures.push(target)
|
||||
}
|
||||
}
|
||||
|
||||
console.debug('[Documents Manager][Store] - Successfully deleted entity:', target)
|
||||
return { success }
|
||||
return { successes, failures }
|
||||
} catch (error: any) {
|
||||
console.error('[Documents Manager][Store] - Failed to delete entity:', error)
|
||||
throw error
|
||||
@@ -277,14 +268,11 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
||||
}
|
||||
|
||||
async function read(
|
||||
provider: string,
|
||||
service: string | number,
|
||||
collection: string | number,
|
||||
identifier: string | number,
|
||||
target: EntityIdentifier,
|
||||
): Promise<string | null> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const response = await entityService.read({ target: `${provider}:${service}:${collection}:${identifier}` })
|
||||
const response = await entityService.read({ target })
|
||||
return response.content
|
||||
} catch (error: any) {
|
||||
console.error('[Documents Manager][Store] - Failed to read entity:', error)
|
||||
@@ -295,15 +283,12 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
||||
}
|
||||
|
||||
async function write(
|
||||
provider: string,
|
||||
service: string | number,
|
||||
collection: string | number,
|
||||
identifier: string | number,
|
||||
target: EntityIdentifier,
|
||||
content: string,
|
||||
): Promise<number> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const response = await entityService.write({ target: `${provider}:${service}:${collection}:${identifier}`, content, encoding: 'base64' })
|
||||
const response = await entityService.write({ target, content, encoding: 'base64' })
|
||||
return response.bytesWritten
|
||||
} catch (error: any) {
|
||||
console.error('[Documents Manager][Store] - Failed to write entity:', error)
|
||||
@@ -325,6 +310,7 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
||||
clearService,
|
||||
clearCollection,
|
||||
clearAll,
|
||||
mergeMany,
|
||||
list,
|
||||
fetch,
|
||||
extant,
|
||||
|
||||
+76
-68
@@ -6,6 +6,7 @@ import { computed, ref, readonly } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import type {
|
||||
CollectionIdentifier,
|
||||
EntityIdentifier,
|
||||
ServiceIdentifier,
|
||||
ListFilter,
|
||||
ListSort,
|
||||
@@ -16,6 +17,7 @@ import type {
|
||||
import { CollectionObject, EntityObject } from '../models'
|
||||
import { useCollectionsStore } from './collectionsStore'
|
||||
import { useEntitiesStore } from './entitiesStore'
|
||||
import { nodeService } from '../services/nodeService'
|
||||
|
||||
export const ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
@@ -46,8 +48,8 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
|
||||
function getServiceNodes(providerId: string, serviceId: string | number): NodeRecord[] {
|
||||
return [
|
||||
...collectionsStore.collectionsForService(providerId, serviceId),
|
||||
...entitiesStore.entitiesForService(providerId, serviceId),
|
||||
...collectionsStore.collectionsForService(`${providerId}:${serviceId}`),
|
||||
...entitiesStore.entitiesForService(`${providerId}:${serviceId}`),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -105,20 +107,13 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
}
|
||||
|
||||
async function fetchCollections(
|
||||
providerId: string,
|
||||
serviceId: string | number,
|
||||
collectionId: string | number | null,
|
||||
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
||||
filter?: ListFilter,
|
||||
sort?: ListSort,
|
||||
): Promise<CollectionObject[]> {
|
||||
error.value = null
|
||||
try {
|
||||
const sources: (ServiceIdentifier | CollectionIdentifier)[] = collectionId === null
|
||||
? [`${providerId}:${serviceId}`]
|
||||
: [`${providerId}:${serviceId}:${collectionId}`]
|
||||
|
||||
await collectionsStore.list(sources, filter, sort)
|
||||
return collectionsStore.collectionsForService(providerId, serviceId)
|
||||
return Object.values(await collectionsStore.list(sources, filter, sort))
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to fetch collections'
|
||||
throw e
|
||||
@@ -126,43 +121,76 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
}
|
||||
|
||||
async function fetchEntities(
|
||||
providerId: string,
|
||||
serviceId: string | number,
|
||||
collectionId: string | number | null,
|
||||
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
||||
filter?: ListFilter,
|
||||
sort?: ListSort,
|
||||
range?: ListRange,
|
||||
): Promise<EntityObject[]> {
|
||||
error.value = null
|
||||
try {
|
||||
const sources: (ServiceIdentifier | CollectionIdentifier)[] = collectionId === null
|
||||
? [`${providerId}:${serviceId}`]
|
||||
: [`${providerId}:${serviceId}:${collectionId}`]
|
||||
|
||||
await entitiesStore.list(sources, filter, sort, range)
|
||||
return entitiesStore.entitiesForCollection(providerId, serviceId, collectionId)
|
||||
return Object.values(await entitiesStore.list(sources, filter, sort, range))
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to fetch entities'
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a unified node listing, optionally limited to a page by range.
|
||||
*/
|
||||
async function fetchNodes(
|
||||
providerId: string,
|
||||
serviceId: string | number,
|
||||
parentId: string | number | null = ROOT_ID,
|
||||
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
||||
filter?: ListFilter,
|
||||
sort?: ListSort,
|
||||
range?: ListRange,
|
||||
): Promise<NodeRecord[]> {
|
||||
mode: 'reset' | 'append' = 'reset',
|
||||
): Promise<{ collections: CollectionObject[]; entities: EntityObject[]; isLastPage: boolean }> {
|
||||
error.value = null
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchCollections(providerId, serviceId, parentId, filter, sort),
|
||||
fetchEntities(providerId, serviceId, parentId, filter, sort, range),
|
||||
])
|
||||
const result = await nodeService.list({ sources, filter, sort, range })
|
||||
|
||||
return getChildren(providerId, serviceId, parentId)
|
||||
// Service selectors list the root; collection selectors list that folder.
|
||||
// Omitted sources list roots across services, not every cached descendant.
|
||||
const locations = new Map<string, { provider: string; service: string; parent: string }>()
|
||||
if (sources?.length) {
|
||||
for (const source of sources) {
|
||||
const [provider, service, parent] = source.split(':')
|
||||
const location = { provider: provider!, service: service!, parent: isRoot(parent) ? ROOT_ID : parent! }
|
||||
locations.set(`${location.provider}:${location.service}:${location.parent}`, location)
|
||||
}
|
||||
} else {
|
||||
for (const node of [...nodeList.value, ...result]) {
|
||||
const location = { provider: node.provider, service: String(node.service), parent: ROOT_ID }
|
||||
locations.set(`${location.provider}:${location.service}:${ROOT_ID}`, location)
|
||||
}
|
||||
}
|
||||
|
||||
// Replace only the selected locations, and only after the stream succeeds.
|
||||
if (mode === 'reset') {
|
||||
for (const { provider, service, parent } of locations.values()) {
|
||||
const staleIds = getChildCollections(provider, service, parent)
|
||||
.map((node): CollectionIdentifier => `${node.provider}:${node.service}:${node.identifier}`)
|
||||
collectionsStore.removeMany(staleIds)
|
||||
entitiesStore.clearCollection(`${provider}:${service}:${parent}`)
|
||||
}
|
||||
}
|
||||
|
||||
const pageCollections = result.filter((node): node is CollectionObject => node instanceof CollectionObject)
|
||||
const pageEntities = result.filter((node): node is EntityObject => node instanceof EntityObject)
|
||||
|
||||
collectionsStore.mergeMany(pageCollections)
|
||||
entitiesStore.mergeMany(pageEntities)
|
||||
|
||||
// The backend applies the range independently to each selected location.
|
||||
const counts = new Map<string, number>()
|
||||
for (const node of result) {
|
||||
const parent = isRoot(node.collection) ? ROOT_ID : String(node.collection)
|
||||
const key = `${node.provider}:${node.service}:${parent}`
|
||||
counts.set(key, (counts.get(key) ?? 0) + 1)
|
||||
}
|
||||
const isLastPage = range === undefined || [...counts.values()].every(count => count < range.tally)
|
||||
|
||||
return { collections: pageCollections, entities: pageEntities, isLastPage }
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to fetch nodes'
|
||||
throw e
|
||||
@@ -170,14 +198,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
}
|
||||
|
||||
async function createCollection(
|
||||
providerId: string,
|
||||
serviceId: string | number,
|
||||
parentCollectionId: string | number | null,
|
||||
target: ServiceIdentifier | CollectionIdentifier,
|
||||
properties: CollectionMutableProperties,
|
||||
): Promise<CollectionObject> {
|
||||
error.value = null
|
||||
try {
|
||||
return await collectionsStore.create(providerId, serviceId, parentCollectionId, properties)
|
||||
return await collectionsStore.create(target, properties)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to create collection'
|
||||
throw e
|
||||
@@ -185,14 +211,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
}
|
||||
|
||||
async function updateCollection(
|
||||
providerId: string,
|
||||
serviceId: string | number,
|
||||
identifier: string | number,
|
||||
target: CollectionIdentifier,
|
||||
properties: CollectionMutableProperties,
|
||||
): Promise<CollectionObject> {
|
||||
error.value = null
|
||||
try {
|
||||
return await collectionsStore.update(providerId, serviceId, identifier, properties)
|
||||
return await collectionsStore.update(target, properties)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to update collection'
|
||||
throw e
|
||||
@@ -200,13 +224,11 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
}
|
||||
|
||||
async function deleteCollection(
|
||||
providerId: string,
|
||||
serviceId: string | number,
|
||||
identifier: string | number,
|
||||
target: CollectionIdentifier,
|
||||
): Promise<boolean> {
|
||||
error.value = null
|
||||
try {
|
||||
const response = await collectionsStore.delete(providerId, serviceId, identifier)
|
||||
const response = await collectionsStore.delete(target)
|
||||
return response.success
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to delete collection'
|
||||
@@ -215,15 +237,13 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
}
|
||||
|
||||
async function createEntity(
|
||||
providerId: string,
|
||||
serviceId: string | number,
|
||||
collectionId: string | number,
|
||||
target: CollectionIdentifier,
|
||||
properties: DocumentInterface,
|
||||
options?: Record<string, unknown>,
|
||||
): Promise<EntityObject> {
|
||||
error.value = null
|
||||
try {
|
||||
return await entitiesStore.create(providerId, serviceId, collectionId, properties, options)
|
||||
return await entitiesStore.create(target, properties, options)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to create entity'
|
||||
throw e
|
||||
@@ -231,15 +251,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
}
|
||||
|
||||
async function updateEntity(
|
||||
providerId: string,
|
||||
serviceId: string | number,
|
||||
collectionId: string | number,
|
||||
identifier: string | number,
|
||||
target: EntityIdentifier,
|
||||
properties: DocumentInterface,
|
||||
): Promise<EntityObject> {
|
||||
error.value = null
|
||||
try {
|
||||
return await entitiesStore.update(providerId, serviceId, collectionId, identifier, properties)
|
||||
return await entitiesStore.update(target, properties)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to update entity'
|
||||
throw e
|
||||
@@ -247,15 +264,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
}
|
||||
|
||||
async function deleteEntity(
|
||||
providerId: string,
|
||||
serviceId: string | number,
|
||||
collectionId: string | number,
|
||||
identifier: string | number,
|
||||
target: EntityIdentifier,
|
||||
): Promise<boolean> {
|
||||
error.value = null
|
||||
try {
|
||||
const response = await entitiesStore.delete(providerId, serviceId, collectionId, identifier)
|
||||
return response.success
|
||||
const response = await entitiesStore.delete([target])
|
||||
return response.successes.includes(target)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to delete entity'
|
||||
throw e
|
||||
@@ -263,14 +277,11 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
}
|
||||
|
||||
async function readEntity(
|
||||
providerId: string,
|
||||
serviceId: string | number,
|
||||
collectionId: string | number,
|
||||
identifier: string | number,
|
||||
target: EntityIdentifier,
|
||||
): Promise<string | null> {
|
||||
error.value = null
|
||||
try {
|
||||
return await entitiesStore.read(providerId, serviceId, collectionId, identifier)
|
||||
return await entitiesStore.read(target)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to read entity'
|
||||
throw e
|
||||
@@ -278,15 +289,12 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
}
|
||||
|
||||
async function writeEntity(
|
||||
providerId: string,
|
||||
serviceId: string | number,
|
||||
collectionId: string | number,
|
||||
identifier: string | number,
|
||||
target: EntityIdentifier,
|
||||
content: string,
|
||||
): Promise<number> {
|
||||
error.value = null
|
||||
try {
|
||||
return await entitiesStore.write(providerId, serviceId, collectionId, identifier, content)
|
||||
return await entitiesStore.write(target, content)
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to write entity'
|
||||
throw e
|
||||
@@ -294,8 +302,8 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
||||
}
|
||||
|
||||
function clearServiceNodes(providerId: string, serviceId: string | number): void {
|
||||
collectionsStore.clearService(providerId, serviceId)
|
||||
entitiesStore.clearService(providerId, serviceId)
|
||||
collectionsStore.clearService(`${providerId}:${serviceId}`)
|
||||
entitiesStore.clearService(`${providerId}:${serviceId}`)
|
||||
}
|
||||
|
||||
function clearNodes(): void {
|
||||
|
||||
@@ -66,7 +66,7 @@ export interface CollectionFetchRequest {
|
||||
targets: CollectionIdentifier[];
|
||||
}
|
||||
|
||||
export interface CollectionFetchResponse extends CollectionInterface {}
|
||||
export type CollectionFetchResponse = Record<CollectionIdentifier, CollectionInterface>;
|
||||
|
||||
/**
|
||||
* Collection extant
|
||||
@@ -87,9 +87,7 @@ export interface CollectionExtantResponse {
|
||||
* Collection create
|
||||
*/
|
||||
export interface CollectionCreateRequest {
|
||||
provider: string;
|
||||
service: string | number;
|
||||
target?: CollectionIdentifier | null; // Parent collection identifier (absent for root)
|
||||
target: ServiceIdentifier | CollectionIdentifier; // Service root or parent collection
|
||||
properties: CollectionMutableProperties;
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
@@ -139,4 +137,4 @@ export interface CollectionMoveRequest {
|
||||
target?: CollectionIdentifier | null; // Destination parent (absent for root)
|
||||
}
|
||||
|
||||
export interface CollectionMoveResponse extends CollectionInterface {}
|
||||
export interface CollectionMoveResponse extends CollectionInterface {}
|
||||
|
||||
@@ -3,4 +3,5 @@ export type * from './provider';
|
||||
export type * from './service';
|
||||
export type * from './collection';
|
||||
export type * from './entity';
|
||||
export type * from './node';
|
||||
export type * from './document';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Node (unified collections + entities) type definitions
|
||||
*/
|
||||
import type { CollectionIdentifier, ListFilter, ListRange, ListSort, ServiceIdentifier } from './common';
|
||||
import type { CollectionInterface } from './collection';
|
||||
import type { EntityInterface } from './entity';
|
||||
|
||||
/**
|
||||
* Node list
|
||||
*/
|
||||
export interface NodeListRequest {
|
||||
sources?: (ServiceIdentifier | CollectionIdentifier)[];
|
||||
filter?: ListFilter;
|
||||
sort?: ListSort;
|
||||
range?: ListRange;
|
||||
}
|
||||
|
||||
export type NodeInterface =
|
||||
| (Omit<CollectionInterface, '@type'> & { '@type': 'document:collection' })
|
||||
| (Omit<EntityInterface, '@type'> & { '@type': 'document:entity' });
|
||||
|
||||
/** Payload of each data frame in the node.list NDJSON stream. */
|
||||
export type NodeListResponse = NodeInterface;
|
||||
Reference in New Issue
Block a user