1 Commits

Author SHA1 Message Date
Sebastian 99bd34f276 chore(deps): update dependency @vue/tsconfig to ^0.9.0
renovate/artifacts Artifact file update failure
2026-05-15 12:24:31 +00:00
39 changed files with 1895 additions and 5194 deletions
-42
View File
@@ -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
-42
View File
@@ -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
-42
View File
@@ -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
+2 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+94 -72
View File
@@ -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;
@@ -28,7 +23,7 @@ use Throwable;
/** /**
* Controller for file transfers (downloads and uploads) * Controller for file transfers (downloads and uploads)
* *
* Handles binary file transfers that don't fit the JSON API pattern: * Handles binary file transfers that don't fit the JSON API pattern:
* - Single file downloads (streamed) * - Single file downloads (streamed)
* - Multi-file downloads as ZIP (streamed) * - Multi-file downloads as ZIP (streamed)
@@ -39,15 +34,15 @@ 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
) {} ) {}
/** /**
* Download a single file * Download a single file
* *
* GET /download/entity/{provider}/{service}/{collection}/{identifier} * GET /download/entity/{provider}/{service}/{collection}/{identifier}
*/ */
#[AuthenticatedRoute( #[AuthenticatedRoute(
@@ -56,15 +51,19 @@ class TransferController extends ControllerAbstract
methods: ['GET'] 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 (empty($entities) || !isset($entities[$identifier])) { if (empty($entities) || !isset($entities[$identifier])) {
return new JsonResponse([ return new JsonResponse([
@@ -74,9 +73,16 @@ class TransferController extends ControllerAbstract
} }
$entity = $entities[$identifier]; $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([
@@ -108,7 +114,7 @@ class TransferController extends ControllerAbstract
if ($size > 0) { if ($size > 0) {
$response->headers->set('Content-Length', (string) $size); $response->headers->set('Content-Length', (string) $size);
} }
$response->headers->set('Content-Disposition', $response->headers->set('Content-Disposition',
$response->headers->makeDisposition('attachment', $filename, $this->asciiFallback($filename)) $response->headers->makeDisposition('attachment', $filename, $this->asciiFallback($filename))
); );
$response->headers->set('Cache-Control', 'private, no-cache'); $response->headers->set('Cache-Control', 'private, no-cache');
@@ -126,7 +132,7 @@ class TransferController extends ControllerAbstract
/** /**
* 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( #[AuthenticatedRoute(
@@ -135,8 +141,8 @@ class TransferController extends ControllerAbstract
methods: ['GET'] 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([
@@ -173,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) {
@@ -212,7 +221,7 @@ class TransferController extends ControllerAbstract
/** /**
* Download a collection (folder) as a ZIP archive with structure preserved * Download a collection (folder) as a ZIP archive with structure preserved
* *
* GET /download/collection/{provider}/{service}/{identifier} * GET /download/collection/{provider}/{service}/{identifier}
*/ */
#[AuthenticatedRoute( #[AuthenticatedRoute(
@@ -221,8 +230,8 @@ class TransferController extends ControllerAbstract
methods: ['GET'] 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
@@ -230,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) {
@@ -240,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
@@ -264,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) {
@@ -311,10 +325,13 @@ class TransferController extends ControllerAbstract
// Try as entity first // Try as entity first
if ($collection !== null) { if ($collection !== null) {
/** @var EntityBaseInterface[] $entities */ /** @var EntityBaseInterface[] $entities */
$entities = $this->manager->entityFetchBulk( $entities = $this->manager->entityFetch(
$tenantId, $tenantId,
$userId, $userId,
new EntityIdentifier($provider, $service, $collection, (string)$id) $provider,
$service,
$collection,
[$id]
); );
if (!empty($entities) && isset($entities[$id])) { if (!empty($entities) && isset($entities[$id])) {
@@ -323,8 +340,8 @@ class TransferController extends ControllerAbstract
'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;
} }
@@ -335,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);
@@ -388,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,
@@ -403,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;
@@ -445,11 +467,11 @@ class TransferController extends ControllerAbstract
// Remove or replace problematic characters // Remove or replace problematic characters
$filename = preg_replace('/[<>:"\/\\|?*\x00-\x1F]/', '_', $filename); $filename = preg_replace('/[<>:"\/\\|?*\x00-\x1F]/', '_', $filename);
$filename = trim($filename, '. '); $filename = trim($filename, '. ');
if ($filename === '') { if ($filename === '') {
$filename = 'download'; $filename = 'download';
} }
return $filename; return $filename;
} }
+619 -774
View File
File diff suppressed because it is too large Load Diff
-20
View File
@@ -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;
}
+275 -1334
View File
File diff suppressed because it is too large Load Diff
+4 -11
View File
@@ -7,24 +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": "^3.0.0",
"vue": "^3.5.13" "vue": "^3.5.13"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node24": "^24.0.0", "@tsconfig/node24": "^24.0.0",
"@types/node": "^24.0.0", "@types/node": "^24.0.0",
"@vue/tsconfig": "^0.9.0", "@vue/tsconfig": "^0.9.0",
"typescript": "~6.0.0", "typescript": "~5.7.2",
"vite": "^8.0.0", "vite": "^8.0.0"
"@vitest/coverage-v8": "^4.1.6",
"jsdom": "^29.1.1",
"vitest": "^4.1.6"
} }
} }
+1 -21
View File
@@ -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
View File
@@ -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);
} }
+2 -30
View File
@@ -16,10 +16,6 @@ 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';
@@ -145,39 +141,15 @@ export const collectionService = {
/** /**
* Delete a collection * Delete a collection
* *
* @param request - delete request parameters * @param request - delete request parameters
* *
* @returns Promise with deletion result * @returns Promise with deletion result
*/ */
async delete(request: CollectionDeleteRequest): Promise<CollectionDeleteResponse> { async delete(request: CollectionDeleteRequest): Promise<CollectionDeleteResponse> {
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;
+12 -52
View File
@@ -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,
@@ -47,14 +43,14 @@ 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,25 +73,11 @@ 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
* *
* @param request - fetch request parameters * @param request - fetch request parameters
* *
* @returns Promise with entity objects keyed by identifier * @returns Promise with entity objects keyed by identifier
*/ */
async fetch(request: EntityFetchRequest): Promise<Record<string, EntityObject>> { async fetch(request: EntityFetchRequest): Promise<Record<string, EntityObject>> {
@@ -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
* *
+1
View File
@@ -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';
+68
View File
@@ -0,0 +1,68 @@
/**
* Node (unified collection/entity) management service
*/
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 = {
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
+3 -82
View File
@@ -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';
@@ -40,90 +40,11 @@ export async function transceivePost<TRequest, TResponse>(
}; };
const response: ApiResponse<TResponse> = await fetchWrapper.post(API_URL, request); const response: ApiResponse<TResponse> = await fetchWrapper.post(API_URL, request);
if (response.status === 'error') { if (response.status === 'error') {
const errorMessage = `[${operation}] ${response.data.message}${response.data.code ? ` (code: ${response.data.code})` : ''}`; const errorMessage = `[${operation}] ${response.data.message}${response.data.code ? ` (code: ${response.data.code})` : ''}`;
throw new Error(errorMessage); throw new Error(errorMessage);
} }
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 }
): Promise<{ total: number }> {
const request: ApiRequest<TRequest> = {
version: API_VERSION,
transaction: generateTransactionId(),
operation,
data,
user: options?.user,
};
let total = 0;
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;
}
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);
}
}
if (buffer.trim()) dispatch(buffer);
} finally {
reader.releaseLock();
}
},
});
return { total };
}
+12 -16
View File
@@ -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'
@@ -71,7 +71,7 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
} }
// 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 })
@@ -101,7 +101,7 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
async function fetch(provider: string, service: string | number, identifier: string | number): Promise<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: [`${provider}:${service}:${identifier}`] }) const response = await collectionService.fetch({ provider, service, collection: identifier })
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
@@ -115,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) {
@@ -137,10 +137,7 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
): Promise<CollectionObject> { ): Promise<CollectionObject> {
transceiving.value = true transceiving.value = true
try { try {
const target: CollectionIdentifier | undefined = collection !== null && collection !== undefined && collection !== '' const response = await collectionService.create({ provider, service, collection, properties })
? `${provider}:${service}:${collection}`
: undefined
const response = await collectionService.create({ provider, service: String(service), target, 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
@@ -162,7 +159,7 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
): Promise<CollectionObject> { ): Promise<CollectionObject> {
transceiving.value = true transceiving.value = true
try { try {
const response = await collectionService.update({ target: `${provider}:${service}:${identifier}`, 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
@@ -176,18 +173,17 @@ export const useCollectionsStore = defineStore('documentsCollectionsStore', () =
} }
} }
async function remove(provider: string, service: string | number, identifier: string | number): 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: `${provider}:${service}:${identifier}` }) 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) const key = identifierKey(provider, service, identifier)
delete _collections.value[key] delete _collections.value[key]
} }
console.debug('[Documents Manager][Store] - Successfully deleted collection:', `${provider}:${service}:${identifier}`) 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
+18 -22
View File
@@ -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'
@@ -101,10 +100,10 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
} }
// 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]) => {
@@ -138,8 +137,7 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
): Promise<Record<string, EntityObject>> { ): Promise<Record<string, EntityObject>> {
transceiving.value = true transceiving.value = true
try { try {
const targets: EntityIdentifier[] = identifiers.map((identifier) => `${provider}:${service}:${collection}:${identifier}` as EntityIdentifier) const response = await entityService.fetch({ provider, service, collection, identifiers })
const response = await entityService.fetch({ targets })
const hydrated: Record<string, EntityObject> = {} const hydrated: Record<string, EntityObject> = {}
Object.entries(response).forEach(([identifier, entityObj]) => { Object.entries(response).forEach(([identifier, entityObj]) => {
@@ -158,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) {
@@ -181,7 +179,7 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
): Promise<EntityObject> { ): Promise<EntityObject> {
transceiving.value = true transceiving.value = true
try { try {
const response = await entityService.create({ target: `${provider}:${service}:${collection}`, 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
@@ -204,7 +202,7 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
): Promise<EntityObject> { ): Promise<EntityObject> {
transceiving.value = true transceiving.value = true
try { try {
const response = await entityService.update({ target: `${provider}:${service}:${collection}:${identifier}`, 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
@@ -223,19 +221,17 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
service: string | number, service: string | number,
collection: string | number, collection: string | number,
identifier: string | number, identifier: string | number,
): Promise<{ success: boolean }> { ): Promise<EntityDeleteResponse> {
transceiving.value = true transceiving.value = true
try { try {
const target: EntityIdentifier = `${provider}:${service}:${collection}:${identifier}` const response = await entityService.delete({ provider, service, collection, identifier })
const response = await entityService.delete({ targets: [target] }) if (response.success) {
const success = response[target]?.disposition === 'deleted'
if (success) {
const key = identifierKey(provider, service, collection, identifier) const key = identifierKey(provider, service, collection, identifier)
delete _entities.value[key] delete _entities.value[key]
} }
console.debug('[Documents Manager][Store] - Successfully deleted entity:', target) console.debug('[Documents Manager][Store] - Successfully deleted entity:', `${provider}:${service}:${collection}:${identifier}`)
return { success } 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
@@ -244,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
@@ -284,7 +280,7 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
): Promise<string | null> { ): Promise<string | null> {
transceiving.value = true transceiving.value = true
try { try {
const response = await entityService.read({ target: `${provider}:${service}:${collection}:${identifier}` }) 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)
@@ -303,7 +299,7 @@ export const useEntitiesStore = defineStore('documentsEntitiesStore', () => {
): Promise<number> { ): Promise<number> {
transceiving.value = true transceiving.value = true
try { try {
const response = await entityService.write({ target: `${provider}:${service}:${collection}:${identifier}`, 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)
+15 -8
View File
@@ -5,8 +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,
ServiceIdentifier,
ListFilter, ListFilter,
ListSort, ListSort,
ListRange, ListRange,
@@ -113,9 +112,13 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
): Promise<CollectionObject[]> { ): Promise<CollectionObject[]> {
error.value = null error.value = null
try { try {
const sources: (ServiceIdentifier | CollectionIdentifier)[] = collectionId === null const sources: SourceSelector = {
? [`${providerId}:${serviceId}`] [providerId]: {
: [`${providerId}:${serviceId}:${collectionId}`] [String(serviceId)]: collectionId === null
? true
: { [String(collectionId)]: true },
},
}
await collectionsStore.list(sources, filter, sort) await collectionsStore.list(sources, filter, sort)
return collectionsStore.collectionsForService(providerId, serviceId) return collectionsStore.collectionsForService(providerId, serviceId)
@@ -135,9 +138,13 @@ export const useNodesStore = defineStore('documentsNodesStore', () => {
): Promise<EntityObject[]> { ): Promise<EntityObject[]> {
error.value = null error.value = null
try { try {
const sources: (ServiceIdentifier | CollectionIdentifier)[] = collectionId === null const sources: SourceSelector = {
? [`${providerId}:${serviceId}`] [providerId]: {
: [`${providerId}:${serviceId}:${collectionId}`] [String(serviceId)]: collectionId === null
? true
: { [String(collectionId)]: true },
},
}
await entitiesStore.list(sources, filter, sort, range) await entitiesStore.list(sources, filter, sort, range)
return entitiesStore.entitiesForCollection(providerId, serviceId, collectionId) return entitiesStore.entitiesForCollection(providerId, serviceId, collectionId)
+8 -8
View File
@@ -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)
+8 -8
View File
@@ -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)
+22 -14
View File
@@ -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,7 +63,9 @@ export interface CollectionListResponse {
* Collection fetch * Collection fetch
*/ */
export interface CollectionFetchRequest { export interface CollectionFetchRequest {
targets: CollectionIdentifier[]; provider: string;
service: string | number;
collection: string | number;
} }
export interface CollectionFetchResponse extends CollectionInterface {} export interface CollectionFetchResponse extends CollectionInterface {}
@@ -72,7 +74,7 @@ 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 {
@@ -89,9 +91,8 @@ export interface CollectionExtantResponse {
export interface CollectionCreateRequest { export interface CollectionCreateRequest {
provider: string; provider: string;
service: string | number; service: string | number;
target?: CollectionIdentifier | null; // Parent collection identifier (absent for root) 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 {}
@@ -100,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;
} }
@@ -110,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 {}
@@ -135,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 {}
+23 -47
View File
@@ -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.
*
* Example usage:
* {
* "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 interface ApiStreamStartResponse { export type SourceSelector = {
type: 'control'; [provider: string]: boolean | ServiceSelector;
status: 'start'; };
version: number;
transaction: string;
total?: number;
}
/** export type ServiceSelector = {
* Stream control end line [service: string]: boolean | CollectionSelector;
*/ };
export interface ApiStreamEndResponse {
type: 'control';
status: 'end';
total: number;
}
/** export type CollectionSelector = {
* Stream error line [collection: string | number]: boolean | EntitySelector;
*/ };
export interface ApiStreamErrorResponse {
type: 'error';
message: string;
}
export interface ApiStreamDataResponse<T = any> { export type EntitySelector = (string | number)[];
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.
* ["default:personal:00000000-0000-0000-0000-000000000000", "default:personal:folder1:file1"].
*/
export type ProviderIdentifier = `${string}`;
export type ServiceIdentifier = `${string}:${string}`;
export type CollectionIdentifier = `${string}:${string}:${string | number}`;
export type EntityIdentifier = `${string}:${string}:${string}:${string | number}`;
/** /**
+40 -31
View File
@@ -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
View File
@@ -4,3 +4,4 @@ export type * from './service';
export type * from './collection'; export type * from './collection';
export type * from './entity'; export type * from './entity';
export type * from './document'; export type * from './document';
export type * from './node';
+37
View File
@@ -0,0 +1,37 @@
/**
* Node types for combined operations
*/
import type { CollectionInterface } from "./collection";
import type { ApiResponse, ListFilterCondition, ListRange, ListSort } from "./common";
import type { EntityInterface } from "./entity";
export interface NodeListRequest {
provider: string;
service: string;
location?: string | null;
recursive?: boolean;
filter?: ListFilterCondition | null;
sort?: ListSort | null;
range?: ListRange | null;
}
export type NodeListResponse = ApiResponse<CollectionInterface | EntityInterface>;
export interface NodeDeltaRequest {
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>;
+6 -6
View File
@@ -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 {
@@ -40,19 +40,19 @@ 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 {}
/** /**
* Provider extant * Provider extant
*/ */
export interface ProviderExtantRequest { export interface ProviderExtantRequest {
targets: ProviderIdentifier[]; sources: SourceSelector;
} }
export interface ProviderExtantResponse { export interface ProviderExtantResponse {
+3 -3
View File
@@ -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 {
-30
View File
@@ -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)
})
})
-31
View File
@@ -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/**',
],
},
},
})
-29
View File
@@ -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));
}
}
-29
View File
@@ -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));
}
}
-17
View File
@@ -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);
}
-41
View File
@@ -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>