Compare commits
1 Commits
main
..
8c363c7f6f
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c363c7f6f |
@@ -1,42 +0,0 @@
|
|||||||
name: Build Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Retrieve Server Install Action
|
|
||||||
uses: actions/checkout@v6.0.2
|
|
||||||
with:
|
|
||||||
repository: Nodarx/action-server-install
|
|
||||||
ref: main
|
|
||||||
path: action-server-install
|
|
||||||
github-server-url: https://git.ktrix.dev
|
|
||||||
|
|
||||||
- name: Install Server
|
|
||||||
uses: ./action-server-install
|
|
||||||
with:
|
|
||||||
install-php: 'false'
|
|
||||||
install-node: 'true'
|
|
||||||
php-version: '8.5'
|
|
||||||
node-version: '24'
|
|
||||||
server-path: './server'
|
|
||||||
|
|
||||||
- name: Checkout Pull Request
|
|
||||||
uses: actions/checkout@v6.0.2
|
|
||||||
with:
|
|
||||||
repository: ${{ github.repository }}
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
path: server/modules/documents_manager
|
|
||||||
github-server-url: https://git.ktrix.dev
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
working-directory: server/modules/documents_manager
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: npm run build
|
|
||||||
working-directory: server/modules/documents_manager
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
name: JS Unit Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Retrieve Server Install Action
|
|
||||||
uses: actions/checkout@v6.0.2
|
|
||||||
with:
|
|
||||||
repository: Nodarx/action-server-install
|
|
||||||
ref: main
|
|
||||||
path: action-server-install
|
|
||||||
github-server-url: https://git.ktrix.dev
|
|
||||||
|
|
||||||
- name: Install Server
|
|
||||||
uses: ./action-server-install
|
|
||||||
with:
|
|
||||||
install-php: 'false'
|
|
||||||
install-node: 'true'
|
|
||||||
php-version: '8.5'
|
|
||||||
node-version: '24'
|
|
||||||
server-path: './server'
|
|
||||||
|
|
||||||
- name: Checkout Pull Request
|
|
||||||
uses: actions/checkout@v6.0.2
|
|
||||||
with:
|
|
||||||
repository: ${{ github.repository }}
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
path: server/modules/documents_manager
|
|
||||||
github-server-url: https://git.ktrix.dev
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
working-directory: server/modules/documents_manager
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: npm run test:unit
|
|
||||||
working-directory: server/modules/documents_manager
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
name: PHP Integration Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Integration Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
services:
|
|
||||||
mongo:
|
|
||||||
image: mongo:8
|
|
||||||
options: >-
|
|
||||||
--health-cmd "mongosh --quiet --eval \"db.adminCommand('ping')\""
|
|
||||||
--health-interval 5s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 12
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Retrieve Server Install Action
|
|
||||||
uses: actions/checkout@v6.0.2
|
|
||||||
with:
|
|
||||||
repository: Nodarx/action-server-install
|
|
||||||
ref: main
|
|
||||||
path: action-server-install
|
|
||||||
github-server-url: https://git.ktrix.dev
|
|
||||||
|
|
||||||
- name: Install server
|
|
||||||
uses: ./action-server-install
|
|
||||||
with:
|
|
||||||
install-php: 'true'
|
|
||||||
php-version: '8.5'
|
|
||||||
server-path: './server'
|
|
||||||
database-uri: 'mongodb://mongo:27017/?tls=false'
|
|
||||||
database-name: 'ktrix_ci'
|
|
||||||
app-environment: 'test'
|
|
||||||
|
|
||||||
- name: Checkout module under test
|
|
||||||
uses: actions/checkout@v6.0.2
|
|
||||||
with:
|
|
||||||
repository: ${{ github.repository }}
|
|
||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
||||||
path: server/modules/documents_manager
|
|
||||||
github-server-url: https://git.ktrix.dev
|
|
||||||
|
|
||||||
- name: Install module dependencies
|
|
||||||
run: composer install --prefer-dist --no-progress
|
|
||||||
working-directory: server/modules/documents_manager
|
|
||||||
|
|
||||||
- name: Install and enable module
|
|
||||||
working-directory: server
|
|
||||||
run: |
|
|
||||||
php bin/console module:install documents_manager
|
|
||||||
php bin/console module:enable documents_manager
|
|
||||||
|
|
||||||
- name: Run integration tests
|
|
||||||
working-directory: server/modules/documents_manager
|
|
||||||
run: composer test:integration
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
name: PHP Unit Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Retrieve Server Install Action
|
|
||||||
uses: actions/checkout@v6.0.2
|
|
||||||
with:
|
|
||||||
repository: Nodarx/action-server-install
|
|
||||||
ref: main
|
|
||||||
path: action-server-install
|
|
||||||
github-server-url: https://git.ktrix.dev
|
|
||||||
|
|
||||||
- name: Install Server
|
|
||||||
uses: ./action-server-install
|
|
||||||
with:
|
|
||||||
install-php: 'true'
|
|
||||||
install-node: 'false'
|
|
||||||
php-version: '8.5'
|
|
||||||
node-version: '24'
|
|
||||||
server-path: './server'
|
|
||||||
|
|
||||||
- name: Checkout Pull Request
|
|
||||||
uses: actions/checkout@v6.0.2
|
|
||||||
with:
|
|
||||||
repository: ${{ github.repository }}
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
path: server/modules/documents_manager
|
|
||||||
github-server-url: https://git.ktrix.dev
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: composer install --prefer-dist --no-progress
|
|
||||||
working-directory: server/modules/documents_manager
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: composer test:unit
|
|
||||||
working-directory: server/modules/documents_manager
|
|
||||||
@@ -25,17 +25,11 @@ jobs:
|
|||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
|
|
||||||
- name: Install Renovate
|
- name: Install Renovate
|
||||||
run: |
|
run: npm install -g renovate
|
||||||
npm install --global --no-audit --fund=false \
|
|
||||||
--prefix "${{ runner.temp }}/renovate-npm" \
|
|
||||||
--cache "${{ runner.temp }}/renovate-npm-cache" \
|
|
||||||
renovate
|
|
||||||
"${{ runner.temp }}/renovate-npm/bin/renovate" --version
|
|
||||||
|
|
||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
env:
|
env:
|
||||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
RENOVATE_PLATFORM: gitea
|
RENOVATE_PLATFORM: gitea
|
||||||
RENOVATE_ENDPOINT: https://git.ktrix.dev/api/v1
|
RENOVATE_ENDPOINT: https://git.ktrix.dev/api/v1
|
||||||
run: |
|
run: renovate ${{ gitea.repository }}
|
||||||
"${{ runner.temp }}/renovate-npm/bin/renovate" ${{ gitea.repository }}
|
|
||||||
+4
-2
@@ -12,10 +12,12 @@ node_modules/
|
|||||||
/static/
|
/static/
|
||||||
|
|
||||||
# Backend development
|
# Backend development
|
||||||
/vendor/
|
|
||||||
/lib/vendor/
|
/lib/vendor/
|
||||||
coverage/
|
coverage/
|
||||||
*.cache
|
phpunit.xml.cache
|
||||||
|
.phpunit.result.cache
|
||||||
|
.php-cs-fixer.cache
|
||||||
|
.phpstan.cache
|
||||||
.phpactor/
|
.phpactor/
|
||||||
|
|
||||||
# Editors
|
# Editors
|
||||||
|
|||||||
+2
-14
@@ -10,29 +10,17 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"optimize-autoloader": true,
|
"optimize-autoloader": true,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "8.3"
|
"php": "8.2"
|
||||||
},
|
},
|
||||||
"autoloader-suffix": "DocumentsManager",
|
"autoloader-suffix": "DocumentsManager",
|
||||||
"vendor-dir": "lib/vendor"
|
"vendor-dir": "lib/vendor"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.3 <=8.5"
|
"php": ">=8.2 <=8.5"
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^12.0"
|
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"KTXM\\DocumentsManager\\": "lib/"
|
"KTXM\\DocumentsManager\\": "lib/"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"autoload-dev": {
|
|
||||||
"psr-4": {
|
|
||||||
"KTXT\\DocumentsManager\\Tests\\": "tests/php/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"test:unit": "phpunit --configuration tests/php/phpunit.xml --testsuite \"Unit Tests\" --colors=always --testdox",
|
|
||||||
"test:integration": "phpunit --configuration tests/php/phpunit.xml --testsuite \"Integration Tests\" --colors=always --testdox"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
-1705
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,14 +12,9 @@ namespace KTXM\DocumentsManager\Controllers;
|
|||||||
use KTXC\Http\Response\JsonResponse;
|
use KTXC\Http\Response\JsonResponse;
|
||||||
use KTXC\Http\Response\Response;
|
use KTXC\Http\Response\Response;
|
||||||
use KTXC\Http\Response\StreamedResponse;
|
use KTXC\Http\Response\StreamedResponse;
|
||||||
use KTXC\Context\IdentityContextInterface;
|
use KTXC\SessionIdentity;
|
||||||
use KTXC\Context\TenantContextInterface;
|
use KTXC\SessionTenant;
|
||||||
use KTXF\Controller\ControllerAbstract;
|
use KTXF\Controller\ControllerAbstract;
|
||||||
use KTXF\Documents\Collection\CollectionBaseInterface;
|
|
||||||
use KTXF\Documents\Entity\EntityBaseInterface;
|
|
||||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
|
||||||
use KTXF\Resource\Identifier\EntityIdentifier;
|
|
||||||
use KTXF\Resource\Identifier\ResourceIdentifiers;
|
|
||||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||||
use KTXM\DocumentsManager\Manager;
|
use KTXM\DocumentsManager\Manager;
|
||||||
use KTXM\DocumentsManager\Transfer\StreamingZip;
|
use KTXM\DocumentsManager\Transfer\StreamingZip;
|
||||||
@@ -39,8 +34,8 @@ use Throwable;
|
|||||||
class TransferController extends ControllerAbstract
|
class TransferController extends ControllerAbstract
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TenantContextInterface $tenantContext,
|
private readonly SessionTenant $tenantIdentity,
|
||||||
private readonly IdentityContextInterface $identityContext,
|
private readonly SessionIdentity $userIdentity,
|
||||||
private readonly Manager $manager,
|
private readonly Manager $manager,
|
||||||
private readonly LoggerInterface $logger
|
private readonly LoggerInterface $logger
|
||||||
) {}
|
) {}
|
||||||
@@ -50,29 +45,44 @@ class TransferController extends ControllerAbstract
|
|||||||
*
|
*
|
||||||
* GET /download/entity/{provider}/{service}/{collection}/{identifier}
|
* GET /download/entity/{provider}/{service}/{collection}/{identifier}
|
||||||
*/
|
*/
|
||||||
#[AuthenticatedRoute('/download/entity/{provider}/{service}/{collection}/{identifier}', name: 'document_manager.download.entity', methods: ['GET'])]
|
#[AuthenticatedRoute(
|
||||||
|
'/download/entity/{provider}/{service}/{collection}/{identifier}',
|
||||||
|
name: 'document_manager.download.entity',
|
||||||
|
methods: ['GET']
|
||||||
|
)]
|
||||||
public function downloadEntity(string $provider, string $service, string $collection, string $identifier): Response {
|
public function downloadEntity(string $provider, string $service, string $collection, string $identifier): Response {
|
||||||
$tenantId = $this->tenantContext->identifier();
|
$tenantId = $this->tenantIdentity->identifier();
|
||||||
$userId = $this->identityContext->identifier();
|
$userId = $this->userIdentity->identifier();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$target = new EntityIdentifier($provider, $service, $collection, $identifier);
|
|
||||||
|
|
||||||
// Fetch entity metadata
|
// Fetch entity metadata
|
||||||
/** @var EntityBaseInterface[] $entities */
|
$entities = $this->manager->entityFetch(
|
||||||
$entities = $this->manager->entityFetchBulk($tenantId, $userId, $target);
|
$tenantId,
|
||||||
|
$userId,
|
||||||
|
$provider,
|
||||||
|
$service,
|
||||||
|
$collection,
|
||||||
|
[$identifier]
|
||||||
|
);
|
||||||
|
|
||||||
if (!isset($entities[(string) $target])) {
|
if (empty($entities) || !isset($entities[$identifier])) {
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'error' => ['code' => 404, 'message' => 'File not found']
|
'error' => ['code' => 404, 'message' => 'File not found']
|
||||||
], Response::HTTP_NOT_FOUND);
|
], Response::HTTP_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
$entity = $entities[(string) $target];
|
$entity = $entities[$identifier];
|
||||||
|
|
||||||
// Get the stream
|
// Get the stream
|
||||||
$stream = $this->manager->entityReadStream($tenantId, $userId, $target);
|
$stream = $this->manager->entityReadStream(
|
||||||
|
$tenantId,
|
||||||
|
$userId,
|
||||||
|
$provider,
|
||||||
|
$service,
|
||||||
|
$collection,
|
||||||
|
$identifier
|
||||||
|
);
|
||||||
|
|
||||||
if ($stream === null) {
|
if ($stream === null) {
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
@@ -120,44 +130,19 @@ class TransferController extends ControllerAbstract
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[AuthenticatedRoute('/preview/entity/{provider}/{service}/{collection}/{identifier}', name: 'document_manager.preview.entity', methods: ['GET'])]
|
|
||||||
public function previewEntity(string $provider, string $service, string $collection, string $identifier, string $variant = 'inline'): Response {
|
|
||||||
try {
|
|
||||||
$preview = $this->manager->entityPreview(
|
|
||||||
$this->tenantContext->identifier(),
|
|
||||||
$this->identityContext->identifier(),
|
|
||||||
new EntityIdentifier($provider, $service, $collection, $identifier),
|
|
||||||
$variant,
|
|
||||||
);
|
|
||||||
if ($preview === null) {
|
|
||||||
return new Response('', Response::HTTP_NO_CONTENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = new StreamedResponse($preview->resource->stream());
|
|
||||||
$response->headers->set('Content-Type', $preview->resource->mimeType());
|
|
||||||
$response->headers->set('Content-Length', (string) $preview->size);
|
|
||||||
$response->headers->set('ETag', '"' . trim($preview->etag, '"') . '"');
|
|
||||||
$response->headers->set('Cache-Control', 'private, max-age=3600');
|
|
||||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
|
||||||
return $response;
|
|
||||||
} catch (Throwable $throwable) {
|
|
||||||
$this->logger->error('Document preview failed', ['exception' => $throwable]);
|
|
||||||
return new JsonResponse([
|
|
||||||
'status' => 'error',
|
|
||||||
'error' => ['code' => 500, 'message' => 'Preview unavailable'],
|
|
||||||
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download multiple files as a ZIP archive
|
* Download multiple files as a ZIP archive
|
||||||
*
|
*
|
||||||
* GET /download/archive?provider=...&service=...&ids[]=...&ids[]=...
|
* GET /download/archive?provider=...&service=...&ids[]=...&ids[]=...
|
||||||
*/
|
*/
|
||||||
#[AuthenticatedRoute('/download/archive', name: 'manager.download.archive', methods: ['GET'])]
|
#[AuthenticatedRoute(
|
||||||
|
'/download/archive',
|
||||||
|
name: 'manager.download.archive',
|
||||||
|
methods: ['GET']
|
||||||
|
)]
|
||||||
public function downloadArchive(string $provider, string $service, array $ids = [], ?string $collection = null, string $name = 'download'): Response {
|
public function downloadArchive(string $provider, string $service, array $ids = [], ?string $collection = null, string $name = 'download'): Response {
|
||||||
$tenantId = $this->tenantContext->identifier();
|
$tenantId = $this->tenantIdentity->identifier();
|
||||||
$userId = $this->identityContext->identifier();
|
$userId = $this->userIdentity->identifier();
|
||||||
|
|
||||||
if (empty($ids)) {
|
if (empty($ids)) {
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
@@ -194,7 +179,10 @@ class TransferController extends ControllerAbstract
|
|||||||
$stream = $this->manager->entityReadStream(
|
$stream = $this->manager->entityReadStream(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
new EntityIdentifier($provider, $service, (string)$file['collection'], (string)$file['id'])
|
$provider,
|
||||||
|
$service,
|
||||||
|
$file['collection'],
|
||||||
|
$file['id']
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($stream !== null) {
|
if ($stream !== null) {
|
||||||
@@ -236,10 +224,14 @@ class TransferController extends ControllerAbstract
|
|||||||
*
|
*
|
||||||
* GET /download/collection/{provider}/{service}/{identifier}
|
* GET /download/collection/{provider}/{service}/{identifier}
|
||||||
*/
|
*/
|
||||||
#[AuthenticatedRoute('/download/collection/{provider}/{service}/{identifier}', name: 'manager.download.collection', methods: ['GET'])]
|
#[AuthenticatedRoute(
|
||||||
|
'/download/collection/{provider}/{service}/{identifier}',
|
||||||
|
name: 'manager.download.collection',
|
||||||
|
methods: ['GET']
|
||||||
|
)]
|
||||||
public function downloadCollection(string $provider, string $service, string $identifier): Response {
|
public function downloadCollection(string $provider, string $service, string $identifier): Response {
|
||||||
$tenantId = $this->tenantContext->identifier();
|
$tenantId = $this->tenantIdentity->identifier();
|
||||||
$userId = $this->identityContext->identifier();
|
$userId = $this->userIdentity->identifier();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch collection metadata
|
// Fetch collection metadata
|
||||||
@@ -247,7 +239,9 @@ class TransferController extends ControllerAbstract
|
|||||||
$collection = $this->manager->collectionFetch(
|
$collection = $this->manager->collectionFetch(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
new CollectionIdentifier($provider, $service, $identifier)
|
$provider,
|
||||||
|
$service,
|
||||||
|
$identifier
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($collection === null) {
|
if ($collection === null) {
|
||||||
@@ -257,7 +251,7 @@ class TransferController extends ControllerAbstract
|
|||||||
], Response::HTTP_NOT_FOUND);
|
], Response::HTTP_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
$folderName = $collection->getProperties()->getLabel() ?? 'folder';
|
$folderName = $collection->getLabel() ?? 'folder';
|
||||||
$archiveName = $this->sanitizeFilename($folderName) . '.zip';
|
$archiveName = $this->sanitizeFilename($folderName) . '.zip';
|
||||||
|
|
||||||
// Build recursive file list
|
// Build recursive file list
|
||||||
@@ -281,7 +275,10 @@ class TransferController extends ControllerAbstract
|
|||||||
$stream = $this->manager->entityReadStream(
|
$stream = $this->manager->entityReadStream(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
new EntityIdentifier($provider, $service, (string)$file['collection'], (string)$file['id'])
|
$provider,
|
||||||
|
$service,
|
||||||
|
$file['collection'],
|
||||||
|
$file['id']
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($stream !== null) {
|
if ($stream !== null) {
|
||||||
@@ -327,22 +324,24 @@ class TransferController extends ControllerAbstract
|
|||||||
foreach ($ids as $id) {
|
foreach ($ids as $id) {
|
||||||
// Try as entity first
|
// Try as entity first
|
||||||
if ($collection !== null) {
|
if ($collection !== null) {
|
||||||
$target = new EntityIdentifier($provider, $service, $collection, (string) $id);
|
|
||||||
/** @var EntityBaseInterface[] $entities */
|
/** @var EntityBaseInterface[] $entities */
|
||||||
$entities = $this->manager->entityFetchBulk(
|
$entities = $this->manager->entityFetch(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
$target
|
$provider,
|
||||||
|
$service,
|
||||||
|
$collection,
|
||||||
|
[$id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isset($entities[(string) $target])) {
|
if (!empty($entities) && isset($entities[$id])) {
|
||||||
$entity = $entities[(string) $target];
|
$entity = $entities[$id];
|
||||||
$files[] = [
|
$files[] = [
|
||||||
'type' => 'file',
|
'type' => 'file',
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
'collection' => $collection,
|
'collection' => $collection,
|
||||||
'path' => $entity->getProperties()->getLabel() ?? $id,
|
'path' => $entity->getLabel() ?? $id,
|
||||||
'modTime' => $entity->modified()?->getTimestamp(),
|
'modTime' => $entity->modifiedOn()?->getTimestamp(),
|
||||||
];
|
];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -353,17 +352,19 @@ class TransferController extends ControllerAbstract
|
|||||||
$collectionNode = $this->manager->collectionFetch(
|
$collectionNode = $this->manager->collectionFetch(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
new CollectionIdentifier($provider, $service, (string)$id)
|
$provider,
|
||||||
|
$service,
|
||||||
|
$id
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($collectionNode !== null) {
|
if ($collectionNode !== null) {
|
||||||
$folderName = $collectionNode->getProperties()->getLabel() ?? $id;
|
$folderName = $collectionNode->getLabel() ?? $id;
|
||||||
$subFiles = $this->resolveCollectionContents(
|
$subFiles = $this->resolveCollectionContents(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
$provider,
|
$provider,
|
||||||
$service,
|
$service,
|
||||||
(string)$id,
|
$id,
|
||||||
$folderName
|
$folderName
|
||||||
);
|
);
|
||||||
$files = array_merge($files, $subFiles);
|
$files = array_merge($files, $subFiles);
|
||||||
@@ -406,13 +407,17 @@ class TransferController extends ControllerAbstract
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// List immediate child collections and entities of this collection;
|
// Get all nodes in this collection using nodeList with recursive=false
|
||||||
// recursion is handled here to build proper archive paths
|
// We handle recursion ourselves to build proper paths
|
||||||
$sources = new ResourceIdentifiers();
|
|
||||||
$sources->add(new CollectionIdentifier($provider, $service, $collectionId));
|
|
||||||
try {
|
try {
|
||||||
$collections = $this->manager->collectionList($tenantId, $userId, $sources)[$provider][$service] ?? [];
|
$nodes = $this->manager->nodeList(
|
||||||
$entities = $this->manager->entityListBulk($tenantId, $userId, $sources)[$provider][$service][$collectionId] ?? [];
|
$tenantId,
|
||||||
|
$userId,
|
||||||
|
$provider,
|
||||||
|
$service,
|
||||||
|
$collectionId,
|
||||||
|
false // Not recursive - we handle it ourselves
|
||||||
|
);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$this->logger->warning('Failed to list collection contents', [
|
$this->logger->warning('Failed to list collection contents', [
|
||||||
'collection' => $collectionId,
|
'collection' => $collectionId,
|
||||||
@@ -421,35 +426,34 @@ class TransferController extends ControllerAbstract
|
|||||||
return $files;
|
return $files;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var CollectionBaseInterface $node */
|
foreach ($nodes as $node) {
|
||||||
foreach ($collections as $node) {
|
$nodeName = $node->getLabel() ?? (string) $node->id();
|
||||||
$nodeName = $node->getProperties()->getLabel() ?? (string) $node->identifier();
|
|
||||||
$nodePath = $basePath !== '' ? $basePath . '/' . $nodeName : $nodeName;
|
$nodePath = $basePath !== '' ? $basePath . '/' . $nodeName : $nodeName;
|
||||||
// Recursively get contents of sub-collection
|
|
||||||
$subFiles = $this->resolveCollectionContents(
|
|
||||||
$tenantId,
|
|
||||||
$userId,
|
|
||||||
$provider,
|
|
||||||
$service,
|
|
||||||
(string) $node->identifier(),
|
|
||||||
$nodePath,
|
|
||||||
$depth + 1,
|
|
||||||
$maxDepth
|
|
||||||
);
|
|
||||||
$files = array_merge($files, $subFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var EntityBaseInterface $node */
|
if ($node->isCollection()) {
|
||||||
foreach ($entities as $node) {
|
// Recursively get contents of sub-collection
|
||||||
$nodeName = $node->getProperties()->getLabel() ?? (string) $node->identifier();
|
$subFiles = $this->resolveCollectionContents(
|
||||||
$nodePath = $basePath !== '' ? $basePath . '/' . $nodeName : $nodeName;
|
$tenantId,
|
||||||
$files[] = [
|
$userId,
|
||||||
'type' => 'file',
|
$provider,
|
||||||
'id' => (string) $node->identifier(),
|
$service,
|
||||||
'collection' => $collectionId,
|
(string) $node->id(),
|
||||||
'path' => $nodePath,
|
$nodePath,
|
||||||
'modTime' => $node->modified()?->getTimestamp(),
|
$depth + 1,
|
||||||
];
|
$maxDepth
|
||||||
|
);
|
||||||
|
$files = array_merge($files, $subFiles);
|
||||||
|
} else {
|
||||||
|
// It's an entity (file)
|
||||||
|
/** @var INodeEntityBase $node */
|
||||||
|
$files[] = [
|
||||||
|
'type' => 'file',
|
||||||
|
'id' => (string) $node->id(),
|
||||||
|
'collection' => $collectionId,
|
||||||
|
'path' => $nodePath,
|
||||||
|
'modTime' => $node->modifiedOn()?->getTimestamp(),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $files;
|
return $files;
|
||||||
|
|||||||
+564
-869
File diff suppressed because it is too large
Load Diff
+12
-8
@@ -2,14 +2,13 @@
|
|||||||
|
|
||||||
namespace KTXM\DocumentsManager;
|
namespace KTXM\DocumentsManager;
|
||||||
|
|
||||||
use KTXF\Module\Configuration\BrowserModuleContextInterface;
|
use KTXF\Module\ModuleBrowserInterface;
|
||||||
use KTXF\Module\Configuration\ModuleContextInterface;
|
|
||||||
use KTXF\Module\ModuleInstanceAbstract;
|
use KTXF\Module\ModuleInstanceAbstract;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File Manager Module
|
* File Manager Module
|
||||||
*/
|
*/
|
||||||
class Module extends ModuleInstanceAbstract
|
class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
||||||
{
|
{
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
@@ -51,11 +50,16 @@ class Module extends ModuleInstanceAbstract
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configure(ModuleContextInterface $context): void
|
public function registerBI(): array {
|
||||||
{
|
return [
|
||||||
if ($context instanceof BrowserModuleContextInterface) {
|
'handle' => $this->handle(),
|
||||||
$context->registerModule($this, 'DocumentsManager', 'static/module.mjs');
|
'namespace' => 'DocumentsManager',
|
||||||
}
|
'version' => $this->version(),
|
||||||
|
'label' => $this->label(),
|
||||||
|
'author' => $this->author(),
|
||||||
|
'description' => $this->description(),
|
||||||
|
'boot' => 'static/module.mjs',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace KTXM\DocumentsManager\Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implemented by a stream event that declares, up front, how many data frames
|
|
||||||
* are expected to follow. When such an event leads a stream generator, the
|
|
||||||
* envelope writer folds its value into the `control:start` frame's `total`
|
|
||||||
* (the progress denominator) instead of emitting it as a `data` frame.
|
|
||||||
*/
|
|
||||||
interface ExpectedTotal {
|
|
||||||
public function expectedTotal(): int;
|
|
||||||
}
|
|
||||||
Generated
+796
-1886
File diff suppressed because it is too large
Load Diff
+6
-16
@@ -7,27 +7,17 @@
|
|||||||
"build": "vite build --mode production --config vite.config.ts",
|
"build": "vite build --mode production --config vite.config.ts",
|
||||||
"dev": "vite build --mode development --config vite.config.ts",
|
"dev": "vite build --mode development --config vite.config.ts",
|
||||||
"watch": "vite build --mode development --watch --config vite.config.ts",
|
"watch": "vite build --mode development --watch --config vite.config.ts",
|
||||||
"typecheck": "vue-tsc --noEmit",
|
"typecheck": "vue-tsc --noEmit"
|
||||||
"test": "vitest run --config tests/js/vitest.config.ts",
|
|
||||||
"test:unit": "vitest run --config tests/js/vitest.config.ts",
|
|
||||||
"test:watch": "vitest watch --config tests/js/vitest.config.ts",
|
|
||||||
"test:coverage": "vitest run --coverage --config tests/js/vitest.config.ts"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^4.0.0",
|
"pinia": "^2.3.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13"
|
||||||
"vuetify": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node24": "^24.0.0",
|
"@tsconfig/node24": "^24.0.0",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^22.10.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
|
||||||
"@vue/tsconfig": "^0.9.0",
|
"@vue/tsconfig": "^0.9.0",
|
||||||
"typescript": "~6.0.0",
|
"typescript": "~5.7.2",
|
||||||
"vite": "^8.0.0",
|
"vite": "^6.0.3"
|
||||||
"vue-tsc": "^3.0.5",
|
|
||||||
"@vitest/coverage-v8": "^4.1.6",
|
|
||||||
"jsdom": "^29.1.1",
|
|
||||||
"vitest": "^4.1.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { useDocumentManager, useDocumentSelection } from '@/composables'
|
|
||||||
import { CollectionObject } from '@/models/collection'
|
|
||||||
import { EntityObject } from '@/models/entity'
|
|
||||||
import { formatSize, getFileIcon } from '@/utils/fileHelpers'
|
|
||||||
|
|
||||||
export interface DocumentSelection {
|
|
||||||
kind: 'entity' | 'collection'
|
|
||||||
identifier: string
|
|
||||||
name: string
|
|
||||||
mimeType?: string
|
|
||||||
size?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const PROVIDER_ID = 'default'
|
|
||||||
const SERVICE_ID = 'personal'
|
|
||||||
const LARGE_FILE_WARNING_BYTES = 25 * 1024 * 1024
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
modelValue: boolean
|
|
||||||
selectable?: ('entity' | 'collection')[]
|
|
||||||
}>(), {
|
|
||||||
selectable: () => ['entity'],
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
'update:modelValue': [value: boolean]
|
|
||||||
select: [items: DocumentSelection[]]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const allowEntities = computed(() => props.selectable.includes('entity'))
|
|
||||||
const allowCollections = computed(() => props.selectable.includes('collection'))
|
|
||||||
|
|
||||||
const documentManager = useDocumentManager({
|
|
||||||
providerId: PROVIDER_ID,
|
|
||||||
serviceId: SERVICE_ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
const selection = useDocumentSelection({
|
|
||||||
multiple: true,
|
|
||||||
allowFolders: allowCollections.value,
|
|
||||||
allowFiles: allowEntities.value,
|
|
||||||
})
|
|
||||||
|
|
||||||
const initialized = ref(false)
|
|
||||||
|
|
||||||
watch(() => props.modelValue, async (isOpen) => {
|
|
||||||
if (!isOpen) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
selection.clear()
|
|
||||||
|
|
||||||
if (!initialized.value) {
|
|
||||||
await documentManager.initialize()
|
|
||||||
initialized.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
await documentManager.navigateToRoot()
|
|
||||||
})
|
|
||||||
|
|
||||||
function nodeId(node: CollectionObject | EntityObject): string {
|
|
||||||
return String(node.identifier ?? '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodeLabel(node: CollectionObject | EntityObject): string {
|
|
||||||
return node.properties.label || nodeId(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
const breadcrumbs = computed(() => [
|
|
||||||
{ id: documentManager.ROOT_ID, label: 'Home' },
|
|
||||||
...documentManager.breadcrumbs.value.map(node => ({ id: nodeId(node), label: nodeLabel(node) })),
|
|
||||||
])
|
|
||||||
|
|
||||||
async function navigateToBreadcrumb(id: string) {
|
|
||||||
selection.clear()
|
|
||||||
await documentManager.navigateTo(id === documentManager.ROOT_ID ? null : id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openFolder(folder: CollectionObject) {
|
|
||||||
selection.clear()
|
|
||||||
await documentManager.navigateTo(nodeId(folder))
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleFolderSelection(folder: CollectionObject, event: Event) {
|
|
||||||
event.stopPropagation()
|
|
||||||
selection.toggle(folder)
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleEntity(entity: EntityObject) {
|
|
||||||
selection.toggle(entity)
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasLargeSelection = computed(() =>
|
|
||||||
selection.selectedEntities.value.some(entity => entity.properties.size > LARGE_FILE_WARNING_BYTES),
|
|
||||||
)
|
|
||||||
|
|
||||||
function entityIdentifier(entity: EntityObject): string {
|
|
||||||
return `${entity.provider}:${entity.service}:${entity.collection}:${entity.identifier}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectionIdentifier(collection: CollectionObject): string {
|
|
||||||
return `${collection.provider}:${collection.service}:${collection.identifier}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function toSelection(node: CollectionObject | EntityObject): DocumentSelection {
|
|
||||||
if (node instanceof EntityObject) {
|
|
||||||
return {
|
|
||||||
kind: 'entity',
|
|
||||||
identifier: entityIdentifier(node),
|
|
||||||
name: nodeLabel(node),
|
|
||||||
mimeType: node.properties.mime || 'application/octet-stream',
|
|
||||||
size: node.properties.size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
kind: 'collection',
|
|
||||||
identifier: collectionIdentifier(node),
|
|
||||||
name: nodeLabel(node),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClose() {
|
|
||||||
emit('update:modelValue', false)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleConfirm() {
|
|
||||||
const items = selection.selectedNodeArray.value.map(toSelection)
|
|
||||||
if (items.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
emit('select', items)
|
|
||||||
selection.clear()
|
|
||||||
emit('update:modelValue', false)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<v-dialog :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" max-width="560">
|
|
||||||
<v-card>
|
|
||||||
<v-card-title class="d-flex align-center">
|
|
||||||
<v-icon class="mr-2">mdi-folder-search-outline</v-icon>
|
|
||||||
Select Documents
|
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<v-card-text>
|
|
||||||
<v-breadcrumbs :items="breadcrumbs" density="compact" class="px-0 mb-2">
|
|
||||||
<template #item="{ item }">
|
|
||||||
<v-breadcrumbs-item :disabled="item.id === breadcrumbs[breadcrumbs.length - 1]?.id" @click="navigateToBreadcrumb(item.id)">
|
|
||||||
{{ item.label }}
|
|
||||||
</v-breadcrumbs-item>
|
|
||||||
</template>
|
|
||||||
</v-breadcrumbs>
|
|
||||||
|
|
||||||
<v-progress-linear v-if="documentManager.isLoading.value" indeterminate color="primary" class="mb-2" />
|
|
||||||
|
|
||||||
<div class="selector-list">
|
|
||||||
<v-list v-if="documentManager.currentCollections.value.length > 0 || documentManager.currentEntities.value.length > 0" density="compact">
|
|
||||||
<v-list-item
|
|
||||||
v-for="folder in documentManager.currentCollections.value"
|
|
||||||
:key="nodeId(folder)"
|
|
||||||
:title="nodeLabel(folder)"
|
|
||||||
@click="openFolder(folder)"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<v-checkbox-btn
|
|
||||||
v-if="allowCollections"
|
|
||||||
:model-value="selection.isSelected(nodeId(folder))"
|
|
||||||
class="mr-2"
|
|
||||||
@click="toggleFolderSelection(folder, $event)"
|
|
||||||
/>
|
|
||||||
<v-icon>mdi-folder</v-icon>
|
|
||||||
</template>
|
|
||||||
<template #append>
|
|
||||||
<v-icon size="small">mdi-chevron-right</v-icon>
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-list-item
|
|
||||||
v-for="entity in documentManager.currentEntities.value"
|
|
||||||
:key="nodeId(entity)"
|
|
||||||
:title="nodeLabel(entity)"
|
|
||||||
:subtitle="formatSize(entity.properties.size)"
|
|
||||||
:disabled="!allowEntities"
|
|
||||||
@click="allowEntities && toggleEntity(entity)"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<v-checkbox-btn v-if="allowEntities" :model-value="selection.isSelected(nodeId(entity))" class="mr-2" />
|
|
||||||
<v-icon>{{ getFileIcon(entity) }}</v-icon>
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
|
|
||||||
<div v-else-if="!documentManager.isLoading.value" class="text-center py-8 text-medium-emphasis">
|
|
||||||
<v-icon size="40" class="mb-2">mdi-folder-open-outline</v-icon>
|
|
||||||
<div>This folder is empty</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-alert v-if="hasLargeSelection" type="warning" variant="tonal" density="compact" class="mt-3">
|
|
||||||
One or more selected files are larger than 25 MB and may take a moment to process.
|
|
||||||
</v-alert>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-card-actions>
|
|
||||||
<span class="text-caption text-medium-emphasis ml-2">
|
|
||||||
{{ selection.count.value }} selected
|
|
||||||
</span>
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn variant="text" @click="handleClose">Cancel</v-btn>
|
|
||||||
<v-btn color="primary" variant="elevated" :disabled="!selection.hasSelection.value" @click="handleConfirm">
|
|
||||||
Select
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.selector-list {
|
|
||||||
max-height: 360px;
|
|
||||||
overflow-y: auto;
|
|
||||||
border: 1px solid rgb(var(--v-border-color));
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Central export point for Documents Manager composables
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { useDocumentManager } from './useDocumentManager'
|
|
||||||
export { useDocumentSelection } from './useDocumentSelection'
|
|
||||||
|
|
||||||
export type { UseDocumentManagerOptions } from './useDocumentManager'
|
|
||||||
export type { UseDocumentSelectionOptions } from './useDocumentSelection'
|
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
/**
|
|
||||||
* File Manager composable for convenient file/folder operations
|
|
||||||
* Provides reactive access to file manager state and actions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { computed, ref, unref } from 'vue'
|
|
||||||
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, ServiceIdentifier, CollectionIdentifier } from '@/types/common'
|
|
||||||
import type { DocumentInterface } from '@/types/document'
|
|
||||||
import { CollectionObject } from '@/models/collection'
|
|
||||||
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>
|
|
||||||
autoFetch?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDocumentManager(options: UseDocumentManagerOptions) {
|
|
||||||
const providersStore = useProvidersStore()
|
|
||||||
const servicesStore = useServicesStore()
|
|
||||||
const nodesStore = useNodesStore()
|
|
||||||
|
|
||||||
const { providerId, serviceId, autoFetch = false } = options
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Loading/error state
|
|
||||||
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()))
|
|
||||||
const rootId = computed(() => ROOT_ID)
|
|
||||||
|
|
||||||
// Current children
|
|
||||||
const currentChildren = computed(() =>
|
|
||||||
nodesStore.getChildren(currentProviderId(), currentServiceId(), currentLocation.value)
|
|
||||||
)
|
|
||||||
|
|
||||||
const currentCollections: ComputedRef<CollectionObject[]> = computed(() =>
|
|
||||||
nodesStore.getChildCollections(currentProviderId(), currentServiceId(), currentLocation.value)
|
|
||||||
)
|
|
||||||
|
|
||||||
const currentEntities: ComputedRef<EntityObject[]> = computed(() =>
|
|
||||||
nodesStore.getChildEntities(currentProviderId(), currentServiceId(), currentLocation.value)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Breadcrumb path
|
|
||||||
const breadcrumbs = computed(() => {
|
|
||||||
if (currentLocation.value === ROOT_ID) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return nodesStore.getPath(currentProviderId(), currentServiceId(), currentLocation.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Is at root?
|
|
||||||
const isAtRoot = computed(() => currentLocation.value === ROOT_ID)
|
|
||||||
|
|
||||||
// Navigate to a folder
|
|
||||||
const navigateTo = async (collectionId: string | null) => {
|
|
||||||
currentLocation.value = collectionId || ROOT_ID
|
|
||||||
await resetAndLoad(currentFilter.value, currentSort.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate up one level
|
|
||||||
const navigateUp = async () => {
|
|
||||||
if (currentLocation.value === ROOT_ID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const currentNode = nodesStore.getNode(currentProviderId(), currentServiceId(), currentLocation.value)
|
|
||||||
if (currentNode) {
|
|
||||||
await navigateTo(currentNode.collection ? String(currentNode.collection) : ROOT_ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to root
|
|
||||||
const navigateToRoot = async () => {
|
|
||||||
await navigateTo(ROOT_ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
'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}`,
|
|
||||||
{ label, owner: '' }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new file
|
|
||||||
const createFile = async (
|
|
||||||
label: string,
|
|
||||||
mime: string = 'application/octet-stream'
|
|
||||||
): Promise<EntityObject> => {
|
|
||||||
const properties: DocumentInterface = {
|
|
||||||
'@type': 'documents.properties',
|
|
||||||
urid: null,
|
|
||||||
size: 0,
|
|
||||||
label,
|
|
||||||
mime,
|
|
||||||
format: null,
|
|
||||||
encoding: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
return await nodesStore.createEntity(
|
|
||||||
`${currentProviderId()}:${currentServiceId()}:${currentLocation.value}`,
|
|
||||||
properties
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename a node
|
|
||||||
const renameNode = async (nodeId: string, newLabel: string) => {
|
|
||||||
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), nodeId)
|
|
||||||
if (!node) {
|
|
||||||
throw new Error('Node not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node instanceof CollectionObject) {
|
|
||||||
return await nodesStore.updateCollection(`${node.provider}:${node.service}:${nodeId}`, {
|
|
||||||
label: newLabel,
|
|
||||||
owner: node.properties.owner,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const properties: DocumentInterface = {
|
|
||||||
'@type': 'documents.properties',
|
|
||||||
urid: node.properties.urid,
|
|
||||||
size: node.properties.size,
|
|
||||||
label: newLabel,
|
|
||||||
mime: node.properties.mime,
|
|
||||||
format: node.properties.format,
|
|
||||||
encoding: node.properties.encoding,
|
|
||||||
}
|
|
||||||
return await nodesStore.updateEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${nodeId}`, properties)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete a node
|
|
||||||
const deleteNode = async (nodeId: string): Promise<boolean> => {
|
|
||||||
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), nodeId)
|
|
||||||
if (!node) {
|
|
||||||
throw new Error('Node not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node instanceof CollectionObject) {
|
|
||||||
return await nodesStore.deleteCollection(`${node.provider}:${node.service}:${nodeId}`)
|
|
||||||
} else {
|
|
||||||
return await nodesStore.deleteEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${nodeId}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file content
|
|
||||||
const readFile = async (entityId: string): Promise<string | null> => {
|
|
||||||
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), entityId)
|
|
||||||
if (!node || !(node instanceof EntityObject)) {
|
|
||||||
throw new Error('Entity not found')
|
|
||||||
}
|
|
||||||
return await nodesStore.readEntity(`${node.provider}:${node.service}:${node.collection || ROOT_ID}:${entityId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write file content
|
|
||||||
const writeFile = async (entityId: string, content: string): Promise<number> => {
|
|
||||||
const node = nodesStore.getNode(currentProviderId(), currentServiceId(), entityId)
|
|
||||||
if (!node || !(node instanceof EntityObject)) {
|
|
||||||
throw new Error('Entity not found')
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
const getEntityUrl = (entityId: string, collectionId?: string | null): string => {
|
|
||||||
const collection = collectionId ?? currentLocation.value
|
|
||||||
return `${TRANSFER_BASE_URL}/download/entity/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const getEntityPreviewUrl = (
|
|
||||||
entityId: string,
|
|
||||||
collectionId?: string | null,
|
|
||||||
variant: 'thumbnail' | 'inline' | 'fullscreen' = 'inline',
|
|
||||||
): string => {
|
|
||||||
const collection = collectionId ?? currentLocation.value
|
|
||||||
return `${TRANSFER_BASE_URL}/preview/entity/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}?variant=${variant}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download a single file
|
|
||||||
const downloadEntity = (entityId: string, collectionId?: string | null): void => {
|
|
||||||
const collection = collectionId ?? currentLocation.value
|
|
||||||
// Use path parameters: /download/entity/{provider}/{service}/{collection}/{identifier}
|
|
||||||
const url = `${TRANSFER_BASE_URL}/download/entity/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collection)}/${encodeURIComponent(entityId)}`
|
|
||||||
|
|
||||||
// Trigger download by opening URL (browser handles it)
|
|
||||||
window.open(url, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadCollection = (collectionId: string): void => {
|
|
||||||
// Use path parameters: /download/collection/{provider}/{service}/{identifier}
|
|
||||||
const url = `${TRANSFER_BASE_URL}/download/collection/${encodeURIComponent(currentProviderId())}/${encodeURIComponent(currentServiceId())}/${encodeURIComponent(collectionId)}`
|
|
||||||
|
|
||||||
window.open(url, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadArchive = (ids: string[], name: string = 'download', collectionId?: string | null): void => {
|
|
||||||
const collection = collectionId ?? currentLocation.value
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
provider: currentProviderId(),
|
|
||||||
service: currentServiceId(),
|
|
||||||
})
|
|
||||||
ids.forEach(id => params.append('ids[]', id))
|
|
||||||
if (name) {
|
|
||||||
params.append('name', name)
|
|
||||||
}
|
|
||||||
if (collection && collection !== ROOT_ID) {
|
|
||||||
params.append('collection', collection)
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${TRANSFER_BASE_URL}/download/archive?${params.toString()}`
|
|
||||||
|
|
||||||
window.open(url, '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize - fetch providers, services, and initial nodes if autoFetch
|
|
||||||
const initialize = async () => {
|
|
||||||
await providersStore.list([currentProviderId()])
|
|
||||||
await servicesStore.list()
|
|
||||||
if (autoFetch) {
|
|
||||||
await resetAndLoad()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// State
|
|
||||||
currentLocation,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
hasMore,
|
|
||||||
isLoadingMore,
|
|
||||||
|
|
||||||
// Provider/Service
|
|
||||||
provider,
|
|
||||||
service,
|
|
||||||
rootId,
|
|
||||||
|
|
||||||
// Current view
|
|
||||||
currentChildren,
|
|
||||||
currentCollections,
|
|
||||||
currentEntities,
|
|
||||||
breadcrumbs,
|
|
||||||
isAtRoot,
|
|
||||||
|
|
||||||
// Navigation
|
|
||||||
navigateTo,
|
|
||||||
navigateUp,
|
|
||||||
navigateToRoot,
|
|
||||||
resetAndLoad,
|
|
||||||
loadMore,
|
|
||||||
|
|
||||||
// Operations
|
|
||||||
createFolder,
|
|
||||||
createFile,
|
|
||||||
renameNode,
|
|
||||||
deleteNode,
|
|
||||||
readFile,
|
|
||||||
writeFile,
|
|
||||||
getEntityUrl,
|
|
||||||
getEntityPreviewUrl,
|
|
||||||
downloadEntity,
|
|
||||||
downloadCollection,
|
|
||||||
downloadArchive,
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
initialize,
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
ROOT_ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useDocumentManager
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
/**
|
|
||||||
* File selection composable
|
|
||||||
* Provides reactive selection management for file manager
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import type { Ref, ComputedRef } from 'vue'
|
|
||||||
import { CollectionObject } from '@/models/collection'
|
|
||||||
import { EntityObject } from '@/models/entity'
|
|
||||||
|
|
||||||
type NodeRecord = CollectionObject | EntityObject
|
|
||||||
|
|
||||||
export interface UseDocumentSelectionOptions {
|
|
||||||
multiple?: boolean
|
|
||||||
allowFolders?: boolean
|
|
||||||
allowFiles?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDocumentSelection(options: UseDocumentSelectionOptions = {}) {
|
|
||||||
const {
|
|
||||||
multiple = true,
|
|
||||||
allowFolders = true,
|
|
||||||
allowFiles = true,
|
|
||||||
} = options
|
|
||||||
|
|
||||||
const selectedIds: Ref<Set<string>> = ref(new Set())
|
|
||||||
const selectedNodes: Ref<Map<string, NodeRecord>> = ref(new Map())
|
|
||||||
|
|
||||||
const count: ComputedRef<number> = computed(() => selectedIds.value.size)
|
|
||||||
const hasSelection: ComputedRef<boolean> = computed(() => selectedIds.value.size > 0)
|
|
||||||
|
|
||||||
const selectedIdArray: ComputedRef<string[]> = computed(() => Array.from(selectedIds.value))
|
|
||||||
const selectedNodeArray: ComputedRef<NodeRecord[]> = computed(() => Array.from(selectedNodes.value.values()))
|
|
||||||
|
|
||||||
const selectedCollections: ComputedRef<CollectionObject[]> = computed(() =>
|
|
||||||
selectedNodeArray.value.filter((node): node is CollectionObject => node instanceof CollectionObject),
|
|
||||||
)
|
|
||||||
|
|
||||||
const selectedEntities: ComputedRef<EntityObject[]> = computed(() =>
|
|
||||||
selectedNodeArray.value.filter((node): node is EntityObject => node instanceof EntityObject),
|
|
||||||
)
|
|
||||||
|
|
||||||
const isSelected = (nodeId: string): boolean => selectedIds.value.has(nodeId)
|
|
||||||
|
|
||||||
const isTypeAllowed = (node: NodeRecord): boolean => {
|
|
||||||
if (node instanceof CollectionObject && !allowFolders) return false
|
|
||||||
if (node instanceof EntityObject && !allowFiles) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const select = (node: NodeRecord) => {
|
|
||||||
if (!isTypeAllowed(node)) return
|
|
||||||
|
|
||||||
if (!multiple) {
|
|
||||||
selectedIds.value.clear()
|
|
||||||
selectedNodes.value.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeId = String(node.identifier)
|
|
||||||
selectedIds.value.add(nodeId)
|
|
||||||
selectedNodes.value.set(nodeId, node)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deselect = (nodeId: string) => {
|
|
||||||
selectedIds.value.delete(nodeId)
|
|
||||||
selectedNodes.value.delete(nodeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggle = (node: NodeRecord) => {
|
|
||||||
const nodeId = String(node.identifier)
|
|
||||||
if (isSelected(nodeId)) {
|
|
||||||
deselect(nodeId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectMultiple = (nodes: NodeRecord[]) => {
|
|
||||||
if (!multiple) {
|
|
||||||
const lastNode = nodes[nodes.length - 1]
|
|
||||||
if (lastNode && isTypeAllowed(lastNode)) {
|
|
||||||
selectedIds.value.clear()
|
|
||||||
selectedNodes.value.clear()
|
|
||||||
const nodeId = String(lastNode.identifier)
|
|
||||||
selectedIds.value.add(nodeId)
|
|
||||||
selectedNodes.value.set(nodeId, lastNode)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (isTypeAllowed(node)) {
|
|
||||||
const nodeId = String(node.identifier)
|
|
||||||
selectedIds.value.add(nodeId)
|
|
||||||
selectedNodes.value.set(nodeId, node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectAll = (nodes: NodeRecord[]) => {
|
|
||||||
if (!multiple) return
|
|
||||||
selectMultiple(nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clear = () => {
|
|
||||||
selectedIds.value.clear()
|
|
||||||
selectedNodes.value.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
const setSelection = (nodes: NodeRecord[]) => {
|
|
||||||
clear()
|
|
||||||
selectMultiple(nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedIds,
|
|
||||||
selectedNodes,
|
|
||||||
count,
|
|
||||||
hasSelection,
|
|
||||||
selectedIdArray,
|
|
||||||
selectedNodeArray,
|
|
||||||
selectedCollections,
|
|
||||||
selectedEntities,
|
|
||||||
isSelected,
|
|
||||||
select,
|
|
||||||
deselect,
|
|
||||||
toggle,
|
|
||||||
selectMultiple,
|
|
||||||
selectAll,
|
|
||||||
clear,
|
|
||||||
setSelection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useDocumentSelection
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import type { ModuleIntegrations } from "@KTXC/types/moduleTypes";
|
|
||||||
|
|
||||||
const integrations: ModuleIntegrations = {
|
|
||||||
documents_selector: [
|
|
||||||
{
|
|
||||||
id: 'dialog',
|
|
||||||
label: 'Document Selector Dialog',
|
|
||||||
priority: 10,
|
|
||||||
component: () => import('./components/DocumentSelectorDialog.vue'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default integrations;
|
|
||||||
+2
-8
@@ -2,20 +2,14 @@
|
|||||||
* Documents Manager Module Boot
|
* Documents Manager Module Boot
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import integrations from '@/integrations'
|
|
||||||
|
|
||||||
console.log('[Documents Manager] Booting module...')
|
console.log('[Documents Manager] Booting module...')
|
||||||
|
|
||||||
console.log('[Documents Manager] Module booted successfully...')
|
console.log('[Documents Manager] Module booted successfully...')
|
||||||
|
|
||||||
// CSS filename is injected by the vite plugin at build time
|
// CSS will be injected by build process
|
||||||
export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
//export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
||||||
|
|
||||||
export { integrations }
|
|
||||||
|
|
||||||
// Export services, stores and models for external use
|
// Export services, stores and models for external use
|
||||||
export * from '@/services'
|
export * from '@/services'
|
||||||
export * from '@/stores'
|
export * from '@/stores'
|
||||||
export * from '@/models'
|
export * from '@/models'
|
||||||
export * from '@/composables'
|
|
||||||
export * from '@/utils/fileHelpers'
|
|
||||||
|
|||||||
@@ -4,19 +4,6 @@
|
|||||||
|
|
||||||
import type { CollectionContentTypes, CollectionInterface, CollectionModelInterface, CollectionPropertiesInterface } from "@/types/collection";
|
import type { CollectionContentTypes, CollectionInterface, CollectionModelInterface, CollectionPropertiesInterface } from "@/types/collection";
|
||||||
|
|
||||||
/**
|
|
||||||
* Reduce a serialized resource identifier ("provider:service:collection")
|
|
||||||
* to its own (last) segment; plain values pass through unchanged
|
|
||||||
*/
|
|
||||||
function plainIdentifier(value: string | number | null | undefined): string | number | null {
|
|
||||||
if (value === null || value === undefined || value === '') {
|
|
||||||
return value ?? null;
|
|
||||||
}
|
|
||||||
const text = String(value);
|
|
||||||
const index = text.lastIndexOf(':');
|
|
||||||
return index >= 0 ? text.slice(index + 1) : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CollectionObject implements CollectionModelInterface {
|
export class CollectionObject implements CollectionModelInterface {
|
||||||
|
|
||||||
_data!: CollectionInterface;
|
_data!: CollectionInterface;
|
||||||
@@ -37,14 +24,7 @@ export class CollectionObject implements CollectionModelInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fromJson(data: CollectionInterface): CollectionObject {
|
fromJson(data: CollectionInterface): CollectionObject {
|
||||||
// the wire format carries full resource identifiers (provider:service:collection);
|
this._data = data;
|
||||||
// reduce them to plain ids, which is what the stores and UI operate on
|
|
||||||
this._data = {
|
|
||||||
...data,
|
|
||||||
service: plainIdentifier(data.service) ?? '',
|
|
||||||
collection: plainIdentifier(data.collection),
|
|
||||||
identifier: plainIdentifier(data.identifier) ?? '',
|
|
||||||
};
|
|
||||||
if (data.properties) {
|
if (data.properties) {
|
||||||
this._data.properties = new CollectionPropertiesObject().fromJson(data.properties as CollectionPropertiesInterface);
|
this._data.properties = new CollectionPropertiesObject().fromJson(data.properties as CollectionPropertiesInterface);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-21
@@ -5,19 +5,6 @@ import type { EntityInterface, EntityModelInterface } from "@/types/entity";
|
|||||||
import type { DocumentInterface, DocumentModelInterface } from "@/types/document";
|
import type { DocumentInterface, DocumentModelInterface } from "@/types/document";
|
||||||
import { DocumentObject } from "./document";
|
import { DocumentObject } from "./document";
|
||||||
|
|
||||||
/**
|
|
||||||
* Reduce a serialized resource identifier ("provider:service:collection:entity")
|
|
||||||
* to its own (last) segment; plain values pass through unchanged
|
|
||||||
*/
|
|
||||||
function plainIdentifier(value: string | number | null | undefined): string | number | null {
|
|
||||||
if (value === null || value === undefined || value === '') {
|
|
||||||
return value ?? null;
|
|
||||||
}
|
|
||||||
const text = String(value);
|
|
||||||
const index = text.lastIndexOf(':');
|
|
||||||
return index >= 0 ? text.slice(index + 1) : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EntityObject implements EntityModelInterface {
|
export class EntityObject implements EntityModelInterface {
|
||||||
|
|
||||||
_data!: EntityInterface<DocumentInterface|DocumentModelInterface>;
|
_data!: EntityInterface<DocumentInterface|DocumentModelInterface>;
|
||||||
@@ -38,14 +25,7 @@ export class EntityObject implements EntityModelInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fromJson(data: EntityInterface): EntityObject {
|
fromJson(data: EntityInterface): EntityObject {
|
||||||
// the wire format carries full resource identifiers (provider:service:collection:entity);
|
this._data = data
|
||||||
// reduce them to plain ids, which is what the stores and UI operate on
|
|
||||||
this._data = {
|
|
||||||
...data,
|
|
||||||
service: String(plainIdentifier(data.service) ?? ''),
|
|
||||||
collection: plainIdentifier(data.collection) ?? '',
|
|
||||||
identifier: plainIdentifier(data.identifier) ?? '',
|
|
||||||
}
|
|
||||||
if (data.properties) {
|
if (data.properties) {
|
||||||
this._data.properties = new DocumentObject().fromJson(data.properties as DocumentInterface);
|
this._data.properties = new DocumentObject().fromJson(data.properties as DocumentInterface);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,20 +16,28 @@ import type {
|
|||||||
CollectionUpdateRequest,
|
CollectionUpdateRequest,
|
||||||
CollectionDeleteResponse,
|
CollectionDeleteResponse,
|
||||||
CollectionDeleteRequest,
|
CollectionDeleteRequest,
|
||||||
CollectionCopyRequest,
|
|
||||||
CollectionCopyResponse,
|
|
||||||
CollectionMoveRequest,
|
|
||||||
CollectionMoveResponse,
|
|
||||||
CollectionInterface,
|
CollectionInterface,
|
||||||
} from '../types/collection';
|
} from '../types/collection';
|
||||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||||
import { CollectionObject, CollectionPropertiesObject } from '../models/collection';
|
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
|
* Helper to create the right collection model class based on provider identifier
|
||||||
* Uses provider-specific factory if available, otherwise returns base CollectionObject
|
* Uses provider-specific factory if available, otherwise returns base CollectionObject
|
||||||
*/
|
*/
|
||||||
export function createCollectionObject(data: CollectionInterface): CollectionObject {
|
function createCollectionObject(data: CollectionInterface): CollectionObject {
|
||||||
const integrationStore = useIntegrationStore();
|
const integrationStore = useIntegrationStore();
|
||||||
const factoryItem = integrationStore.getItemById('documents_collection_factory', data.provider) as any;
|
const factoryItem = integrationStore.getItemById('documents_collection_factory', data.provider) as any;
|
||||||
const factory = factoryItem?.factory;
|
const factory = factoryItem?.factory;
|
||||||
@@ -56,8 +64,19 @@ export const collectionService = {
|
|||||||
const serviceList: Record<string, Record<string, CollectionObject>> = {};
|
const serviceList: Record<string, Record<string, CollectionObject>> = {};
|
||||||
Object.entries(providerServices).forEach(([serviceId, serviceCollections]) => {
|
Object.entries(providerServices).forEach(([serviceId, serviceCollections]) => {
|
||||||
const collectionList: Record<string, CollectionObject> = {};
|
const collectionList: Record<string, CollectionObject> = {};
|
||||||
Object.entries(serviceCollections).forEach(([collectionId, collectionData]) => {
|
Object.entries(serviceCollections as Record<string, unknown>).forEach(([collectionId, collectionData]) => {
|
||||||
collectionList[collectionId] = createCollectionObject(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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
serviceList[serviceId] = collectionList;
|
serviceList[serviceId] = collectionList;
|
||||||
});
|
});
|
||||||
@@ -68,15 +87,15 @@ export const collectionService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve collections by resource identifier
|
* Retrieve a specific collection by provider and identifier
|
||||||
*
|
*
|
||||||
* @param request - fetch request parameters
|
* @param request - fetch request parameters
|
||||||
*
|
*
|
||||||
* @returns Promise with collections keyed by resource identifier
|
* @returns Promise with collection object
|
||||||
*/
|
*/
|
||||||
async fetch(request: CollectionFetchRequest): Promise<Record<string, CollectionObject>> {
|
async fetch(request: CollectionFetchRequest): Promise<CollectionObject> {
|
||||||
const response = await transceivePost<CollectionFetchRequest, CollectionFetchResponse>('collection.fetch', request);
|
const response = await transceivePost<CollectionFetchRequest, CollectionFetchResponse>('collection.fetch', request);
|
||||||
return Object.fromEntries(Object.entries(response).map(([target, data]) => [target, createCollectionObject(data)]));
|
return createCollectionObject(response);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,30 +150,6 @@ export const collectionService = {
|
|||||||
return await transceivePost<CollectionDeleteRequest, CollectionDeleteResponse>('collection.delete', request);
|
return await transceivePost<CollectionDeleteRequest, CollectionDeleteResponse>('collection.delete', request);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy a collection to a new parent
|
|
||||||
*
|
|
||||||
* @param request - copy request parameters
|
|
||||||
*
|
|
||||||
* @returns Promise with copied collection object
|
|
||||||
*/
|
|
||||||
async copy(request: CollectionCopyRequest): Promise<CollectionObject> {
|
|
||||||
const response = await transceivePost<CollectionCopyRequest, CollectionCopyResponse>('collection.copy', request);
|
|
||||||
return createCollectionObject(response);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move a collection to a new parent
|
|
||||||
*
|
|
||||||
* @param request - move request parameters
|
|
||||||
*
|
|
||||||
* @returns Promise with moved collection object
|
|
||||||
*/
|
|
||||||
async move(request: CollectionMoveRequest): Promise<CollectionObject> {
|
|
||||||
const response = await transceivePost<CollectionMoveRequest, CollectionMoveResponse>('collection.move', request);
|
|
||||||
return createCollectionObject(response);
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default collectionService;
|
export default collectionService;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Entity management service
|
* Entity management service
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { transceivePost, transceiveStream } from './transceive';
|
import { transceivePost } from './transceive';
|
||||||
import type {
|
import type {
|
||||||
EntityListRequest,
|
EntityListRequest,
|
||||||
EntityListResponse,
|
EntityListResponse,
|
||||||
@@ -18,10 +18,6 @@ import type {
|
|||||||
EntityDeleteResponse,
|
EntityDeleteResponse,
|
||||||
EntityDeltaRequest,
|
EntityDeltaRequest,
|
||||||
EntityDeltaResponse,
|
EntityDeltaResponse,
|
||||||
EntityCopyRequest,
|
|
||||||
EntityCopyResponse,
|
|
||||||
EntityMoveRequest,
|
|
||||||
EntityMoveResponse,
|
|
||||||
EntityReadRequest,
|
EntityReadRequest,
|
||||||
EntityReadResponse,
|
EntityReadResponse,
|
||||||
EntityWriteRequest,
|
EntityWriteRequest,
|
||||||
@@ -35,7 +31,7 @@ import { EntityObject } from '../models';
|
|||||||
* Helper to create the right entity model class based on provider identifier
|
* Helper to create the right entity model class based on provider identifier
|
||||||
* Uses provider-specific factory if available, otherwise returns base EntityObject
|
* Uses provider-specific factory if available, otherwise returns base EntityObject
|
||||||
*/
|
*/
|
||||||
export function createEntityObject(data: EntityInterface): EntityObject {
|
function createEntityObject(data: EntityInterface): EntityObject {
|
||||||
const integrationStore = useIntegrationStore();
|
const integrationStore = useIntegrationStore();
|
||||||
const factoryItem = integrationStore.getItemById('documents_entity_factory', data.provider) as any;
|
const factoryItem = integrationStore.getItemById('documents_entity_factory', data.provider) as any;
|
||||||
const factory = factoryItem?.factory;
|
const factory = factoryItem?.factory;
|
||||||
@@ -47,14 +43,14 @@ export function createEntityObject(data: EntityInterface): EntityObject {
|
|||||||
export const entityService = {
|
export const entityService = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve list of entities, optionally filtered by source identifiers
|
* Retrieve list of entities, optionally filtered by source selector
|
||||||
*
|
*
|
||||||
* @param request - list request parameters
|
* @param request - list request parameters
|
||||||
*
|
*
|
||||||
* @returns Promise with entity object list grouped by provider, service, collection, and entity identifier
|
* @returns Promise with entity object list grouped by provider, service, collection, and entity identifier
|
||||||
*/
|
*/
|
||||||
async listBulk(request: EntityListRequest = {}): Promise<Record<string, Record<string, Record<string, Record<string, EntityObject>>>>> {
|
async list(request: EntityListRequest = {}): Promise<Record<string, Record<string, Record<string, Record<string, EntityObject>>>>> {
|
||||||
const response = await transceivePost<EntityListRequest, EntityListResponse>('entity.listBulk', request);
|
const response = await transceivePost<EntityListRequest, EntityListResponse>('entity.list', request);
|
||||||
|
|
||||||
// Convert nested response to EntityObject instances
|
// Convert nested response to EntityObject instances
|
||||||
const providerList: Record<string, Record<string, Record<string, Record<string, EntityObject>>>> = {};
|
const providerList: Record<string, Record<string, Record<string, Record<string, EntityObject>>>> = {};
|
||||||
@@ -77,20 +73,6 @@ export const entityService = {
|
|||||||
return providerList;
|
return providerList;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Stream entities one by one, invoking the callback for each entity
|
|
||||||
*
|
|
||||||
* @param request - list request parameters
|
|
||||||
* @param onEntity - callback invoked for each streamed entity
|
|
||||||
*
|
|
||||||
* @returns Promise with the total number of streamed entities
|
|
||||||
*/
|
|
||||||
async listStream(request: EntityListRequest, onEntity: (entity: EntityObject) => void): Promise<{ total: number }> {
|
|
||||||
return await transceiveStream<EntityListRequest, EntityInterface>('entity.listStream', request, (entityData) => {
|
|
||||||
onEntity(createEntityObject(entityData));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a specific entity by provider and identifier
|
* Retrieve a specific entity by provider and identifier
|
||||||
*
|
*
|
||||||
@@ -146,38 +128,16 @@ export const entityService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete entities
|
* Delete an entity
|
||||||
*
|
*
|
||||||
* @param request - delete request parameters
|
* @param request - delete request parameters
|
||||||
*
|
*
|
||||||
* @returns Promise with per-entity disposition results
|
* @returns Promise with deletion result
|
||||||
*/
|
*/
|
||||||
async delete(request: EntityDeleteRequest): Promise<EntityDeleteResponse> {
|
async delete(request: EntityDeleteRequest): Promise<EntityDeleteResponse> {
|
||||||
return await transceivePost<EntityDeleteRequest, EntityDeleteResponse>('entity.delete', request);
|
return await transceivePost<EntityDeleteRequest, EntityDeleteResponse>('entity.delete', request);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy entities to another collection
|
|
||||||
*
|
|
||||||
* @param request - copy request parameters
|
|
||||||
*
|
|
||||||
* @returns Promise with per-entity disposition results
|
|
||||||
*/
|
|
||||||
async copy(request: EntityCopyRequest): Promise<EntityCopyResponse> {
|
|
||||||
return await transceivePost<EntityCopyRequest, EntityCopyResponse>('entity.copy', request);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move entities to another collection
|
|
||||||
*
|
|
||||||
* @param request - move request parameters
|
|
||||||
*
|
|
||||||
* @returns Promise with per-entity disposition results
|
|
||||||
*/
|
|
||||||
async move(request: EntityMoveRequest): Promise<EntityMoveResponse> {
|
|
||||||
return await transceivePost<EntityMoveRequest, EntityMoveResponse>('entity.move', request);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve delta changes for entities
|
* Retrieve delta changes for entities
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export { providerService } from './providerService';
|
|||||||
export { serviceService } from './serviceService';
|
export { serviceService } from './serviceService';
|
||||||
export { collectionService } from './collectionService';
|
export { collectionService } from './collectionService';
|
||||||
export { entityService } from './entityService';
|
export { entityService } from './entityService';
|
||||||
|
export { nodeService } from './nodeService';
|
||||||
|
|||||||
+64
-30
@@ -1,34 +1,68 @@
|
|||||||
import { transceiveStream } from './transceive';
|
/**
|
||||||
import type { NodeListRequest, NodeInterface } from '../types/node';
|
* Node (unified collection/entity) management service
|
||||||
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)[];
|
import { transceivePost } from './transceive'
|
||||||
|
import type { ListFilter, ListSort, ListRange } from '../types/common'
|
||||||
|
import type { CollectionInterface } from '../types/collection'
|
||||||
|
import type { EntityInterface } from '../types/entity'
|
||||||
|
|
||||||
|
export type NodeItem = CollectionInterface | EntityInterface
|
||||||
|
|
||||||
|
export interface NodeListRequest {
|
||||||
|
provider: string
|
||||||
|
service: string | number
|
||||||
|
location?: string | number | null
|
||||||
|
recursive?: boolean
|
||||||
|
filter?: ListFilter | null
|
||||||
|
sort?: ListSort | null
|
||||||
|
range?: ListRange | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeListResponse = Record<string, NodeItem>
|
||||||
|
|
||||||
|
export interface NodeDeltaRequest {
|
||||||
|
provider: string
|
||||||
|
service: string | number
|
||||||
|
location?: string | number | null
|
||||||
|
signature: string
|
||||||
|
recursive?: boolean
|
||||||
|
detail?: 'ids' | 'full'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeDeltaResult {
|
||||||
|
added: Array<string | number | NodeItem>
|
||||||
|
modified: Array<string | number | NodeItem>
|
||||||
|
removed: Array<string | number>
|
||||||
|
signature: string
|
||||||
|
}
|
||||||
|
|
||||||
export const nodeService = {
|
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;
|
async list(request: NodeListRequest): Promise<NodeItem[]> {
|
||||||
|
const response = await transceivePost<NodeListRequest, NodeListResponse>('node.list', {
|
||||||
|
provider: request.provider,
|
||||||
|
service: request.service,
|
||||||
|
location: request.location ?? null,
|
||||||
|
recursive: request.recursive ?? false,
|
||||||
|
filter: request.filter ?? null,
|
||||||
|
sort: request.sort ?? null,
|
||||||
|
range: request.range ?? null,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.values(response)
|
||||||
|
},
|
||||||
|
|
||||||
|
async delta(request: NodeDeltaRequest): Promise<NodeDeltaResult> {
|
||||||
|
return await transceivePost<NodeDeltaRequest, NodeDeltaResult>('node.delta', {
|
||||||
|
provider: request.provider,
|
||||||
|
service: request.service,
|
||||||
|
location: request.location ?? null,
|
||||||
|
signature: request.signature,
|
||||||
|
recursive: request.recursive ?? false,
|
||||||
|
detail: request.detail ?? 'ids',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default nodeService
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createFetchWrapper } from '@KTXC';
|
import { createFetchWrapper } from '@KTXC';
|
||||||
import type { ApiRequest, ApiResponse, ApiStreamResponse } from '../types/common';
|
import type { ApiRequest, ApiResponse } from '../types/common';
|
||||||
|
|
||||||
const fetchWrapper = createFetchWrapper();
|
const fetchWrapper = createFetchWrapper();
|
||||||
const API_URL = '/m/documents_manager/v1';
|
const API_URL = '/m/documents_manager/v1';
|
||||||
@@ -48,88 +48,3 @@ export async function transceivePost<TRequest, TResponse>(
|
|||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stream an NDJSON API response, unwrapping data frames for the caller.
|
|
||||||
*
|
|
||||||
* @param operation - Operation name, e.g. 'entity.listStream'
|
|
||||||
* @param data - Operation-specific request data
|
|
||||||
* @param onData - Synchronous callback invoked for every unwrapped data payload.
|
|
||||||
* @param options - Optional `user` override and an `onStart` hook.
|
|
||||||
* @returns Promise resolving to the final stream total from the control/end frame
|
|
||||||
*/
|
|
||||||
export async function transceiveStream<TRequest, TData>(
|
|
||||||
operation: string,
|
|
||||||
data: TRequest,
|
|
||||||
onData: (data: TData) => void,
|
|
||||||
options?: { user?: string; onStart?: (expected?: number) => void; requireEnd?: boolean }
|
|
||||||
): Promise<{ total: number }> {
|
|
||||||
const request: ApiRequest<TRequest> = {
|
|
||||||
version: API_VERSION,
|
|
||||||
transaction: generateTransactionId(),
|
|
||||||
operation,
|
|
||||||
data,
|
|
||||||
user: options?.user,
|
|
||||||
};
|
|
||||||
|
|
||||||
let total = 0;
|
|
||||||
let ended = false;
|
|
||||||
|
|
||||||
const dispatch = (line: string): void => {
|
|
||||||
const message = JSON.parse(line) as ApiStreamResponse<TData>;
|
|
||||||
|
|
||||||
if (message.type === 'control') {
|
|
||||||
if (message.status === 'start') {
|
|
||||||
options?.onStart?.(message.total);
|
|
||||||
} else if (message.status === 'end') {
|
|
||||||
total = message.total;
|
|
||||||
ended = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 'error') {
|
|
||||||
throw new Error(`[${operation}] ${message.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
onData(message.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
await fetchWrapper.post(API_URL, request, {
|
|
||||||
headers: { 'Accept': 'application/json' },
|
|
||||||
onStream: async (response: Response) => {
|
|
||||||
if (!response.body) {
|
|
||||||
throw new Error(`[${operation}] Response body is not readable`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true });
|
|
||||||
const lines = buffer.split('\n');
|
|
||||||
buffer = lines.pop()!;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim()) dispatch(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer += decoder.decode();
|
|
||||||
if (buffer.trim()) dispatch(buffer);
|
|
||||||
} finally {
|
|
||||||
reader.releaseLock();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (options?.requireEnd && !ended) {
|
|
||||||
throw new Error(`[${operation}] Stream ended without a completion frame`);
|
|
||||||
}
|
|
||||||
return { total };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { ref, computed, readonly } from 'vue'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { collectionService } from '../services/collectionService'
|
import { collectionService } from '../services/collectionService'
|
||||||
import type {
|
import type {
|
||||||
CollectionIdentifier,
|
SourceSelector,
|
||||||
ServiceIdentifier,
|
|
||||||
ListFilter,
|
ListFilter,
|
||||||
ListSort,
|
ListSort,
|
||||||
CollectionMutableProperties,
|
CollectionMutableProperties,
|
||||||
|
CollectionDeleteResponse,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { CollectionObject } from '../models/collection'
|
import { CollectionObject } from '../models/collection'
|
||||||
|
|
||||||
@@ -40,25 +40,25 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
return `${provider}:${service ?? ''}:${identifier ?? ''}`
|
return `${provider}:${service ?? ''}:${identifier ?? ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function collection(target: CollectionIdentifier, retrieve: boolean = false): CollectionObject | null {
|
function collection(provider: string, service: string | number, identifier: string | number, retrieve: boolean = false): CollectionObject | null {
|
||||||
const key = target
|
const key = identifierKey(provider, service, identifier)
|
||||||
if (retrieve === true && !_collections.value[key]) {
|
if (retrieve === true && !_collections.value[key]) {
|
||||||
console.debug(`[Documents Manager][Store] - Force fetching collection "${key}"`)
|
console.debug(`[Documents Manager][Store] - Force fetching collection "${key}"`)
|
||||||
void fetch([target])
|
fetch(provider, service, identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
return _collections.value[key] || null
|
return _collections.value[key] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectionsForService(target: ServiceIdentifier): CollectionObject[] {
|
function collectionsForService(provider: string, service: string | number): CollectionObject[] {
|
||||||
const serviceKeyPrefix = `${target}:`
|
const serviceKeyPrefix = `${provider}:${service}:`
|
||||||
return Object.entries(_collections.value)
|
return Object.entries(_collections.value)
|
||||||
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
||||||
.map(([_, collectionObj]) => collectionObj)
|
.map(([_, collectionObj]) => collectionObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearService(target: ServiceIdentifier): void {
|
function clearService(provider: string, service: string | number): void {
|
||||||
const serviceKeyPrefix = `${target}:`
|
const serviceKeyPrefix = `${provider}:${service}:`
|
||||||
Object.keys(_collections.value)
|
Object.keys(_collections.value)
|
||||||
.filter((key) => key.startsWith(serviceKeyPrefix))
|
.filter((key) => key.startsWith(serviceKeyPrefix))
|
||||||
.forEach((key) => {
|
.forEach((key) => {
|
||||||
@@ -70,29 +70,8 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
_collections.value = {}
|
_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
|
// Actions
|
||||||
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort): Promise<Record<string, CollectionObject>> {
|
async function list(sources?: SourceSelector, filter?: ListFilter, sort?: ListSort): Promise<Record<string, CollectionObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.list({ sources, filter, sort })
|
const response = await collectionService.list({ sources, filter, sort })
|
||||||
@@ -119,11 +98,14 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetch(targets: CollectionIdentifier[]): Promise<Record<string, CollectionObject>> {
|
async function fetch(provider: string, service: string | number, identifier: string | number): Promise<CollectionObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.fetch({ targets })
|
const response = await collectionService.fetch({ provider, service, collection: identifier })
|
||||||
_collections.value = { ..._collections.value, ...response }
|
const key = identifierKey(response.provider, response.service, response.identifier)
|
||||||
|
_collections.value[key] = response
|
||||||
|
|
||||||
|
console.debug('[Documents Manager][Store] - Successfully fetched collection:', key)
|
||||||
return response
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to fetch collection:', error)
|
console.error('[Documents Manager][Store] - Failed to fetch collection:', error)
|
||||||
@@ -133,10 +115,10 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extant(targets: CollectionIdentifier[]) {
|
async function extant(sources: SourceSelector) {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.extant({ targets })
|
const response = await collectionService.extant({ sources })
|
||||||
console.debug('[Documents Manager][Store] - Successfully checked collection availability')
|
console.debug('[Documents Manager][Store] - Successfully checked collection availability')
|
||||||
return response
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -148,12 +130,14 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function create(
|
async function create(
|
||||||
target: ServiceIdentifier | CollectionIdentifier,
|
provider: string,
|
||||||
|
service: string | number,
|
||||||
|
collection: string | number | null,
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.create({ target, properties })
|
const response = await collectionService.create({ provider, service, collection, properties })
|
||||||
const key = identifierKey(response.provider, response.service, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.identifier)
|
||||||
_collections.value[key] = response
|
_collections.value[key] = response
|
||||||
|
|
||||||
@@ -168,12 +152,14 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function update(
|
async function update(
|
||||||
target: CollectionIdentifier,
|
provider: string,
|
||||||
|
service: string | number,
|
||||||
|
identifier: string | number,
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.update({ target, properties })
|
const response = await collectionService.update({ provider, service, identifier, properties })
|
||||||
const key = identifierKey(response.provider, response.service, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.identifier)
|
||||||
_collections.value[key] = response
|
_collections.value[key] = response
|
||||||
|
|
||||||
@@ -187,17 +173,17 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(target: CollectionIdentifier): Promise<{ success: boolean }> {
|
async function remove(provider: string, service: string | number, identifier: string | number): Promise<CollectionDeleteResponse> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await collectionService.delete({ target })
|
const response = await collectionService.delete({ provider, service, identifier })
|
||||||
const success = response.disposition === 'deleted' || response.disposition === 'moved'
|
if (response.success) {
|
||||||
if (success) {
|
const key = identifierKey(provider, service, identifier)
|
||||||
delete _collections.value[target]
|
delete _collections.value[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('[Documents Manager][Store] - Successfully deleted collection:', target)
|
console.debug('[Documents Manager][Store] - Successfully deleted collection:', `${provider}:${service}:${identifier}`)
|
||||||
return { success }
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to delete collection:', error)
|
console.error('[Documents Manager][Store] - Failed to delete collection:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -216,8 +202,6 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
|
|||||||
collectionsForService,
|
collectionsForService,
|
||||||
clearService,
|
clearService,
|
||||||
clearAll,
|
clearAll,
|
||||||
mergeMany,
|
|
||||||
removeMany,
|
|
||||||
list,
|
list,
|
||||||
fetch,
|
fetch,
|
||||||
extant,
|
extant,
|
||||||
|
|||||||
+68
-58
@@ -7,13 +7,12 @@ import { defineStore } from 'pinia'
|
|||||||
import { entityService } from '../services/entityService'
|
import { entityService } from '../services/entityService'
|
||||||
import { EntityObject } from '../models'
|
import { EntityObject } from '../models'
|
||||||
import type {
|
import type {
|
||||||
CollectionIdentifier,
|
SourceSelector,
|
||||||
EntityIdentifier,
|
|
||||||
ServiceIdentifier,
|
|
||||||
ListFilter,
|
ListFilter,
|
||||||
ListSort,
|
ListSort,
|
||||||
ListRange,
|
ListRange,
|
||||||
DocumentInterface,
|
DocumentInterface,
|
||||||
|
EntityDeleteResponse,
|
||||||
EntityDeltaResponse,
|
EntityDeltaResponse,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
@@ -49,34 +48,37 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function entity(
|
function entity(
|
||||||
target: EntityIdentifier,
|
provider: string,
|
||||||
|
service: string | number,
|
||||||
|
collection: string | number,
|
||||||
|
identifier: string | number,
|
||||||
retrieve: boolean = false,
|
retrieve: boolean = false,
|
||||||
): EntityObject | null {
|
): EntityObject | null {
|
||||||
const key = target
|
const key = identifierKey(provider, service, collection, identifier)
|
||||||
if (retrieve === true && !_entities.value[key]) {
|
if (retrieve === true && !_entities.value[key]) {
|
||||||
console.debug(`[Documents Manager][Store] - Force fetching entity "${key}"`)
|
console.debug(`[Documents Manager][Store] - Force fetching entity "${key}"`)
|
||||||
void fetch([target])
|
fetch(provider, service, collection, [identifier])
|
||||||
}
|
}
|
||||||
|
|
||||||
return _entities.value[key] || null
|
return _entities.value[key] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
function entitiesForService(target: ServiceIdentifier): EntityObject[] {
|
function entitiesForService(provider: string, service: string | number): EntityObject[] {
|
||||||
const serviceKeyPrefix = `${target}:`
|
const serviceKeyPrefix = `${provider}:${service}:`
|
||||||
return Object.entries(_entities.value)
|
return Object.entries(_entities.value)
|
||||||
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
.filter(([key]) => key.startsWith(serviceKeyPrefix))
|
||||||
.map(([_, entityObj]) => entityObj)
|
.map(([_, entityObj]) => entityObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function entitiesForCollection(target: CollectionIdentifier): EntityObject[] {
|
function entitiesForCollection(provider: string, service: string | number, collection: string | number): EntityObject[] {
|
||||||
const collectionKeyPrefix = `${target}:`
|
const collectionKeyPrefix = `${provider}:${service}:${collection}:`
|
||||||
return Object.entries(_entities.value)
|
return Object.entries(_entities.value)
|
||||||
.filter(([key]) => key.startsWith(collectionKeyPrefix))
|
.filter(([key]) => key.startsWith(collectionKeyPrefix))
|
||||||
.map(([_, entityObj]) => entityObj)
|
.map(([_, entityObj]) => entityObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearService(target: ServiceIdentifier): void {
|
function clearService(provider: string, service: string | number): void {
|
||||||
const serviceKeyPrefix = `${target}:`
|
const serviceKeyPrefix = `${provider}:${service}:`
|
||||||
Object.keys(_entities.value)
|
Object.keys(_entities.value)
|
||||||
.filter((key) => key.startsWith(serviceKeyPrefix))
|
.filter((key) => key.startsWith(serviceKeyPrefix))
|
||||||
.forEach((key) => {
|
.forEach((key) => {
|
||||||
@@ -84,8 +86,8 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearCollection(target: CollectionIdentifier): void {
|
function clearCollection(provider: string, service: string | number, collection: string | number): void {
|
||||||
const collectionKeyPrefix = `${target}:`
|
const collectionKeyPrefix = `${provider}:${service}:${collection}:`
|
||||||
Object.keys(_entities.value)
|
Object.keys(_entities.value)
|
||||||
.filter((key) => key.startsWith(collectionKeyPrefix))
|
.filter((key) => key.startsWith(collectionKeyPrefix))
|
||||||
.forEach((key) => {
|
.forEach((key) => {
|
||||||
@@ -97,23 +99,11 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
_entities.value = {}
|
_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
|
// Actions
|
||||||
async function list(sources?: (ServiceIdentifier | CollectionIdentifier)[], filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise<Record<string, EntityObject>> {
|
async function list(sources?: SourceSelector, filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise<Record<string, EntityObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.listBulk({ sources, filter, sort, range })
|
const response = await entityService.list({ sources, filter, sort, range })
|
||||||
|
|
||||||
const hydrated: Record<string, EntityObject> = {}
|
const hydrated: Record<string, EntityObject> = {}
|
||||||
Object.entries(response).forEach(([providerId, providerServices]) => {
|
Object.entries(response).forEach(([providerId, providerServices]) => {
|
||||||
@@ -140,13 +130,24 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetch(
|
async function fetch(
|
||||||
targets: EntityIdentifier[],
|
provider: string,
|
||||||
|
service: string | number,
|
||||||
|
collection: string | number,
|
||||||
|
identifiers: (string | number)[],
|
||||||
): Promise<Record<string, EntityObject>> {
|
): Promise<Record<string, EntityObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.fetch({ targets })
|
const response = await entityService.fetch({ provider, service, collection, identifiers })
|
||||||
_entities.value = { ..._entities.value, ...response }
|
|
||||||
return response
|
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
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to fetch entities:', error)
|
console.error('[Documents Manager][Store] - Failed to fetch entities:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -155,10 +156,10 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extant(targets: (CollectionIdentifier | EntityIdentifier)[]) {
|
async function extant(sources: SourceSelector) {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.extant({ targets })
|
const response = await entityService.extant({ sources })
|
||||||
console.debug('[Documents Manager][Store] - Successfully checked entity availability')
|
console.debug('[Documents Manager][Store] - Successfully checked entity availability')
|
||||||
return response
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -170,13 +171,15 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function create(
|
async function create(
|
||||||
target: CollectionIdentifier,
|
provider: string,
|
||||||
|
service: string | number,
|
||||||
|
collection: string | number,
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
options?: Record<string, unknown>,
|
options?: Record<string, unknown>,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.create({ target, properties, options })
|
const response = await entityService.create({ provider, service, collection, properties, options })
|
||||||
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
||||||
_entities.value[key] = response
|
_entities.value[key] = response
|
||||||
|
|
||||||
@@ -191,12 +194,15 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function update(
|
async function update(
|
||||||
target: EntityIdentifier,
|
provider: string,
|
||||||
|
service: string | number,
|
||||||
|
collection: string | number,
|
||||||
|
identifier: string | number,
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.update({ target, properties })
|
const response = await entityService.update({ provider, service, collection, identifier, properties })
|
||||||
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
const key = identifierKey(response.provider, response.service, response.collection, response.identifier)
|
||||||
_entities.value[key] = response
|
_entities.value[key] = response
|
||||||
|
|
||||||
@@ -211,22 +217,21 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function remove(
|
async function remove(
|
||||||
targets: EntityIdentifier[],
|
provider: string,
|
||||||
): Promise<{ successes: EntityIdentifier[]; failures: EntityIdentifier[] }> {
|
service: string | number,
|
||||||
|
collection: string | number,
|
||||||
|
identifier: string | number,
|
||||||
|
): Promise<EntityDeleteResponse> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.delete({ targets })
|
const response = await entityService.delete({ provider, service, collection, identifier })
|
||||||
const successes: EntityIdentifier[] = []
|
if (response.success) {
|
||||||
const failures: EntityIdentifier[] = []
|
const key = identifierKey(provider, service, collection, identifier)
|
||||||
for (const target of targets) {
|
delete _entities.value[key]
|
||||||
if (response[target]?.disposition === 'deleted') {
|
|
||||||
delete _entities.value[target]
|
|
||||||
successes.push(target)
|
|
||||||
} else {
|
|
||||||
failures.push(target)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { successes, failures }
|
|
||||||
|
console.debug('[Documents Manager][Store] - Successfully deleted entity:', `${provider}:${service}:${collection}:${identifier}`)
|
||||||
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to delete entity:', error)
|
console.error('[Documents Manager][Store] - Failed to delete entity:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -235,10 +240,10 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function delta(targets: (CollectionIdentifier | EntityIdentifier)[]): Promise<EntityDeltaResponse> {
|
async function delta(sources: SourceSelector): Promise<EntityDeltaResponse> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.delta({ targets })
|
const response = await entityService.delta({ sources })
|
||||||
|
|
||||||
Object.entries(response).forEach(([provider, providerData]) => {
|
Object.entries(response).forEach(([provider, providerData]) => {
|
||||||
if (providerData === false) return
|
if (providerData === false) return
|
||||||
@@ -268,11 +273,14 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function read(
|
async function read(
|
||||||
target: EntityIdentifier,
|
provider: string,
|
||||||
|
service: string | number,
|
||||||
|
collection: string | number,
|
||||||
|
identifier: string | number,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.read({ target })
|
const response = await entityService.read({ provider, service, collection, identifier })
|
||||||
return response.content
|
return response.content
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to read entity:', error)
|
console.error('[Documents Manager][Store] - Failed to read entity:', error)
|
||||||
@@ -283,12 +291,15 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function write(
|
async function write(
|
||||||
target: EntityIdentifier,
|
provider: string,
|
||||||
|
service: string | number,
|
||||||
|
collection: string | number,
|
||||||
|
identifier: string | number,
|
||||||
content: string,
|
content: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.write({ target, content, encoding: 'base64' })
|
const response = await entityService.write({ provider, service, collection, identifier, content, encoding: 'base64' })
|
||||||
return response.bytesWritten
|
return response.bytesWritten
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to write entity:', error)
|
console.error('[Documents Manager][Store] - Failed to write entity:', error)
|
||||||
@@ -310,7 +321,6 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
|
|||||||
clearService,
|
clearService,
|
||||||
clearCollection,
|
clearCollection,
|
||||||
clearAll,
|
clearAll,
|
||||||
mergeMany,
|
|
||||||
list,
|
list,
|
||||||
fetch,
|
fetch,
|
||||||
extant,
|
extant,
|
||||||
|
|||||||
+77
-78
@@ -5,9 +5,7 @@
|
|||||||
import { computed, ref, readonly } from 'vue'
|
import { computed, ref, readonly } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type {
|
import type {
|
||||||
CollectionIdentifier,
|
SourceSelector,
|
||||||
EntityIdentifier,
|
|
||||||
ServiceIdentifier,
|
|
||||||
ListFilter,
|
ListFilter,
|
||||||
ListSort,
|
ListSort,
|
||||||
ListRange,
|
ListRange,
|
||||||
@@ -17,7 +15,6 @@ import type {
|
|||||||
import { CollectionObject, EntityObject } from '../models'
|
import { CollectionObject, EntityObject } from '../models'
|
||||||
import { useCollectionsStore } from './collectionsStore'
|
import { useCollectionsStore } from './collectionsStore'
|
||||||
import { useEntitiesStore } from './entitiesStore'
|
import { useEntitiesStore } from './entitiesStore'
|
||||||
import { nodeService } from '../services/nodeService'
|
|
||||||
|
|
||||||
export const ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
export const ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
||||||
|
|
||||||
@@ -48,8 +45,8 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
|
|
||||||
function getServiceNodes(providerId: string, serviceId: string | number): NodeRecord[] {
|
function getServiceNodes(providerId: string, serviceId: string | number): NodeRecord[] {
|
||||||
return [
|
return [
|
||||||
...collectionsStore.collectionsForService(`${providerId}:${serviceId}`),
|
...collectionsStore.collectionsForService(providerId, serviceId),
|
||||||
...entitiesStore.entitiesForService(`${providerId}:${serviceId}`),
|
...entitiesStore.entitiesForService(providerId, serviceId),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,13 +104,24 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCollections(
|
async function fetchCollections(
|
||||||
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
providerId: string,
|
||||||
|
serviceId: string | number,
|
||||||
|
collectionId: string | number | null,
|
||||||
filter?: ListFilter,
|
filter?: ListFilter,
|
||||||
sort?: ListSort,
|
sort?: ListSort,
|
||||||
): Promise<CollectionObject[]> {
|
): Promise<CollectionObject[]> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return Object.values(await collectionsStore.list(sources, filter, sort))
|
const sources: SourceSelector = {
|
||||||
|
[providerId]: {
|
||||||
|
[String(serviceId)]: collectionId === null
|
||||||
|
? true
|
||||||
|
: { [String(collectionId)]: true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await collectionsStore.list(sources, filter, sort)
|
||||||
|
return collectionsStore.collectionsForService(providerId, serviceId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to fetch collections'
|
error.value = e instanceof Error ? e.message : 'Failed to fetch collections'
|
||||||
throw e
|
throw e
|
||||||
@@ -121,76 +129,47 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchEntities(
|
async function fetchEntities(
|
||||||
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
providerId: string,
|
||||||
|
serviceId: string | number,
|
||||||
|
collectionId: string | number | null,
|
||||||
filter?: ListFilter,
|
filter?: ListFilter,
|
||||||
sort?: ListSort,
|
sort?: ListSort,
|
||||||
range?: ListRange,
|
range?: ListRange,
|
||||||
): Promise<EntityObject[]> {
|
): Promise<EntityObject[]> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return Object.values(await entitiesStore.list(sources, filter, sort, range))
|
const sources: SourceSelector = {
|
||||||
|
[providerId]: {
|
||||||
|
[String(serviceId)]: collectionId === null
|
||||||
|
? true
|
||||||
|
: { [String(collectionId)]: true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await entitiesStore.list(sources, filter, sort, range)
|
||||||
|
return entitiesStore.entitiesForCollection(providerId, serviceId, collectionId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to fetch entities'
|
error.value = e instanceof Error ? e.message : 'Failed to fetch entities'
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a unified node listing, optionally limited to a page by range.
|
|
||||||
*/
|
|
||||||
async function fetchNodes(
|
async function fetchNodes(
|
||||||
sources?: (ServiceIdentifier | CollectionIdentifier)[],
|
providerId: string,
|
||||||
|
serviceId: string | number,
|
||||||
|
parentId: string | number | null = ROOT_ID,
|
||||||
filter?: ListFilter,
|
filter?: ListFilter,
|
||||||
sort?: ListSort,
|
sort?: ListSort,
|
||||||
range?: ListRange,
|
range?: ListRange,
|
||||||
mode: 'reset' | 'append' = 'reset',
|
): Promise<NodeRecord[]> {
|
||||||
): Promise<{ collections: CollectionObject[]; entities: EntityObject[]; isLastPage: boolean }> {
|
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const result = await nodeService.list({ sources, filter, sort, range })
|
await Promise.all([
|
||||||
|
fetchCollections(providerId, serviceId, parentId, filter, sort),
|
||||||
|
fetchEntities(providerId, serviceId, parentId, filter, sort, range),
|
||||||
|
])
|
||||||
|
|
||||||
// Service selectors list the root; collection selectors list that folder.
|
return getChildren(providerId, serviceId, parentId)
|
||||||
// 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) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to fetch nodes'
|
error.value = e instanceof Error ? e.message : 'Failed to fetch nodes'
|
||||||
throw e
|
throw e
|
||||||
@@ -198,12 +177,14 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createCollection(
|
async function createCollection(
|
||||||
target: ServiceIdentifier | CollectionIdentifier,
|
providerId: string,
|
||||||
|
serviceId: string | number,
|
||||||
|
parentCollectionId: string | number | null,
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await collectionsStore.create(target, properties)
|
return await collectionsStore.create(providerId, serviceId, parentCollectionId, properties)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to create collection'
|
error.value = e instanceof Error ? e.message : 'Failed to create collection'
|
||||||
throw e
|
throw e
|
||||||
@@ -211,12 +192,14 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateCollection(
|
async function updateCollection(
|
||||||
target: CollectionIdentifier,
|
providerId: string,
|
||||||
|
serviceId: string | number,
|
||||||
|
identifier: string | number,
|
||||||
properties: CollectionMutableProperties,
|
properties: CollectionMutableProperties,
|
||||||
): Promise<CollectionObject> {
|
): Promise<CollectionObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await collectionsStore.update(target, properties)
|
return await collectionsStore.update(providerId, serviceId, identifier, properties)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to update collection'
|
error.value = e instanceof Error ? e.message : 'Failed to update collection'
|
||||||
throw e
|
throw e
|
||||||
@@ -224,11 +207,13 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCollection(
|
async function deleteCollection(
|
||||||
target: CollectionIdentifier,
|
providerId: string,
|
||||||
|
serviceId: string | number,
|
||||||
|
identifier: string | number,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const response = await collectionsStore.delete(target)
|
const response = await collectionsStore.delete(providerId, serviceId, identifier)
|
||||||
return response.success
|
return response.success
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to delete collection'
|
error.value = e instanceof Error ? e.message : 'Failed to delete collection'
|
||||||
@@ -237,13 +222,15 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createEntity(
|
async function createEntity(
|
||||||
target: CollectionIdentifier,
|
providerId: string,
|
||||||
|
serviceId: string | number,
|
||||||
|
collectionId: string | number,
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
options?: Record<string, unknown>,
|
options?: Record<string, unknown>,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.create(target, properties, options)
|
return await entitiesStore.create(providerId, serviceId, collectionId, properties, options)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to create entity'
|
error.value = e instanceof Error ? e.message : 'Failed to create entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -251,12 +238,15 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateEntity(
|
async function updateEntity(
|
||||||
target: EntityIdentifier,
|
providerId: string,
|
||||||
|
serviceId: string | number,
|
||||||
|
collectionId: string | number,
|
||||||
|
identifier: string | number,
|
||||||
properties: DocumentInterface,
|
properties: DocumentInterface,
|
||||||
): Promise<EntityObject> {
|
): Promise<EntityObject> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.update(target, properties)
|
return await entitiesStore.update(providerId, serviceId, collectionId, identifier, properties)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to update entity'
|
error.value = e instanceof Error ? e.message : 'Failed to update entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -264,12 +254,15 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteEntity(
|
async function deleteEntity(
|
||||||
target: EntityIdentifier,
|
providerId: string,
|
||||||
|
serviceId: string | number,
|
||||||
|
collectionId: string | number,
|
||||||
|
identifier: string | number,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
const response = await entitiesStore.delete([target])
|
const response = await entitiesStore.delete(providerId, serviceId, collectionId, identifier)
|
||||||
return response.successes.includes(target)
|
return response.success
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to delete entity'
|
error.value = e instanceof Error ? e.message : 'Failed to delete entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -277,11 +270,14 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function readEntity(
|
async function readEntity(
|
||||||
target: EntityIdentifier,
|
providerId: string,
|
||||||
|
serviceId: string | number,
|
||||||
|
collectionId: string | number,
|
||||||
|
identifier: string | number,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.read(target)
|
return await entitiesStore.read(providerId, serviceId, collectionId, identifier)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to read entity'
|
error.value = e instanceof Error ? e.message : 'Failed to read entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -289,12 +285,15 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function writeEntity(
|
async function writeEntity(
|
||||||
target: EntityIdentifier,
|
providerId: string,
|
||||||
|
serviceId: string | number,
|
||||||
|
collectionId: string | number,
|
||||||
|
identifier: string | number,
|
||||||
content: string,
|
content: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
return await entitiesStore.write(target, content)
|
return await entitiesStore.write(providerId, serviceId, collectionId, identifier, content)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to write entity'
|
error.value = e instanceof Error ? e.message : 'Failed to write entity'
|
||||||
throw e
|
throw e
|
||||||
@@ -302,8 +301,8 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearServiceNodes(providerId: string, serviceId: string | number): void {
|
function clearServiceNodes(providerId: string, serviceId: string | number): void {
|
||||||
collectionsStore.clearService(`${providerId}:${serviceId}`)
|
collectionsStore.clearService(providerId, serviceId)
|
||||||
entitiesStore.clearService(`${providerId}:${serviceId}`)
|
entitiesStore.clearService(providerId, serviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearNodes(): void {
|
function clearNodes(): void {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ref, computed, readonly } from 'vue'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { providerService } from '../services'
|
import { providerService } from '../services'
|
||||||
import { ProviderObject } from '../models/provider'
|
import { ProviderObject } from '../models/provider'
|
||||||
import type { ProviderIdentifier } from '../types'
|
import type { SourceSelector } from '../types'
|
||||||
|
|
||||||
export const useProvidersStore = defineStore('documentsProvidersStore', () => {
|
export const useProvidersStore = defineStore('documentsProvidersStore', () => {
|
||||||
// State
|
// State
|
||||||
@@ -54,10 +54,10 @@ export const useProvidersStore = defineStore('documentsProvidersStore', () => {
|
|||||||
*
|
*
|
||||||
* @returns Promise with provider object list keyed by provider identifier
|
* @returns Promise with provider object list keyed by provider identifier
|
||||||
*/
|
*/
|
||||||
async function list(targets?: ProviderIdentifier[]): Promise<Record<string, ProviderObject>> {
|
async function list(sources?: SourceSelector): Promise<Record<string, ProviderObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const providers = await providerService.list({ targets })
|
const providers = await providerService.list({ sources })
|
||||||
|
|
||||||
// Merge retrieved providers into state
|
// Merge retrieved providers into state
|
||||||
_providers.value = { ..._providers.value, ...providers }
|
_providers.value = { ..._providers.value, ...providers }
|
||||||
@@ -82,7 +82,7 @@ export const useProvidersStore = defineStore('documentsProvidersStore', () => {
|
|||||||
async function fetch(identifier: string): Promise<ProviderObject> {
|
async function fetch(identifier: string): Promise<ProviderObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const provider = await providerService.fetch({ target: identifier })
|
const provider = await providerService.fetch({ identifier })
|
||||||
|
|
||||||
// Merge fetched provider into state
|
// Merge fetched provider into state
|
||||||
_providers.value[provider.identifier] = provider
|
_providers.value[provider.identifier] = provider
|
||||||
@@ -100,14 +100,14 @@ export const useProvidersStore = defineStore('documentsProvidersStore', () => {
|
|||||||
/**
|
/**
|
||||||
* Retrieve provider availability status for a given source selector
|
* Retrieve provider availability status for a given source selector
|
||||||
*
|
*
|
||||||
* @param targets - provider identifiers to check availability for
|
* @param sources - source selector to check availability for
|
||||||
*
|
*
|
||||||
* @returns Promise with provider availability status
|
* @returns Promise with provider availability status
|
||||||
*/
|
*/
|
||||||
async function extant(targets: ProviderIdentifier[]) {
|
async function extant(sources: SourceSelector) {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await providerService.extant({ targets })
|
const response = await providerService.extant({ sources })
|
||||||
|
|
||||||
Object.entries(response).forEach(([providerId, providerStatus]) => {
|
Object.entries(response).forEach(([providerId, providerStatus]) => {
|
||||||
if (providerStatus === false) {
|
if (providerStatus === false) {
|
||||||
@@ -115,7 +115,7 @@ export const useProvidersStore = defineStore('documentsProvidersStore', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.debug('[Documents Manager][Store] - Successfully checked', targets ? targets.length : 0, 'providers')
|
console.debug('[Documents Manager][Store] - Successfully checked', sources ? Object.keys(sources).length : 0, 'providers')
|
||||||
return response
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to check providers:', error)
|
console.error('[Documents Manager][Store] - Failed to check providers:', error)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { defineStore } from 'pinia'
|
|||||||
import { serviceService } from '../services'
|
import { serviceService } from '../services'
|
||||||
import { ServiceObject } from '../models/service'
|
import { ServiceObject } from '../models/service'
|
||||||
import type {
|
import type {
|
||||||
ServiceIdentifier,
|
SourceSelector,
|
||||||
ServiceInterface,
|
ServiceInterface,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
@@ -76,14 +76,14 @@ export const useServicesStore = defineStore('documentsServicesStore', () => {
|
|||||||
/**
|
/**
|
||||||
* Retrieve all or specific services, optionally filtered by source selector
|
* Retrieve all or specific services, optionally filtered by source selector
|
||||||
*
|
*
|
||||||
* @param targets - optional service identifiers
|
* @param sources - optional source selector
|
||||||
*
|
*
|
||||||
* @returns Promise with service object list keyed by provider and service identifier
|
* @returns Promise with service object list keyed by provider and service identifier
|
||||||
*/
|
*/
|
||||||
async function list(targets?: ServiceIdentifier[]): Promise<Record<string, ServiceObject>> {
|
async function list(sources?: SourceSelector): Promise<Record<string, ServiceObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await serviceService.list({ targets })
|
const response = await serviceService.list({ sources })
|
||||||
|
|
||||||
// Flatten nested structure: provider-id: { service-id: object } -> "provider-id:service-id": object
|
// Flatten nested structure: provider-id: { service-id: object } -> "provider-id:service-id": object
|
||||||
const services: Record<string, ServiceObject> = {}
|
const services: Record<string, ServiceObject> = {}
|
||||||
@@ -137,16 +137,16 @@ export const useServicesStore = defineStore('documentsServicesStore', () => {
|
|||||||
/**
|
/**
|
||||||
* Retrieve service availability status for a given source selector
|
* Retrieve service availability status for a given source selector
|
||||||
*
|
*
|
||||||
* @param targets - service identifiers to check availability for
|
* @param sources - source selector to check availability for
|
||||||
*
|
*
|
||||||
* @returns Promise with service availability status
|
* @returns Promise with service availability status
|
||||||
*/
|
*/
|
||||||
async function extant(targets: ServiceIdentifier[]) {
|
async function extant(sources: SourceSelector) {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await serviceService.extant({ targets })
|
const response = await serviceService.extant({ sources })
|
||||||
|
|
||||||
console.debug('[Documents Manager][Store] - Successfully checked', targets ? targets.length : 0, 'services')
|
console.debug('[Documents Manager][Store] - Successfully checked', sources ? Object.keys(sources).length : 0, 'services')
|
||||||
return response
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Documents Manager][Store] - Failed to check services:', error)
|
console.error('[Documents Manager][Store] - Failed to check services:', error)
|
||||||
|
|||||||
+25
-15
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Collection type definitions
|
* Collection type definitions
|
||||||
*/
|
*/
|
||||||
import type { CollectionIdentifier, ListFilter, ListSort, ServiceIdentifier } from './common';
|
import type { ListFilter, ListSort, SourceSelector } from './common';
|
||||||
|
|
||||||
|
|
||||||
export interface CollectionModelInterface extends Omit<CollectionInterface, '@type' | 'created' | 'modified'> {
|
export interface CollectionModelInterface extends Omit<CollectionInterface, '@type' | 'created' | 'modified'> {
|
||||||
@@ -46,7 +46,7 @@ export interface CollectionPropertiesInterface extends CollectionMutableProperti
|
|||||||
* Collection list
|
* Collection list
|
||||||
*/
|
*/
|
||||||
export interface CollectionListRequest {
|
export interface CollectionListRequest {
|
||||||
sources?: (ServiceIdentifier | CollectionIdentifier)[];
|
sources?: SourceSelector;
|
||||||
filter?: ListFilter;
|
filter?: ListFilter;
|
||||||
sort?: ListSort;
|
sort?: ListSort;
|
||||||
}
|
}
|
||||||
@@ -63,16 +63,18 @@ export interface CollectionListResponse {
|
|||||||
* Collection fetch
|
* Collection fetch
|
||||||
*/
|
*/
|
||||||
export interface CollectionFetchRequest {
|
export interface CollectionFetchRequest {
|
||||||
targets: CollectionIdentifier[];
|
provider: string;
|
||||||
|
service: string | number;
|
||||||
|
collection: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CollectionFetchResponse = Record<CollectionIdentifier, CollectionInterface>;
|
export interface CollectionFetchResponse extends CollectionInterface {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection extant
|
* Collection extant
|
||||||
*/
|
*/
|
||||||
export interface CollectionExtantRequest {
|
export interface CollectionExtantRequest {
|
||||||
targets: CollectionIdentifier[];
|
sources: SourceSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionExtantResponse {
|
export interface CollectionExtantResponse {
|
||||||
@@ -87,9 +89,10 @@ export interface CollectionExtantResponse {
|
|||||||
* Collection create
|
* Collection create
|
||||||
*/
|
*/
|
||||||
export interface CollectionCreateRequest {
|
export interface CollectionCreateRequest {
|
||||||
target: ServiceIdentifier | CollectionIdentifier; // Service root or parent collection
|
provider: string;
|
||||||
|
service: string | number;
|
||||||
|
collection?: string | number | null; // Parent Collection Identifier
|
||||||
properties: CollectionMutableProperties;
|
properties: CollectionMutableProperties;
|
||||||
options?: Record<string, unknown>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionCreateResponse extends CollectionInterface {}
|
export interface CollectionCreateResponse extends CollectionInterface {}
|
||||||
@@ -98,7 +101,9 @@ export interface CollectionCreateResponse extends CollectionInterface {}
|
|||||||
* Collection modify
|
* Collection modify
|
||||||
*/
|
*/
|
||||||
export interface CollectionUpdateRequest {
|
export interface CollectionUpdateRequest {
|
||||||
target: CollectionIdentifier;
|
provider: string;
|
||||||
|
service: string | number;
|
||||||
|
identifier: string | number;
|
||||||
properties: CollectionMutableProperties;
|
properties: CollectionMutableProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,23 +113,26 @@ export interface CollectionUpdateResponse extends CollectionInterface {}
|
|||||||
* Collection delete
|
* Collection delete
|
||||||
*/
|
*/
|
||||||
export interface CollectionDeleteRequest {
|
export interface CollectionDeleteRequest {
|
||||||
target: CollectionIdentifier;
|
provider: string;
|
||||||
|
service: string | number;
|
||||||
|
identifier: string | number;
|
||||||
options?: {
|
options?: {
|
||||||
force?: boolean; // Whether to force delete even if collection is not empty
|
force?: boolean; // Whether to force delete even if collection is not empty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionDeleteResponse {
|
export interface CollectionDeleteResponse {
|
||||||
disposition: 'deleted' | 'moved';
|
success: boolean;
|
||||||
mutation?: CollectionInterface;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection copy
|
* Collection copy
|
||||||
*/
|
*/
|
||||||
export interface CollectionCopyRequest {
|
export interface CollectionCopyRequest {
|
||||||
source: CollectionIdentifier;
|
provider: string;
|
||||||
target?: CollectionIdentifier | null; // Destination parent (absent for root)
|
service: string;
|
||||||
|
identifier: string;
|
||||||
|
location?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionCopyResponse extends CollectionInterface {}
|
export interface CollectionCopyResponse extends CollectionInterface {}
|
||||||
@@ -133,8 +141,10 @@ export interface CollectionCopyResponse extends CollectionInterface {}
|
|||||||
* Collection move
|
* Collection move
|
||||||
*/
|
*/
|
||||||
export interface CollectionMoveRequest {
|
export interface CollectionMoveRequest {
|
||||||
source: CollectionIdentifier;
|
provider: string;
|
||||||
target?: CollectionIdentifier | null; // Destination parent (absent for root)
|
service: string;
|
||||||
|
identifier: string;
|
||||||
|
location?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionMoveResponse extends CollectionInterface {}
|
export interface CollectionMoveResponse extends CollectionInterface {}
|
||||||
+25
-49
@@ -44,57 +44,33 @@ export interface ApiErrorResponse {
|
|||||||
export type ApiResponse<T = any> = ApiSuccessResponse<T> | ApiErrorResponse;
|
export type ApiResponse<T = any> = ApiSuccessResponse<T> | ApiErrorResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stream control start line.
|
* Selector for targeting specific providers, services, collections, or entities in list or extant operations.
|
||||||
*/
|
|
||||||
export interface ApiStreamStartResponse {
|
|
||||||
type: 'control';
|
|
||||||
status: 'start';
|
|
||||||
version: number;
|
|
||||||
transaction: string;
|
|
||||||
total?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stream control end line
|
|
||||||
*/
|
|
||||||
export interface ApiStreamEndResponse {
|
|
||||||
type: 'control';
|
|
||||||
status: 'end';
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stream error line
|
|
||||||
*/
|
|
||||||
export interface ApiStreamErrorResponse {
|
|
||||||
type: 'error';
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiStreamDataResponse<T = any> {
|
|
||||||
type: 'data';
|
|
||||||
data: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared stream control lines
|
|
||||||
*/
|
|
||||||
export type ApiStreamResponse<T = any> =
|
|
||||||
| ApiStreamStartResponse
|
|
||||||
| ApiStreamEndResponse
|
|
||||||
| ApiStreamErrorResponse
|
|
||||||
| ApiStreamDataResponse<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifiers for targeting specific providers, services, collections, or entities in list or extant operations.
|
|
||||||
*
|
*
|
||||||
* Operations accept flat arrays of colon-separated identifier strings, e.g.
|
* Example usage:
|
||||||
* ["default:personal:00000000-0000-0000-0000-000000000000", "default:personal:folder1:file1"].
|
* {
|
||||||
|
* "provider1": true, // Select all services/collections/entities under provider1
|
||||||
|
* "provider2": {
|
||||||
|
* "serviceA": true, // Select all collections/entities under serviceA of provider2
|
||||||
|
* "serviceB": {
|
||||||
|
* "collectionX": true, // Select all entities under collectionX of serviceB of provider2
|
||||||
|
* "collectionY": [1, 2, 3] // Select entities with identifiers 1, 2, and 3 under collectionY of serviceB of provider2
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
export type ProviderIdentifier = `${string}`;
|
export type SourceSelector = {
|
||||||
export type ServiceIdentifier = `${string}:${string}`;
|
[provider: string]: boolean | ServiceSelector;
|
||||||
export type CollectionIdentifier = `${string}:${string}:${string | number}`;
|
};
|
||||||
export type EntityIdentifier = `${string}:${string}:${string}:${string | number}`;
|
|
||||||
|
export type ServiceSelector = {
|
||||||
|
[service: string]: boolean | CollectionSelector;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CollectionSelector = {
|
||||||
|
[collection: string | number]: boolean | EntitySelector;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EntitySelector = (string | number)[];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+40
-31
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Entity type definitions
|
* Entity type definitions
|
||||||
*/
|
*/
|
||||||
import type { CollectionIdentifier, EntityIdentifier, ListFilter, ListRange, ListSort, ServiceIdentifier } from './common';
|
import type { ListFilter, ListRange, ListSort, SourceSelector } from './common';
|
||||||
import type { DocumentInterface, DocumentModelInterface } from './document';
|
import type { DocumentInterface, DocumentModelInterface } from './document';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,21 +25,11 @@ export interface EntityInterface<T = DocumentInterface> {
|
|||||||
properties: T;
|
properties: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity mutation result (delete/move/copy operations)
|
|
||||||
*/
|
|
||||||
export interface EntityMutationResult {
|
|
||||||
disposition: 'deleted' | 'moved' | 'copied' | 'error';
|
|
||||||
destination?: CollectionIdentifier | null;
|
|
||||||
mutation?: EntityIdentifier;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity list
|
* Entity list
|
||||||
*/
|
*/
|
||||||
export interface EntityListRequest {
|
export interface EntityListRequest {
|
||||||
sources?: (ServiceIdentifier | CollectionIdentifier)[];
|
sources?: SourceSelector;
|
||||||
filter?: ListFilter;
|
filter?: ListFilter;
|
||||||
sort?: ListSort;
|
sort?: ListSort;
|
||||||
range?: ListRange;
|
range?: ListRange;
|
||||||
@@ -59,7 +49,10 @@ export interface EntityListResponse {
|
|||||||
* Entity fetch
|
* Entity fetch
|
||||||
*/
|
*/
|
||||||
export interface EntityFetchRequest {
|
export interface EntityFetchRequest {
|
||||||
targets: EntityIdentifier[];
|
provider: string;
|
||||||
|
service: string | number;
|
||||||
|
collection: string | number;
|
||||||
|
identifiers: (string | number)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityFetchResponse {
|
export interface EntityFetchResponse {
|
||||||
@@ -70,7 +63,7 @@ export interface EntityFetchResponse {
|
|||||||
* Entity extant
|
* Entity extant
|
||||||
*/
|
*/
|
||||||
export interface EntityExtantRequest {
|
export interface EntityExtantRequest {
|
||||||
targets: (CollectionIdentifier | EntityIdentifier)[];
|
sources: SourceSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityExtantResponse {
|
export interface EntityExtantResponse {
|
||||||
@@ -87,7 +80,9 @@ export interface EntityExtantResponse {
|
|||||||
* Entity create
|
* Entity create
|
||||||
*/
|
*/
|
||||||
export interface EntityCreateRequest<T = DocumentInterface> {
|
export interface EntityCreateRequest<T = DocumentInterface> {
|
||||||
target: CollectionIdentifier;
|
provider: string;
|
||||||
|
service: string | number;
|
||||||
|
collection: string | number;
|
||||||
properties: T;
|
properties: T;
|
||||||
options?: Record<string, unknown>;
|
options?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
@@ -98,7 +93,10 @@ export interface EntityCreateResponse<T = DocumentInterface> extends EntityInter
|
|||||||
* Entity update
|
* Entity update
|
||||||
*/
|
*/
|
||||||
export interface EntityUpdateRequest<T = DocumentInterface> {
|
export interface EntityUpdateRequest<T = DocumentInterface> {
|
||||||
target: EntityIdentifier;
|
provider: string;
|
||||||
|
service: string | number;
|
||||||
|
collection: string | number;
|
||||||
|
identifier: string | number;
|
||||||
properties: T;
|
properties: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,18 +106,21 @@ export interface EntityUpdateResponse<T = DocumentInterface> extends EntityInter
|
|||||||
* Entity delete
|
* Entity delete
|
||||||
*/
|
*/
|
||||||
export interface EntityDeleteRequest {
|
export interface EntityDeleteRequest {
|
||||||
targets: EntityIdentifier[];
|
provider: string;
|
||||||
|
service: string | number;
|
||||||
|
collection: string | number;
|
||||||
|
identifier: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityDeleteResponse {
|
export interface EntityDeleteResponse {
|
||||||
[identifier: string]: EntityMutationResult;
|
success: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity delta
|
* Entity delta
|
||||||
*/
|
*/
|
||||||
export interface EntityDeltaRequest {
|
export interface EntityDeltaRequest {
|
||||||
targets: (CollectionIdentifier | EntityIdentifier)[];
|
sources: SourceSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityDeltaResponse {
|
export interface EntityDeltaResponse {
|
||||||
@@ -139,31 +140,36 @@ export interface EntityDeltaResponse {
|
|||||||
* Entity copy
|
* Entity copy
|
||||||
*/
|
*/
|
||||||
export interface EntityCopyRequest {
|
export interface EntityCopyRequest {
|
||||||
target: CollectionIdentifier;
|
provider: string;
|
||||||
sources: EntityIdentifier[];
|
service: string | number;
|
||||||
|
collection: string | number;
|
||||||
|
identifier: string | number;
|
||||||
|
destination?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityCopyResponse {
|
export interface EntityCopyResponse<T = DocumentInterface> extends EntityInterface<T> {}
|
||||||
[identifier: string]: EntityMutationResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity move
|
* Entity move
|
||||||
*/
|
*/
|
||||||
export interface EntityMoveRequest {
|
export interface EntityMoveRequest {
|
||||||
target: CollectionIdentifier;
|
provider: string;
|
||||||
sources: EntityIdentifier[];
|
service: string | number;
|
||||||
|
collection: string | number;
|
||||||
|
identifier: string | number;
|
||||||
|
destination?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityMoveResponse {
|
export interface EntityMoveResponse<T = DocumentInterface> extends EntityInterface<T> {}
|
||||||
[identifier: string]: EntityMutationResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity read content
|
* Entity read content
|
||||||
*/
|
*/
|
||||||
export interface EntityReadRequest {
|
export interface EntityReadRequest {
|
||||||
target: EntityIdentifier;
|
provider: string;
|
||||||
|
service: string | number;
|
||||||
|
collection: string | number;
|
||||||
|
identifier: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityReadResult {
|
export interface EntityReadResult {
|
||||||
@@ -177,7 +183,10 @@ export type EntityReadResponse = EntityReadResult;
|
|||||||
* Entity write content
|
* Entity write content
|
||||||
*/
|
*/
|
||||||
export interface EntityWriteRequest {
|
export interface EntityWriteRequest {
|
||||||
target: EntityIdentifier;
|
provider: string;
|
||||||
|
service: string | number;
|
||||||
|
collection: string | number;
|
||||||
|
identifier: string | number;
|
||||||
content: string;
|
content: string;
|
||||||
encoding?: 'base64';
|
encoding?: 'base64';
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -3,5 +3,5 @@ export type * from './provider';
|
|||||||
export type * from './service';
|
export type * from './service';
|
||||||
export type * from './collection';
|
export type * from './collection';
|
||||||
export type * from './entity';
|
export type * from './entity';
|
||||||
export type * from './node';
|
|
||||||
export type * from './document';
|
export type * from './document';
|
||||||
|
export type * from './node';
|
||||||
|
|||||||
+30
-16
@@ -1,23 +1,37 @@
|
|||||||
/**
|
/**
|
||||||
* Node (unified collections + entities) type definitions
|
* Node types for combined operations
|
||||||
*/
|
*/
|
||||||
import type { CollectionIdentifier, ListFilter, ListRange, ListSort, ServiceIdentifier } from './common';
|
|
||||||
import type { CollectionInterface } from './collection';
|
|
||||||
import type { EntityInterface } from './entity';
|
|
||||||
|
|
||||||
/**
|
import type { CollectionInterface } from "./collection";
|
||||||
* Node list
|
import type { ApiResponse, ListFilterCondition, ListRange, ListSort } from "./common";
|
||||||
*/
|
import type { EntityInterface } from "./entity";
|
||||||
|
|
||||||
export interface NodeListRequest {
|
export interface NodeListRequest {
|
||||||
sources?: (ServiceIdentifier | CollectionIdentifier)[];
|
provider: string;
|
||||||
filter?: ListFilter;
|
service: string;
|
||||||
sort?: ListSort;
|
location?: string | null;
|
||||||
range?: ListRange;
|
recursive?: boolean;
|
||||||
|
filter?: ListFilterCondition | null;
|
||||||
|
sort?: ListSort | null;
|
||||||
|
range?: ListRange | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NodeInterface =
|
export type NodeListResponse = ApiResponse<CollectionInterface | EntityInterface>;
|
||||||
| (Omit<CollectionInterface, '@type'> & { '@type': 'document:collection' })
|
|
||||||
| (Omit<EntityInterface, '@type'> & { '@type': 'document:entity' });
|
|
||||||
|
|
||||||
/** Payload of each data frame in the node.list NDJSON stream. */
|
export interface NodeDeltaRequest {
|
||||||
export type NodeListResponse = NodeInterface;
|
provider: string;
|
||||||
|
service: string;
|
||||||
|
location?: string | null;
|
||||||
|
signature: string;
|
||||||
|
recursive?: boolean;
|
||||||
|
detail?: 'ids' | 'full';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeDeltaResult {
|
||||||
|
added: string[];
|
||||||
|
modified: string[];
|
||||||
|
removed: string[];
|
||||||
|
signature: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeDeltaResponse = ApiResponse<NodeDeltaResult>;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Provider type definitions
|
* Provider type definitions
|
||||||
*/
|
*/
|
||||||
import type { ProviderIdentifier } from "./common";
|
import type { SourceSelector } from "./common";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider capabilities
|
* Provider capabilities
|
||||||
@@ -32,7 +32,7 @@ export interface ProviderInterface {
|
|||||||
* Provider list
|
* Provider list
|
||||||
*/
|
*/
|
||||||
export interface ProviderListRequest {
|
export interface ProviderListRequest {
|
||||||
targets?: ProviderIdentifier[];
|
sources?: SourceSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderListResponse {
|
export interface ProviderListResponse {
|
||||||
@@ -43,7 +43,7 @@ export interface ProviderListResponse {
|
|||||||
* Provider fetch
|
* Provider fetch
|
||||||
*/
|
*/
|
||||||
export interface ProviderFetchRequest {
|
export interface ProviderFetchRequest {
|
||||||
target: ProviderIdentifier;
|
identifier: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderFetchResponse extends ProviderInterface {}
|
export interface ProviderFetchResponse extends ProviderInterface {}
|
||||||
@@ -52,7 +52,7 @@ export interface ProviderFetchResponse extends ProviderInterface {}
|
|||||||
* Provider extant
|
* Provider extant
|
||||||
*/
|
*/
|
||||||
export interface ProviderExtantRequest {
|
export interface ProviderExtantRequest {
|
||||||
targets: ProviderIdentifier[];
|
sources: SourceSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderExtantResponse {
|
export interface ProviderExtantResponse {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Service type definitions
|
* Service type definitions
|
||||||
*/
|
*/
|
||||||
import type { ServiceIdentifier as ServiceIdentifierString, ListFilterComparisonOperator } from './common';
|
import type { SourceSelector, ListFilterComparisonOperator } from './common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service capabilities
|
* Service capabilities
|
||||||
@@ -66,7 +66,7 @@ export interface ServiceInterface {
|
|||||||
* Service list
|
* Service list
|
||||||
*/
|
*/
|
||||||
export interface ServiceListRequest {
|
export interface ServiceListRequest {
|
||||||
targets?: ServiceIdentifierString[];
|
sources?: SourceSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceListResponse {
|
export interface ServiceListResponse {
|
||||||
@@ -89,7 +89,7 @@ export interface ServiceFetchResponse extends ServiceInterface {}
|
|||||||
* Service extant
|
* Service extant
|
||||||
*/
|
*/
|
||||||
export interface ServiceExtantRequest {
|
export interface ServiceExtantRequest {
|
||||||
targets: ServiceIdentifierString[];
|
sources: SourceSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceExtantResponse {
|
export interface ServiceExtantResponse {
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { EntityObject } from '@/models/entity'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the appropriate icon for a file based on its MIME type
|
|
||||||
*/
|
|
||||||
export function getFileIcon(entity: EntityObject): string {
|
|
||||||
const mime = entity.properties.mime || ''
|
|
||||||
if (!mime) return 'mdi-file'
|
|
||||||
if (mime.startsWith('image/')) return 'mdi-file-image'
|
|
||||||
if (mime.startsWith('video/')) return 'mdi-file-video'
|
|
||||||
if (mime.startsWith('audio/')) return 'mdi-file-music'
|
|
||||||
if (mime.startsWith('text/')) return 'mdi-file-document'
|
|
||||||
if (mime === 'application/pdf') return 'mdi-file-pdf-box'
|
|
||||||
if (mime.includes('zip') || mime.includes('tar') || mime.includes('compressed')) return 'mdi-folder-zip'
|
|
||||||
if (mime.includes('word') || mime.includes('document')) return 'mdi-file-word'
|
|
||||||
if (mime.includes('excel') || mime.includes('spreadsheet')) return 'mdi-file-excel'
|
|
||||||
if (mime.includes('powerpoint') || mime.includes('presentation')) return 'mdi-file-powerpoint'
|
|
||||||
return 'mdi-file'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format file size in human readable format
|
|
||||||
*/
|
|
||||||
export function formatSize(bytes: number): string {
|
|
||||||
const safeBytes = Number.isFinite(bytes) ? bytes : 0
|
|
||||||
if (safeBytes === 0) return '0 B'
|
|
||||||
const k = 1024
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
||||||
const i = Math.floor(Math.log(safeBytes) / Math.log(k))
|
|
||||||
return parseFloat((safeBytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format date string to localized format
|
|
||||||
*/
|
|
||||||
export function formatDate(value: Date | string | null | undefined): string {
|
|
||||||
if (!value) return '—'
|
|
||||||
const date = value instanceof Date ? value : new Date(value)
|
|
||||||
if (Number.isNaN(date.getTime())) return '—'
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get upload status icon
|
|
||||||
*/
|
|
||||||
export function getUploadStatusIcon(status: string): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'pending': return 'mdi-clock-outline'
|
|
||||||
case 'uploading': return 'mdi-loading mdi-spin'
|
|
||||||
case 'completed': return 'mdi-check-circle'
|
|
||||||
case 'error': return 'mdi-alert-circle'
|
|
||||||
default: return 'mdi-file'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get upload status color
|
|
||||||
*/
|
|
||||||
export function getUploadStatusColor(status: string): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'pending': return 'grey'
|
|
||||||
case 'uploading': return 'primary'
|
|
||||||
case 'completed': return 'success'
|
|
||||||
case 'error': return 'error'
|
|
||||||
default: return 'grey'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
|
|
||||||
describe('Basic Tests', () => {
|
|
||||||
it('should perform basic assertion', () => {
|
|
||||||
expect(true).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should test array operations', () => {
|
|
||||||
const array = ['foo', 'bar', 'baz']
|
|
||||||
|
|
||||||
expect(array).toHaveLength(3)
|
|
||||||
expect(array).toContain('bar')
|
|
||||||
expect(array[0]).toBe('foo')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should test string operations', () => {
|
|
||||||
const string = 'Hello, World!'
|
|
||||||
|
|
||||||
expect(string).toContain('World')
|
|
||||||
expect(string.length).toBe(13)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should test object operations', () => {
|
|
||||||
const obj = { foo: 'bar', count: 42 }
|
|
||||||
|
|
||||||
expect(obj).toHaveProperty('foo')
|
|
||||||
expect(obj.foo).toBe('bar')
|
|
||||||
expect(obj.count).toBeGreaterThan(40)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import { defineConfig, configDefaults } from 'vitest/config'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
|
||||||
const __dirname = path.dirname(__filename)
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': path.resolve(__dirname, '../../src'),
|
|
||||||
'@KTXC': path.resolve(__dirname, '../../../../core/src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
environment: 'jsdom',
|
|
||||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
|
||||||
root: fileURLToPath(new URL('../../', import.meta.url)),
|
|
||||||
coverage: {
|
|
||||||
provider: 'v8',
|
|
||||||
reporter: ['text', 'json', 'html'],
|
|
||||||
exclude: [
|
|
||||||
'node_modules/',
|
|
||||||
'tests/',
|
|
||||||
'**/*.d.ts',
|
|
||||||
'**/*.config.*',
|
|
||||||
'**/dist/**',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace KTXT\DocumentsManager\Tests\Integration;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
class BaseTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testBasicAssertion(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testArrayOperations(): void
|
|
||||||
{
|
|
||||||
$array = ['foo' => 'bar'];
|
|
||||||
|
|
||||||
$this->assertArrayHasKey('foo', $array);
|
|
||||||
$this->assertEquals('bar', $array['foo']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testStringOperations(): void
|
|
||||||
{
|
|
||||||
$string = 'Hello, World!';
|
|
||||||
|
|
||||||
$this->assertStringContainsString('World', $string);
|
|
||||||
$this->assertEquals(13, strlen($string));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace KTXT\DocumentsManager\Tests\Unit;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
class BaseTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testBasicAssertion(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testArrayOperations(): void
|
|
||||||
{
|
|
||||||
$array = ['foo' => 'bar'];
|
|
||||||
|
|
||||||
$this->assertArrayHasKey('foo', $array);
|
|
||||||
$this->assertEquals('bar', $array['foo']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testStringOperations(): void
|
|
||||||
{
|
|
||||||
$string = 'Hello, World!';
|
|
||||||
|
|
||||||
$this->assertStringContainsString('World', $string);
|
|
||||||
$this->assertEquals(13, strlen($string));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require dirname(__DIR__, 2).'/lib/vendor/autoload.php';
|
|
||||||
|
|
||||||
// When this module is checked out inside a full server (server/modules/<handle>,
|
|
||||||
// as it is in CI and in this monorepo checkout), also load the server's own
|
|
||||||
// core/shared autoloader so tests can reference framework (KTXC/KTXF) types.
|
|
||||||
// Standalone module checkouts without a server alongside them skip this.
|
|
||||||
define('SERVER_ROOT', dirname(__DIR__, 4));
|
|
||||||
$serverAutoload = SERVER_ROOT . '/vendor/autoload.php';
|
|
||||||
if (is_file($serverAutoload)) {
|
|
||||||
require $serverAutoload;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_SERVER['APP_DEBUG']) && $_SERVER['APP_DEBUG']) {
|
|
||||||
umask(0000);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:noNamespaceSchemaLocation="../../lib/vendor/phpunit/phpunit/phpunit.xsd"
|
|
||||||
colors="true"
|
|
||||||
failOnDeprecation="true"
|
|
||||||
failOnNotice="true"
|
|
||||||
failOnWarning="true"
|
|
||||||
bootstrap="bootstrap.php"
|
|
||||||
cacheDirectory="../../.phpunit.cache"
|
|
||||||
>
|
|
||||||
<php>
|
|
||||||
<ini name="display_errors" value="1" />
|
|
||||||
<ini name="error_reporting" value="-1" />
|
|
||||||
<server name="APP_ENV" value="test" force="true" />
|
|
||||||
<server name="SHELL_VERBOSITY" value="-1" />
|
|
||||||
</php>
|
|
||||||
|
|
||||||
<testsuites>
|
|
||||||
<testsuite name="Unit Tests">
|
|
||||||
<directory>Unit</directory>
|
|
||||||
</testsuite>
|
|
||||||
<testsuite name="Integration Tests">
|
|
||||||
<directory>Integration</directory>
|
|
||||||
</testsuite>
|
|
||||||
</testsuites>
|
|
||||||
|
|
||||||
<source ignoreSuppressionOfDeprecations="true"
|
|
||||||
ignoreIndirectDeprecations="true"
|
|
||||||
restrictNotices="true"
|
|
||||||
restrictWarnings="true"
|
|
||||||
>
|
|
||||||
<include>
|
|
||||||
<directory>../../lib</directory>
|
|
||||||
</include>
|
|
||||||
</source>
|
|
||||||
|
|
||||||
<extensions>
|
|
||||||
</extensions>
|
|
||||||
</phpunit>
|
|
||||||
+1
-1
@@ -15,5 +15,5 @@
|
|||||||
"@KTXC/*": ["../../core/src/*"]
|
"@KTXC/*": ["../../core/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user