From a7ccac98a2db1a126a88c930cf0b00b630f29841 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 21 Dec 2025 09:53:16 -0500 Subject: [PATCH] Initial commit --- .gitignore | 29 + composer.json | 26 + composer.lock | 18 + lib/Controllers/DefaultController.php | 370 +++++++ lib/Manager.php | 685 ++++++++++++ lib/Module.php | 64 ++ package-lock.json | 1456 +++++++++++++++++++++++++ package.json | 24 + src/main.ts | 18 + src/models/collection.ts | 139 +++ src/models/entity.ts | 153 +++ src/models/group.ts | 262 +++++ src/models/index.ts | 11 + src/models/individual.ts | 483 ++++++++ src/models/organization.ts | 359 ++++++ src/models/provider.ts | 61 ++ src/models/service.ts | 81 ++ src/services/collectionService.ts | 84 ++ src/services/entityService.ts | 96 ++ src/services/index.ts | 16 + src/services/providerService.ts | 32 + src/services/serviceService.ts | 48 + src/services/transceive.ts | 50 + src/stores/collectionsStore.ts | 203 ++++ src/stores/entitiesStore.ts | 276 +++++ src/stores/index.ts | 8 + src/stores/providersStore.ts | 62 ++ src/stores/servicesStore.ts | 95 ++ src/types/collection.ts | 161 +++ src/types/common.ts | 107 ++ src/types/entity.ts | 159 +++ src/types/group.ts | 75 ++ src/types/index.ts | 12 + src/types/individual.ts | 176 +++ src/types/organization.ts | 115 ++ src/types/provider.ts | 53 + src/types/service.ts | 211 ++++ src/utils/key-generator.ts | 31 + src/vite-env.d.ts | 1 + tsconfig.app.json | 19 + tsconfig.json | 7 + tsconfig.node.json | 25 + vite.config.ts | 30 + 43 files changed, 6391 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 lib/Controllers/DefaultController.php create mode 100644 lib/Manager.php create mode 100644 lib/Module.php create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/main.ts create mode 100644 src/models/collection.ts create mode 100644 src/models/entity.ts create mode 100644 src/models/group.ts create mode 100644 src/models/index.ts create mode 100644 src/models/individual.ts create mode 100644 src/models/organization.ts create mode 100644 src/models/provider.ts create mode 100644 src/models/service.ts create mode 100644 src/services/collectionService.ts create mode 100644 src/services/entityService.ts create mode 100644 src/services/index.ts create mode 100644 src/services/providerService.ts create mode 100644 src/services/serviceService.ts create mode 100644 src/services/transceive.ts create mode 100644 src/stores/collectionsStore.ts create mode 100644 src/stores/entitiesStore.ts create mode 100644 src/stores/index.ts create mode 100644 src/stores/providersStore.ts create mode 100644 src/stores/servicesStore.ts create mode 100644 src/types/collection.ts create mode 100644 src/types/common.ts create mode 100644 src/types/entity.ts create mode 100644 src/types/group.ts create mode 100644 src/types/index.ts create mode 100644 src/types/individual.ts create mode 100644 src/types/organization.ts create mode 100644 src/types/provider.ts create mode 100644 src/types/service.ts create mode 100644 src/utils/key-generator.ts create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..812e4cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Frontend development +node_modules/ +*.local +.env.local +.env.*.local +.cache/ +.vite/ +.temp/ +.tmp/ + +# Frontend build +/static/ + +# Backend development +/lib/vendor/ +coverage/ +phpunit.xml.cache +.phpunit.result.cache +.php-cs-fixer.cache +.phpstan.cache +.phpactor/ + +# Editors +.DS_Store +.vscode/ +.idea/ + +# Logs +*.log diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a198d5c --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "ktxm/people-manager", + "type": "project", + "authors": [ + { + "name": "Sebastian Krupinski", + "email": "krupinski01@gmail.com" + } + ], + "config": { + "optimize-autoloader": true, + "platform": { + "php": "8.2" + }, + "autoloader-suffix": "PeopleManager", + "vendor-dir": "lib/vendor" + }, + "require": { + "php": ">=8.2 <=8.5" + }, + "autoload": { + "psr-4": { + "KTXM\\PeopleManager\\": "lib/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..45d98d3 --- /dev/null +++ b/composer.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "b932f96922722e0403842d3975e00a91", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/lib/Controllers/DefaultController.php b/lib/Controllers/DefaultController.php new file mode 100644 index 0000000..458d317 --- /dev/null +++ b/lib/Controllers/DefaultController.php @@ -0,0 +1,370 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace KTXM\PeopleManager\Controllers; + +use InvalidArgumentException; +use KTXC\Http\Response\JsonResponse; +use KTXC\SessionIdentity; +use KTXC\SessionTenant; +use KTXF\Controller\ControllerAbstract; +use KTXF\People\Collection\ICollectionBase; +use KTXF\Resource\Selector\SourceSelector; +use KTXF\Routing\Attributes\AuthenticatedRoute; +use KTXM\PeopleManager\Manager; + +class DefaultController extends ControllerAbstract { + + public function __construct( + private readonly SessionTenant $tenantIdentity, + private readonly SessionIdentity $userIdentity, + private Manager $peopleManager, + ) {} + + /** + * Retrieve list of available providers + * + * @return JsonResponse + */ + #[AuthenticatedRoute('/v1', name: 'peoplemanager.v1', methods: ['POST'])] + + public function index(int $version, string $transaction, string $operation, array $data = [], string|null $user = null): JsonResponse { + + // authorize request + $tenantId = $this->tenantIdentity->identifier(); + $userId = $this->userIdentity->identifier(); + + try { + $data = $this->process($tenantId, $userId, $operation, $data); + return new JsonResponse([ + 'version' => $version, + 'transaction' => $transaction, + 'operation' => $operation, + 'status' => 'success', + 'data' => $data, + ], JsonResponse::HTTP_OK); + } catch (\Throwable $t) { + return new JsonResponse([ + 'version' => $version, + 'transaction' => $transaction, + 'operation' => $operation, + 'status' => 'error', + 'data' => [ + 'code' => $t->getCode(), + 'message' => $t->getMessage(), + ] + ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + } + + private function process(string $tenantId, string $userId, string $operation, array $data): mixed { + + return match ($operation) { + 'provider.list' => $this->providerList($tenantId, $userId, $data), + 'provider.extant' => $this->providerExtant($tenantId, $userId, $data), + 'service.list' => $this->serviceList($tenantId, $userId, $data), + 'service.extant' => $this->serviceExtant($tenantId, $userId, $data), + 'service.fetch' => $this->serviceFetch($tenantId, $userId, $data), + 'collection.list' => $this->collectionList($tenantId, $userId, $data), + 'collection.extant' => $this->collectionExtant($tenantId, $userId, $data), + 'collection.fetch' => $this->collectionFetch($tenantId, $userId, $data), + 'collection.create' => $this->collectionCreate($tenantId, $userId, $data), + 'collection.modify' => $this->collectionModify($tenantId, $userId, $data), + 'collection.destroy' => $this->collectionDestroy($tenantId, $userId, $data), + 'entity.list' => $this->entityList($tenantId, $userId, $data), + 'entity.delta' => $this->entityDelta($tenantId, $userId, $data), + 'entity.extant' => $this->entityExtant($tenantId, $userId, $data), + 'entity.fetch' => $this->entityFetch($tenantId, $userId, $data), + 'entity.create' => $this->entityCreate($tenantId, $userId, $data), + 'entity.modify' => $this->entityModify($tenantId, $userId, $data), + 'entity.destroy' => $this->entityDestroy($tenantId, $userId, $data), + default => throw new InvalidArgumentException("Invalid operation: $operation"), + }; + + } + + // ==================== Provider Operations ==================== + + private function providerList(string $tenantId, string $userId, array $data): mixed { + + $sources = null; + if (isset($data['sources']) && is_array($data['sources'])) { + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + } + + return $this->peopleManager->providerList($tenantId, $userId, $sources); + + } + + private function providerExtant(string $tenantId, string $userId, array $data): mixed { + + if (!isset($data['sources']) || !is_array($data['sources'])) { + throw new InvalidArgumentException('Invalid sources selector provided'); + } + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + + return $this->peopleManager->providerExtant($tenantId, $userId, $sources); + + } + + // ==================== Service Operations ===================== + + private function serviceList(string $tenantId, string $userId, array $data): mixed { + + $sources = null; + if (isset($data['sources']) && is_array($data['sources'])) { + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + } + + return $this->peopleManager->serviceList($tenantId, $userId, $sources); + + } + + private function serviceExtant(string $tenantId, string $userId, array $data): mixed { + + if (!isset($data['sources']) || !is_array($data['sources'])) { + throw new InvalidArgumentException('Invalid sources selector provided'); + } + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + + return $this->peopleManager->serviceExtant($tenantId, $userId, $sources); + + } + + private function serviceFetch(string $tenantId, string $userId, array $data): mixed { + + if (!isset($data['provider']) || !is_string($data['provider'])) { + throw new InvalidArgumentException('Invalid provider identifier provided'); + } + if (!isset($data['identifier']) || !is_string($data['identifier'])) { + throw new InvalidArgumentException('Invalid service identifier provided'); + } + + return $this->peopleManager->serviceFetch($tenantId, $userId, $data['provider'], $data['identifier']); + + } + + private function collectionList(string $tenantId, string $userId, array $data): mixed { + + $sources = null; + if (isset($data['sources']) && is_array($data['sources'])) { + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + } + $filter = $data['filter'] ?? null; + $sort = $data['sort'] ?? null; + // retrieve collections + return $this->peopleManager->collectionList($tenantId, $userId, $sources, $filter, $sort); + + } + + private function collectionExtant(string $tenantId, string $userId, array $data = []): mixed { + + if (!isset($data['sources']) || !is_array($data['sources'])) { + throw new InvalidArgumentException('Invalid sources selector provided'); + } + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + // retrieve collection status + return $this->peopleManager->collectionExtant($tenantId, $userId, $sources); + + } + + private function collectionFetch(string $tenantId, string $userId, array $data = []): mixed { + + if (!isset($data['provider']) || !is_string($data['provider'])) { + throw new InvalidArgumentException('Invalid provider identifier provided'); + } + if (!isset($data['service']) || !is_string($data['service'])) { + throw new InvalidArgumentException('Invalid service identifier provided'); + } + if (!isset($data['identifier'])) { + throw new InvalidArgumentException('Invalid collection identifier provided'); + } + // retrieve collection + return $this->peopleManager->collectionFetch($tenantId, $userId, $data['provider'], $data['service'], $data['identifier']); + + } + + private function collectionCreate(string $tenantId, string $userId, array $data = []): mixed { + + if (!isset($data['provider']) || !is_string($data['provider'])) { + throw new InvalidArgumentException('Invalid provider identifier provided'); + } + if (!isset($data['service']) || !is_string($data['service'])) { + throw new InvalidArgumentException('Invalid service identifier provided'); + } + if (!isset($data['data'])) { + throw new InvalidArgumentException('Invalid collection data provided'); + } + $options = $data['options'] ?? []; + // create collection + return $this->peopleManager->collectionCreate($tenantId, $userId, $data['provider'], $data['service'], $data['data'], $options); + + } + + private function collectionModify(string $tenantId, string $userId, array $data = []): mixed { + + if (!isset($data['provider']) || !is_string($data['provider'])) { + throw new InvalidArgumentException('Invalid provider identifier provided'); + } + if (!isset($data['service']) || !is_string($data['service'])) { + throw new InvalidArgumentException('Invalid service identifier provided'); + } + if (!isset($data['identifier'])) { + throw new InvalidArgumentException('Invalid collection identifier provided'); + } + if (!isset($data['data'])) { + throw new InvalidArgumentException('Invalid collection data provided'); + } + // modify collection + return $this->peopleManager->collectionModify($tenantId, $userId, $data['provider'], $data['service'], $data['identifier'], $data['data']); + + } + + private function collectionDestroy(string $tenantId, string $userId, array $data = []): mixed { + + if (!isset($data['provider']) || !is_string($data['provider'])) { + throw new InvalidArgumentException('Invalid provider identifier provided'); + } + if (!isset($data['service']) || !is_string($data['service'])) { + throw new InvalidArgumentException('Invalid service identifier provided'); + } + if (!isset($data['identifier'])) { + throw new InvalidArgumentException('Invalid collection identifier provided'); + } + // destroy collection + return ['success' => $this->peopleManager->collectionDestroy($tenantId, $userId, $data['provider'], $data['service'], $data['identifier'])]; + + } + + private function entityList(string $tenantId, string $userId, array $data = []): mixed { + + $sources = null; + if (isset($data['sources']) && is_array($data['sources'])) { + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + } + $filter = $data['filter'] ?? null; + $sort = $data['sort'] ?? null; + $range = $data['range'] ?? null; + // retrieve entities + return $this->peopleManager->entityList($tenantId, $userId, $sources, $filter, $sort, $range); + + } + + private function entityDelta(string $tenantId, string $userId, array $data = []): mixed { + + if (!isset($data['sources']) || !is_array($data['sources'])) { + throw new InvalidArgumentException('Invalid sources selector provided'); + } + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + // retrieve entity delta + return $this->peopleManager->entityDelta($tenantId, $userId, $sources); + + } + + private function entityExtant(string $tenantId, string $userId, array $data = []): mixed { + + if (!isset($data['sources']) || !is_array($data['sources'])) { + throw new InvalidArgumentException('Invalid sources selector provided'); + } + $sources = new SourceSelector(); + $sources->jsonDeserialize($data['sources']); + // retrieve entity status + return $this->peopleManager->entityExtant($tenantId, $userId, $sources); + + } + + private function entityFetch(string $tenantId, string $userId, array $data = []): mixed { + + if (!isset($data['provider']) || !is_string($data['provider'])) { + throw new InvalidArgumentException('Invalid provider identifier provided'); + } + if (!isset($data['service']) || !is_string($data['service'])) { + throw new InvalidArgumentException('Invalid service identifier provided'); + } + if (!isset($data['collection'])) { + throw new InvalidArgumentException('Invalid collection identifier provided'); + } + if (!isset($data['identifiers']) || !is_array($data['identifiers'])) { + throw new InvalidArgumentException('Invalid entity identifiers provided'); + } + // retrieve entities + return $this->peopleManager->entityFetch($tenantId, $userId, $data['provider'], $data['service'], $data['collection'], $data['identifiers']); + + } + + private function entityCreate(string $tenantId, string $userId, array $data = []): mixed { + + if (!isset($data['provider']) || !is_string($data['provider'])) { + throw new InvalidArgumentException('Invalid provider identifier provided'); + } + if (!isset($data['service']) || !is_string($data['service'])) { + throw new InvalidArgumentException('Invalid service identifier provided'); + } + if (!isset($data['collection'])) { + throw new InvalidArgumentException('Invalid collection identifier provided'); + } + if (!isset($data['data']) || !is_array($data['data'])) { + throw new InvalidArgumentException('Invalid entity data provided'); + } + $options = $data['options'] ?? []; + // create entity + return $this->peopleManager->entityCreate($tenantId, $userId, $data['provider'], $data['service'], $data['collection'], $data['data'], $options); + + } + + private function entityModify(string $tenantId, string $userId, array $data = []): mixed { + + if (!isset($data['provider']) || !is_string($data['provider'])) { + throw new InvalidArgumentException('Invalid provider identifier provided'); + } + if (!isset($data['service']) || !is_string($data['service'])) { + throw new InvalidArgumentException('Invalid service identifier provided'); + } + if (!isset($data['collection'])) { + throw new InvalidArgumentException('Invalid collection identifier provided'); + } + if (!isset($data['identifier'])) { + throw new InvalidArgumentException('Invalid entity identifier provided'); + } + if (!isset($data['data']) || !is_array($data['data'])) { + throw new InvalidArgumentException('Invalid entity data provided'); + } + // modify entity + return $this->peopleManager->entityModify($tenantId, $userId, $data['provider'], $data['service'], $data['collection'], $data['identifier'], $data['data']); + + } + + private function entityDestroy(string $tenantId, string $userId, array $data = []): mixed { + + if (!isset($data['provider']) || !is_string($data['provider'])) { + throw new InvalidArgumentException('Invalid provider identifier provided'); + } + if (!isset($data['service']) || !is_string($data['service'])) { + throw new InvalidArgumentException('Invalid service identifier provided'); + } + if (!isset($data['collection'])) { + throw new InvalidArgumentException('Invalid collection identifier provided'); + } + if (!isset($data['identifier'])) { + throw new InvalidArgumentException('Invalid entity identifier provided'); + } + // destroy entity + return ['success' => $this->peopleManager->entityDestroy($tenantId, $userId, $data['provider'], $data['service'], $data['collection'], $data['identifier'])]; + + } + +} diff --git a/lib/Manager.php b/lib/Manager.php new file mode 100644 index 0000000..cb46c32 --- /dev/null +++ b/lib/Manager.php @@ -0,0 +1,685 @@ + collection of available providers e.g. ['provider1' => IProvider, 'provider2' => IProvider] + */ + public function providerList(string $tenantId, string $userId, ?SourceSelector $sources = null): array { + // determine filter from sources + $filter = ($sources !== null && $sources->identifiers() !== []) ? $sources->identifiers() : null; + // retrieve providers from provider manager + return $this->providerManager->providers(ProviderInterface::TYPE_PEOPLE, $filter); + } + + /** + * Confirm which providers are available + * + * @param SourceSelector|null $sources collection of provider identifiers to confirm + * + * @return array collection of providers and their availability status e.g. ['provider1' => true, 'provider2' => false] + */ + public function providerExtant(string $tenantId, string $userId, SourceSelector $sources): array { + // determine which providers are available + $providersResolved = $this->providerList($tenantId, $userId, $sources); + $providersAvailable = array_keys($providersResolved); + $providersUnavailable = array_diff($sources->identifiers(), $providersAvailable); + // construct response data + $responseData = array_merge( + array_fill_keys($providersAvailable, true), + array_fill_keys($providersUnavailable, false) + ); + return $responseData; + } + + /** + * Retrieve specific provider for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $provider provider identifier + * + * @return IProviderBase + * @throws InvalidArgumentException + */ + public function providerFetch(string $tenantId, string $userId, string $provider): IProviderBase { + // retrieve provider + $providers = $this->providerList($tenantId, $userId, new SourceSelector([$provider => true])); + if (!isset($providers[$provider])) { + throw new InvalidArgumentException("Provider '$provider' not found"); + } + return $providers[$provider]; + } + + /** + * Retrieve available services for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector|null $sources list of provider and service identifiers + * + * @return array> collections of available services e.g. ['provider1' => ['service1' => IServiceBase], 'provider2' => ['service2' => IServiceBase]] + */ + public function serviceList(string $tenantId, string $userId, ?SourceSelector $sources = null): array { + // retrieve providers + $providers = $this->providerList($tenantId, $userId, $sources); + // retrieve services for each provider + $responseData = []; + foreach ($providers as $provider) { + $serviceFilter = $sources[$provider->id()] instanceof ServiceSelector ? $sources[$provider->id()]->identifiers() : []; + $services = $provider->serviceList($tenantId, $userId, $serviceFilter); + $responseData[$provider->id()] = $services; + } + return $responseData; + } + + /** + * Confirm which services are available + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector|null $sources collection of provider and service identifiers to confirm + * + * @return array collection of providers and their availability status e.g. ['provider1' => ['service1' => false], 'provider2' => ['service2' => true, 'service3' => true]] + */ + public function serviceExtant(string $tenantId, string $userId, SourceSelector $sources): array { + // retrieve providers + $providers = $this->providerList($tenantId, $userId, $sources); + $providersRequested = $sources->identifiers(); + $providersUnavailable = array_diff($providersRequested, array_keys($providers)); + + // initialize response with unavailable providers + $responseData = array_fill_keys($providersUnavailable, false); + + // retrieve services for each available provider + foreach ($providers as $provider) { + $serviceSelector = $sources[$provider->id()]; + $serviceAvailability = $provider->serviceExtant($tenantId, $userId, ...$serviceSelector->identifiers()); + $responseData[$provider->id()] = $serviceAvailability; + } + return $responseData; + } + + /** + * Retrieve service for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * + * @return IServiceBase + * @throws InvalidArgumentException + */ + public function serviceFetch(string $tenantId, string $userId, string $providerId, string|int $serviceId): IServiceBase { + // retrieve provider and service + $service = $this->providerFetch($tenantId, $userId, $providerId)->serviceFetch($tenantId, $userId, $serviceId); + if ($service === null) { + throw new InvalidArgumentException("Service '$serviceId' not found for provider '$providerId'"); + } + // retrieve services + return $service; + } + + /** + * Retrieve available collections for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector $sources list of provider and service identifiers + * + * @return array> collections of available services e.g. ['provider1' => ['service1' => [ICollectionBase], 'service2' => [ICollectionBase]]] + */ + public function collectionList(string $tenantId, string $userId, ?SourceSelector $sources = null, ?array $filter = null, ?array $sort = null): array { + // confirm that sources are provided + if ($sources === null) { + $sources = new SourceSelector([]); + } + // retrieve providers + $providers = $this->providerList($tenantId, $userId, $sources); + // retrieve services for each provider + $responseData = []; + foreach ($providers as $provider) { + $serviceFilter = $sources[$provider->id()] instanceof ServiceSelector ? $sources[$provider->id()]->identifiers() : []; + $services = $provider->serviceList($tenantId, $userId, $serviceFilter); + // retrieve collections for each service + foreach ($services as $service) { + // construct filter for collections + $collectionFilter = null; + if ($filter !== null && $filter !== []) { + $collectionFilter = $service->collectionListFilter(); + foreach ($filter as $attribute => $value) { + $collectionFilter->condition($attribute, $value); + } + } + // construct sort for collections + $collectionSort = null; + if ($sort !== null && $sort !== []) { + $collectionSort = $service->collectionListSort(); + foreach ($sort as $attribute => $direction) { + $collectionSort->condition($attribute, $direction); + } + } + $collections = $service->collectionList($collectionFilter, $collectionSort); + if ($collections !== []) { + $responseData[$provider->id()][$service->id()] = $collections; + } + } + } + return $responseData; + } + + /** + * Confirm which collections are available + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector $sources collection of provider and service identifiers to confirm + * + * @return array collection of providers and their availability status e.g. ['provider1' => ['service1' => ['collection1' => true, 'collection2' => false]]]] + */ + public function collectionExtant(string $tenantId, string $userId, SourceSelector $sources): array { + // retrieve available providers + $providers = $this->providerList($tenantId, $userId, $sources); + $providersRequested = $sources->identifiers(); + $providersUnavailable = array_diff($providersRequested, array_keys($providers)); + + // initialize response with unavailable providers + $responseData = array_fill_keys($providersUnavailable, false); + + // check services and collections for each available provider + foreach ($providers as $provider) { + $serviceSelector = $sources[$provider->id()]; + $servicesRequested = $serviceSelector->identifiers(); + $servicesAvailable = $provider->serviceList($tenantId, $userId, $servicesRequested); + $servicesUnavailable = array_diff($servicesRequested, array_keys($servicesAvailable)); + + // mark unavailable services as false + if ($servicesUnavailable !== []) { + $responseData[$provider->id()] = array_fill_keys($servicesUnavailable, false); + } + + // confirm collections for each available service + foreach ($servicesAvailable as $service) { + $collectionSelector = $serviceSelector[$service->id()]; + $collectionsRequested = $collectionSelector->identifiers(); + + if ($collectionsRequested === []) { + continue; + } + + // check each requested collection + foreach ($collectionsRequested as $collectionId) { + $responseData[$provider->id()][$service->id()][$collectionId] = $service->collectionExtant((string)$collectionId); + } + } + } + return $responseData; + } + + /** + * Retrieve collection for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * + * @return ICollectionBase + * @throws InvalidArgumentException + */ + public function collectionFetch(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId): ICollectionBase { + // retrieve services + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + // retrieve collection + return $service->collectionFetch($collectionId); + } + + /** + * Create a new collection for a specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param ICollectionBase|array $collection collection to create + * @param array $options additional options for creation + * + * @return ICollectionBase + * @throws InvalidArgumentException + */ + public function collectionCreate(string $tenantId, string $userId, string $providerId, string|int $serviceId, ICollectionBase|array $collection, array $options = []): ICollectionBase { + // retrieve service + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + + // Check if service supports collection creation + if (!($service instanceof IServiceCollectionMutable)) { + throw new InvalidArgumentException("Service does not support collection mutations"); + } + if (!$service->capable(IServiceCollectionMutable::CAPABILITY_COLLECTION_CREATE)) { + throw new InvalidArgumentException("Service is not capable of creating collections"); + } + + if (is_array($collection)) { + $collection = $service->collectionFresh()->jsonDeserialize($collection); + } + + // Create collection (location is empty string for root) + return $service->collectionCreate('', $collection, $options); + } + + /** + * Modify an existing collection for a specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * @param ICollectionBase $collectionData collection with modifications + * + * @return ICollectionBase + * @throws InvalidArgumentException + */ + public function collectionModify(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, ICollectionBase|array $collectionData): ICollectionBase { + // retrieve service + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + + // Check if service supports collection modification + if (!($service instanceof IServiceCollectionMutable)) { + throw new InvalidArgumentException("Service does not support collection mutations"); + } + if (!$service->capable(IServiceCollectionMutable::CAPABILITY_COLLECTION_MODIFY)) { + throw new InvalidArgumentException("Service is not capable of modifying collections"); + } + + if (is_array($collectionData)) { + $collectionData = $service->collectionFresh()->jsonDeserialize($collectionData); + } + + // Modify collection + return $service->collectionModify($collectionId, $collectionData); + } + /** + * Delete a collection for a specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * + * @return bool + * @throws InvalidArgumentException + */ + public function collectionDestroy(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId): bool { + // retrieve service + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + + // Check if service supports collection destruction + if (!($service instanceof IServiceCollectionMutable)) { + throw new InvalidArgumentException("Service does not support collection mutations"); + } + if (!$service->capable(IServiceCollectionMutable::CAPABILITY_COLLECTION_DESTROY)) { + throw new InvalidArgumentException("Service is not capable of destroying collections"); + } + + // Destroy collection and cast result to bool + return (bool)$service->collectionDestroy($collectionId); + } + + + /** + * Retrieve available entities for specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector $sources list of provider and service identifiers + * + * @return array> collections of store enteties e.g. ['provider1' => ['service1' => [ICollectionBase], 'service2' => [ICollectionBase]]] + */ + public function entityList(string $tenantId, string $userId, ?SourceSelector $sources = null, ?array $filter = null, ?array $sort = null, ?array $range = null): array { + // confirm that sources are provided + if ($sources === null) { + $sources = new SourceSelector([]); + } + // retrieve providers + $providers = $this->providerList($tenantId, $userId, $sources); + // retrieve services for each provider + $responseData = []; + foreach ($providers as $provider) { + // retrieve services for each provider + $serviceSelector = $sources[$provider->id()]; + $servicesSelected = $provider->serviceList($tenantId,$userId, $serviceSelector->identifiers()); + foreach ($servicesSelected as $service) { + // retrieve collections for each service + $collectionSelector = $serviceSelector[$service->id()]; + $collectionSelected = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; + if ($collectionSelected === []) { + $collections = $service->collectionList(); + $collectionSelected = array_map( + fn($collection) => $collection->id(), + $collections + ); + } + if ($collectionSelected === []) { + continue; + } + // construct filter for entities + $entityFilter = null; + if ($filter !== null && $filter !== []) { + $entityFilter = $service->entityListFilter(); + foreach ($filter as $attribute => $value) { + $entityFilter->condition($attribute, $value); + } + } + // construct sort for entities + $entitySort = null; + if ($sort !== null && $sort !== []) { + $entitySort = $service->entityListSort(); + foreach ($sort as $attribute => $direction) { + $entitySort->condition($attribute, $direction); + } + } + // construct range for entities + $entityRange = null; + if ($range !== null && $range !== [] && isset($range['type'])) { + $entityRange = $service->entityListRange(RangeType::from($range['type'])); + // Cast to IRangeTally if the range type is TALLY + if ($entityRange->type() === RangeType::TALLY) { + /** @var IRangeTally $entityRange */ + if (isset($range['anchor'])) { + $entityRange->setAnchor(RangeAnchorType::from($range['anchor'])); + } + if (isset($range['position'])) { + $entityRange->setPosition($range['position']); + } + if (isset($range['tally'])) { + $entityRange->setTally($range['tally']); + } + } + } + // retrieve entities for each collection + foreach ($collectionSelected as $collectionId) { + $entities = $service->entityList($collectionId, $entityFilter, $entitySort, $entityRange, null); + // skip collections with no entities + if ($entities === []) { + continue; + } + $responseData[$provider->id()][$service->id()][$collectionId] = $entities; + } + } + } + return $responseData; + } + + /** + * Confirm which entities are available + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param array $sources collection of provider and service identifiers to confirm + * + * @return array collection of providers and their availability status + */ + public function entityDelta(string $tenantId, string $userId, SourceSelector $sources): array { + // confirm that sources are provided + if ($sources === null) { + $sources = new SourceSelector([]); + } + // retrieve providers + $providers = $this->providerList($tenantId, $userId, $sources); + $providersRequested = $sources->identifiers(); + $providersUnavailable = array_diff($providersRequested, array_keys($providers)); + // initialize response with unavailable providers + $responseData = array_fill_keys($providersUnavailable, false); + // iterate through available providers + foreach ($providers as $provider) { + $serviceSelector = $sources[$provider->id()]; + $servicesRequested = $serviceSelector instanceof ServiceSelector ? $serviceSelector->identifiers() : []; + $services = $provider->serviceList($tenantId, $userId, $servicesRequested); + $servicesUnavailable = array_diff($servicesRequested, array_keys($services)); + if ($servicesUnavailable !== []) { + $responseData[$provider->id()] = array_fill_keys($servicesUnavailable, false); + } + // iterate through available services + foreach ($services as $service) { + $collectionSelector = $serviceSelector[$service->id()]; + $collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; + if ($collectionsRequested === []) { + $responseData[$provider->id()][$service->id()] = false; + continue; + } + foreach ($collectionsRequested as $collection) { + $entitySelector = $collectionSelector[$collection] ?? null; + $responseData[$provider->id()][$service->id()][$collection] = $service->entityDelta($collection, $entitySelector); + } + } + } + return $responseData; + } + + /** + * Confirm which entities are available + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param SourceSelector $sources collection of provider and service identifiers to confirm + * + * @return array collection of providers and their availability status e.g. ['provider1' => ['service1' => ['collection1' => ['entity1' => true, 'entity2' => false]]]] + */ + public function entityExtant(string $tenantId, string $userId, SourceSelector $sources): array { + // confirm that sources are provided + if ($sources === null) { + $sources = new SourceSelector([]); + } + // retrieve available providers + $providers = $this->providerList($tenantId, $userId, $sources); + $providersRequested = $sources->identifiers(); + $providersUnavailable = array_diff($providersRequested, array_keys($providers)); + + // initialize response with unavailable providers + $responseData = array_fill_keys($providersUnavailable, false); + + // check services, collections, and entities for each available provider + foreach ($providers as $provider) { + $serviceSelector = $sources[$provider->id()]; + $servicesRequested = $serviceSelector->identifiers(); + $servicesAvailable = $provider->serviceList($tenantId, $userId, $servicesRequested); + $servicesUnavailable = array_diff($servicesRequested, array_keys($servicesAvailable)); + + // mark unavailable services as false + if ($servicesUnavailable !== []) { + $responseData[$provider->id()] = array_fill_keys($servicesUnavailable, false); + } + + // check collections and entities for each available service + foreach ($servicesAvailable as $service) { + $collectionSelector = $serviceSelector[$service->id()]; + $collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : []; + + if ($collectionsRequested === []) { + continue; + } + + // check entities for each requested collection + foreach ($collectionsRequested as $collectionId) { + // first check if collection exists + $collectionExists = $service->collectionExtant((string)$collectionId); + + if (!$collectionExists) { + // collection doesn't exist, mark as false + $responseData[$provider->id()][$service->id()][$collectionId] = false; + continue; + } + + // extract entity identifiers from collection selector + $entitySelector = $collectionSelector[$collectionId]; + + // handle both array of entity IDs and boolean true (meaning check if collection exists) + if ($entitySelector instanceof EntitySelector) { + // check specific entities within the collection + $responseData[$provider->id()][$service->id()][$collectionId] = $service->entityExtant($collectionId, ...$entitySelector->identifiers()); + } elseif ($entitySelector === true) { + // just checking if collection exists (already confirmed above) + $responseData[$provider->id()][$service->id()][$collectionId] = true; + } + } + } + } + return $responseData; + } + + /** + * Retrieve entity for specific user and collection + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * @param array $identifiers entity identifiers + * + * @return array + */ + public function entityFetch(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, array $identifiers): array { + // retrieve services + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + // retrieve collection + return $service->entityFetch($collectionId, ...$identifiers); + } + + /** + * Create a new entity in a specific collection for a specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * @param IEntityBase|array $entity entity to create + * @param array $options additional options for creation + * + * @return IEntityBase + * @throws InvalidArgumentException + */ + public function entityCreate(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, IEntityBase|array $entity, array $options = []): IEntityBase { + // retrieve service + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + + // Check if service supports entity creation + if (!($service instanceof IServiceEntityMutable)) { + throw new InvalidArgumentException("Service does not support entity mutations"); + } + if (!$service->capable(IServiceEntityMutable::CAPABILITY_ENTITY_CREATE)) { + throw new InvalidArgumentException("Service is not capable of creating entities"); + } + + if (is_array($entity)) { + $entityInstance = $service->entityFresh(); + $entityInstance->jsonDeserialize($entity); + } else { + $entityInstance = $entity; + } + + // Create entity + return $service->entityCreate($collectionId, $entityInstance, $options); + } + + /** + * Modify an existing entity in a collection for a specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * @param string|int $identifier entity identifier + * @param IEntityBase|array $entity entity with modifications + * + * @return IEntityBase + * @throws InvalidArgumentException + */ + public function entityModify(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, string|int $identifier, IEntityBase|array $entity): IEntityBase { + // retrieve service + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + + // Check if service supports entity modification + if (!($service instanceof IServiceEntityMutable)) { + throw new InvalidArgumentException("Service does not support entity mutations"); + } + if (!$service->capable(IServiceEntityMutable::CAPABILITY_ENTITY_MODIFY)) { + throw new InvalidArgumentException("Service is not capable of modifying entities"); + } + + if (is_array($entity)) { + $entityInstance = $service->entityFresh(); + $entityInstance->jsonDeserialize($entity); + } else { + $entityInstance = $entity; + } + + // Modify entity + return $service->entityModify($collectionId, $identifier, $entityInstance); + } + + /** + * Delete an entity from a collection for a specific user + * + * @param string $tenantId tenant identifier + * @param string $userId user identifier + * @param string $providerId provider identifier + * @param string|int $serviceId service identifier + * @param string|int $collectionId collection identifier + * @param string|int $identifier entity identifier + * + * @return bool + * @throws InvalidArgumentException + */ + public function entityDestroy(string $tenantId, string $userId, string $providerId, string|int $serviceId, string|int $collectionId, string|int $identifier): bool { + // retrieve service + $service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId); + + // Check if service supports entity destruction + if (!($service instanceof IServiceEntityMutable)) { + throw new InvalidArgumentException("Service does not support entity mutations"); + } + if (!$service->capable(IServiceEntityMutable::CAPABILITY_ENTITY_DESTROY)) { + throw new InvalidArgumentException("Service is not capable of destroying entities"); + } + + // Destroy entity and cast result to bool + return (bool)$service->entityDestroy($collectionId, $identifier); + } +} \ No newline at end of file diff --git a/lib/Module.php b/lib/Module.php new file mode 100644 index 0000000..30483fb --- /dev/null +++ b/lib/Module.php @@ -0,0 +1,64 @@ + [ + 'label' => 'Access People Manager', + 'description' => 'View and access the people manager module', + 'group' => 'People Management' + ], + ]; + } + + public function registerBI(): array { + return [ + 'handle' => $this->handle(), + 'namespace' => 'PeopleManager', + 'version' => $this->version(), + 'label' => $this->label(), + 'author' => $this->author(), + 'description' => $this->description(), + 'boot' => 'static/module.mjs', + ]; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..aa7ee91 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1456 @@ +{ + "name": "people_manager", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "people_manager", + "version": "1.0.0", + "license": "AGPL-3.0-or-later", + "dependencies": { + "pinia": "^2.3.1" + }, + "devDependencies": { + "@vue/tsconfig": "^0.7.0", + "typescript": "~5.8.3", + "vite": "^7.1.2", + "vue-tsc": "^3.0.5" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/language-core": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", + "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.23" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", + "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz", + "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz", + "integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "@vue/shared": "3.5.22", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz", + "integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz", + "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.4", + "@vue/compiler-core": "3.5.22", + "@vue/compiler-dom": "3.5.22", + "@vue/compiler-ssr": "3.5.22", + "@vue/shared": "3.5.22", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.19", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz", + "integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.1.tgz", + "integrity": "sha512-qjMY3Q+hUCjdH+jLrQapqgpsJ0rd/2mAY02lZoHG3VFJZZZKLjAlV+Oo9QmWIT4jh8+Rx8RUGUi++d7T9Wb6Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz", + "integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz", + "integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.22", + "@vue/shared": "3.5.22" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz", + "integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.22", + "@vue/runtime-core": "3.5.22", + "@vue/shared": "3.5.22", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz", + "integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.22", + "@vue/shared": "3.5.22" + }, + "peerDependencies": { + "vue": "3.5.22" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz", + "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz", + "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.0.0.tgz", + "integrity": "sha512-JHoRJf18Y6HN4/KZALr3iU+0vW9LKG+8FMThQlbn4+gv8utsLIkwpomjElGPccGeNwh0FI2HN6BLnyFLo6OyLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", + "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.22", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz", + "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.22", + "@vue/compiler-sfc": "3.5.22", + "@vue/runtime-dom": "3.5.22", + "@vue/server-renderer": "3.5.22", + "@vue/shared": "3.5.22" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.1.tgz", + "integrity": "sha512-fyixKxFniOVgn+L/4+g8zCG6dflLLt01Agz9jl3TO45Bgk87NZJRmJVPsiK+ouq3LB91jJCbOV+pDkzYTxbI7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.23", + "@vue/language-core": "3.1.1" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ae2d8c7 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "people_manager", + "description": "Ktrix People Manager Module - Backend Store", + "version": "1.0.0", + "private": true, + "license": "AGPL-3.0-or-later", + "author": "Ktrix", + "type": "module", + "scripts": { + "build": "vite build --mode production --config vite.config.ts", + "dev": "vite build --mode development --config vite.config.ts", + "watch": "vite build --mode development --watch --config vite.config.ts", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "pinia": "^2.3.1" + }, + "devDependencies": { + "@vue/tsconfig": "^0.7.0", + "typescript": "~5.8.3", + "vite": "^7.1.2", + "vue-tsc": "^3.0.5" + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..3b9b34e --- /dev/null +++ b/src/main.ts @@ -0,0 +1,18 @@ +import { useCollectionsStore } from '@/stores/collectionsStore' +import { useEntitiesStore } from '@/stores/entitiesStore' +import { useProvidersStore } from '@/stores/providersStore' +import { useServicesStore } from '@/stores/servicesStore' + +/** + * People Manager Module Boot Script + * + * This script is executed when the people_manager module is loaded. + * It initializes the peopleStore which manages contacts and address books state. + */ + +console.log('[PeopleManager] Booting People Manager module...') + +console.log('[PeopleManager] People Manager module booted successfully') + +// Export store for external use if needed +export { useCollectionsStore, useEntitiesStore, useProvidersStore, useServicesStore } diff --git a/src/models/collection.ts b/src/models/collection.ts new file mode 100644 index 0000000..0abcce5 --- /dev/null +++ b/src/models/collection.ts @@ -0,0 +1,139 @@ +/** + * Class model for Collection Interface + */ + +import type { + CollectionInterface, + CollectionContentsInterface, + CollectionPermissionsInterface, + CollectionRolesInterface +} from "@/types/collection"; + +export class CollectionObject implements CollectionInterface { + + _data!: CollectionInterface; + + constructor() { + this._data = { + '@type': 'people:collection', + provider: null, + service: null, + in: null, + id: null, + label: null, + description: null, + priority: null, + visibility: null, + color: null, + enabled: false, + signature: null, + permissions: {}, + roles: {}, + contents: {}, + }; + } + + fromJson(data: CollectionInterface) : CollectionObject { + this._data = data; + return this; + } + + toJson(): CollectionInterface { + return this._data; + } + + clone(): CollectionObject { + const cloned = new CollectionObject() + cloned._data = JSON.parse(JSON.stringify(this._data)) + return cloned + } + + /** Immutable Properties */ + + get '@type'(): string { + return this._data['@type']; + } + + get provider(): string | null { + return this._data.provider + } + + get service(): string | null { + return this._data.service + } + + get in(): number | string | null { + return this._data.in + } + + get id(): number | string | null { + return this._data.id + } + + get signature(): string | null { + return this._data.signature + } + + get permissions(): CollectionPermissionsInterface { + return this._data.permissions + } + + get roles(): CollectionRolesInterface { + return this._data.roles + } + + get contents(): CollectionContentsInterface { + return this._data.contents + } + + /** Mutable Properties */ + + get label(): string | null { + return this._data.label + } + + set label(value: string ) { + this._data.label = value + } + + get description(): string | null { + return this._data.description + } + + set description(value: string ) { + this._data.description = value + } + + get priority(): number | null { + return this._data.priority + } + + set priority(value: number ) { + this._data.priority = value + } + + get visibility(): string | null { + return this._data.visibility + } + + set visibility(value: string ) { + this._data.visibility = value + } + + get color(): string | null { + return this._data.color + } + + set color(value: string ) { + this._data.color = value + } + + get enabled(): boolean { + return this._data.enabled + } + + set enabled(value: boolean ) { + this._data.enabled = value + } + +} \ No newline at end of file diff --git a/src/models/entity.ts b/src/models/entity.ts new file mode 100644 index 0000000..b631ebd --- /dev/null +++ b/src/models/entity.ts @@ -0,0 +1,153 @@ +/** + * Class model for Entity Interface + */ + +import type { EntityInterface } from "@/types/entity"; +import type { IndividualInterface } from "@/types/individual"; +import type { OrganizationInterface } from "@/types/organization"; +import type { GroupInterface } from "@/types/group"; +import { IndividualObject } from "./individual"; +import { OrganizationObject } from "./organization"; +import { GroupObject } from "./group"; + +export class EntityObject implements EntityInterface { + + _data!: EntityInterface; + + constructor() { + this._data = { + '@type': 'people:entity', + version: 1, + in: null, + id: null, + createdOn: null, + createdBy: null, + modifiedOn: null, + modifiedBy: null, + signature: null, + data: null, + }; + } + + fromJson(data: EntityInterface) : EntityObject { + this._data = data; + if (data.data) { + const type = data.data.type; + if (type === 'organization') { + this._data.data = new OrganizationObject().fromJson(data.data as OrganizationInterface); + } else if (type === 'group') { + this._data.data = new GroupObject().fromJson(data.data as GroupInterface); + } else { + this._data.data = new IndividualObject().fromJson(data.data as IndividualInterface); + } + } else { + this._data.data = null; + } + return this; + } + + toJson(): EntityInterface { + const json = { ...this._data }; + if (this._data.data instanceof IndividualObject || + this._data.data instanceof OrganizationObject || + this._data.data instanceof GroupObject) { + json.data = this._data.data.toJson(); + } + return json; + } + + clone(): EntityObject { + const cloned = new EntityObject() + cloned._data = JSON.parse(JSON.stringify(this._data)) + return cloned + } + + /** Immutable Properties */ + + get '@type'(): string { + return this._data['@type']; + } + + get in(): number | string | null { + return this._data.in; + } + + get id(): number | string | null { + return this._data.id; + } + + get version(): number { + return this._data.version; + } + + get createdOn(): Date | null { + return this._data.createdOn; + } + + get createdBy(): string | null { + return this._data.createdBy; + } + + get modifiedOn(): Date | null { + return this._data.modifiedOn; + } + + get modifiedBy(): string | null { + return this._data.modifiedBy; + } + + get signature(): string | null { + return this._data.signature; + } + + /** Mutable Properties */ + + set createdOn(value: Date | null) { + this._data.createdOn = value; + } + + set createdBy(value: string | null) { + this._data.createdBy = value; + } + + set modifiedOn(value: Date | null) { + this._data.modifiedOn = value; + } + + set modifiedBy(value: string | null) { + this._data.modifiedBy = value; + } + + set signature(value: string | null) { + this._data.signature = value; + } + + get data(): IndividualObject | OrganizationObject | GroupObject | null { + if (this._data.data instanceof IndividualObject || + this._data.data instanceof OrganizationObject || + this._data.data instanceof GroupObject) { + return this._data.data; + } + + if (this._data.data) { + const type = this._data.data.type; + let hydrated; + if (type === 'organization') { + hydrated = new OrganizationObject().fromJson(this._data.data as OrganizationInterface); + } else if (type === 'group') { + hydrated = new GroupObject().fromJson(this._data.data as GroupInterface); + } else { + hydrated = new IndividualObject().fromJson(this._data.data as IndividualInterface); + } + this._data.data = hydrated; + return hydrated; + } + + return null; + } + + set data(value: IndividualObject | OrganizationObject | GroupObject | null) { + this._data.data = value; + } + +} \ No newline at end of file diff --git a/src/models/group.ts b/src/models/group.ts new file mode 100644 index 0000000..4c549c7 --- /dev/null +++ b/src/models/group.ts @@ -0,0 +1,262 @@ +/** + * Class model for Group Interface + */ + +import { reactive } from 'vue' +import { generateUrid, generateKey } from "../utils/key-generator"; +import type { + GroupInterface, + GroupName, + GroupMember, + GroupVirtualLocation, + GroupMedia, + GroupNote +} from '@/types/group'; + +export class GroupObject implements GroupInterface { + + _data: GroupInterface; + + constructor() { + this._data = reactive({ + type: 'group', + version: 1, + urid: generateUrid(), + created: null, + modified: null, + label: '', + names: { + full: null, + sort: null, + aliases: [] + }, + members: {}, + virtualLocations: {}, + media: {}, + tags: [], + notes: {}, + }); + } + + fromJson(data: GroupInterface): GroupObject { + // Normalize arrays to Records for properties that should be Record types + const normalized = { + ...data, + members: Array.isArray(data.members) ? {} : (data.members || {}), + virtualLocations: Array.isArray(data.virtualLocations) ? {} : (data.virtualLocations || {}), + media: Array.isArray(data.media) ? {} : (data.media || {}), + notes: Array.isArray(data.notes) ? {} : (data.notes || {}), + }; + this._data = reactive(normalized); + return this; + } + + toJson(): GroupInterface { + return this._data; + } + + clone(): GroupObject { + const cloned = new GroupObject() + cloned._data = JSON.parse(JSON.stringify(this._data)) + return cloned + } + + /** Immutable Properties */ + get type(): string { + return this._data.type; + } + + get version(): number { + return this._data.version; + } + + get urid(): string | null { + return this._data.urid; + } + + /** Mutable Properties */ + + get created(): Date | null { + return this._data.created; + } + + set created(value: Date | null) { + this._data.created = value; + } + + get modified(): Date | null { + return this._data.modified; + } + + set modified(value: Date | null) { + this._data.modified = value; + } + + get label(): string | null { + return this._data.label; + } + + set label(value: string | null) { + this._data.label = value; + } + + get names(): GroupName { + return this._data.names; + } + + set names(value: GroupName) { + this._data.names = value; + } + + get members(): Record { + return this._data.members; + } + + set members(value: Record) { + this._data.members = value; + } + + get virtualLocations(): Record { + return this._data.virtualLocations; + } + + set virtualLocations(value: Record) { + this._data.virtualLocations = value; + } + + get media(): Record { + return this._data.media; + } + + set media(value: Record) { + this._data.media = value; + } + + get tags(): string[] { + return this._data.tags; + } + + set tags(value: string[]) { + this._data.tags = value; + } + + get notes(): Record { + return this._data.notes; + } + + set notes(value: Record) { + this._data.notes = value; + } + + /** + * Mutation helpers for complex properties + */ + + addMember(initial?: Partial): string { + const members = this._data.members || (this._data.members = {}); + const key = generateKey(); + members[key] = { + entityId: null, + role: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeMember(key: string): void { + if (this._data.members) { + delete this._data.members[key]; + } + } + + addVirtualLocation(initial?: Partial): string { + const virtualLocations = this._data.virtualLocations || (this._data.virtualLocations = {}); + const key = generateKey(); + virtualLocations[key] = { + location: null, + label: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeVirtualLocation(key: string): void { + if (this._data.virtualLocations) { + delete this._data.virtualLocations[key]; + } + } + + addMedia(initial?: Partial): string { + const media = this._data.media || (this._data.media = {}); + const key = generateKey(); + media[key] = { + type: "logo", + kind: "logo", + uri: "", + mediaType: null, + contexts: null, + pref: null, + label: null, + ...initial, + }; + return key; + } + + removeMedia(key: string): void { + if (this._data.media) { + delete this._data.media[key]; + } + } + + addNote(initial?: Partial): string { + const notes = this._data.notes || (this._data.notes = {}); + const key = generateKey(); + notes[key] = { + content: null, + date: null, + authorUri: null, + authorName: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeNote(key: string): void { + if (this._data.notes) { + delete this._data.notes[key]; + } + } + + addTag(value: string): void { + const tags = this._data.tags || (this._data.tags = []); + tags.push(value); + } + + removeTag(value: string): void { + const tags = this._data.tags; + const index = tags.indexOf(value); + if (index >= 0) { + tags.splice(index, 1); + } + } + + addAlias(value: string): void { + const aliases = this._data.names.aliases || (this._data.names.aliases = []); + aliases.push(value); + } + + removeAlias(value: string): void { + const aliases = this._data.names.aliases; + const index = aliases.indexOf(value); + if (index >= 0) { + aliases.splice(index, 1); + } + } + +} diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..9390914 --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,11 @@ +/** + * Central export point for all People Manager models + */ + +export { CollectionObject } from './collection'; +export { EntityObject } from './entity'; +export { GroupObject } from './group'; +export { IndividualObject } from './individual'; +export { OrganizationObject } from './organization'; +export { ProviderObject } from './provider'; +export { ServiceObject } from './service'; \ No newline at end of file diff --git a/src/models/individual.ts b/src/models/individual.ts new file mode 100644 index 0000000..856e09a --- /dev/null +++ b/src/models/individual.ts @@ -0,0 +1,483 @@ +/** + * Class model for Individual Interface + */ + +import { reactive } from 'vue' +import { generateUrid, generateKey } from "../utils/key-generator"; +import type { + IndividualAnniversary, + IndividualCrypto, + IndividualEmail, + IndividualInterface, + IndividualLanguage, + IndividualMedia, + IndividualName, + IndividualNote, + IndividualOrganization, + IndividualPhone, + IndividualPhysicalLocation, + IndividualTitle, + IndividualVirtualLocation +} from "@/types/individual"; + +export class IndividualObject implements IndividualInterface { + + _data: IndividualInterface; + + constructor() { + this._data = reactive({ + type: 'individual', + version: 1, + urid: generateUrid(), + created: null, + modified: null, + label: '', + names: { + family: null, + given: null, + additional: null, + prefix: null, + suffix: null, + phoneticFamily: null, + phoneticGiven: null, + phoneticAdditional: null, + aliases: [] + }, + titles: {}, + anniversaries: [], + physicalLocations: {}, + phones: {}, + emails: {}, + virtualLocations: {}, + media: {}, + organizations: {}, + tags: [], + notes: {}, + language: null, + languages: [], + crypto: {}, + }); + } + + fromJson(data: IndividualInterface) : IndividualObject { + // Normalize arrays to Records for properties that should be Record types + const normalized = { + ...data, + titles: Array.isArray(data.titles) ? {} : (data.titles || {}), + physicalLocations: Array.isArray(data.physicalLocations) ? {} : (data.physicalLocations || {}), + phones: Array.isArray(data.phones) ? {} : (data.phones || {}), + emails: Array.isArray(data.emails) ? {} : (data.emails || {}), + virtualLocations: Array.isArray(data.virtualLocations) ? {} : (data.virtualLocations || {}), + media: Array.isArray(data.media) ? {} : (data.media || {}), + notes: Array.isArray(data.notes) ? {} : (data.notes || {}), + crypto: Array.isArray(data.crypto) ? {} : (data.crypto || {}), + }; + this._data = reactive(normalized); + return this; + } + + toJson(): IndividualInterface { + return this._data; + } + + clone(): IndividualObject { + const cloned = new IndividualObject() + cloned._data = JSON.parse(JSON.stringify(this._data)) + return cloned + } + + /** Immutable Properties */ + get type(): string { + return this._data.type; + } + + get version(): number { + return this._data.version; + } + + get urid(): string | null { + return this._data.urid; + } + + /** Mutable Properties */ + + get created(): Date | null { + return this._data.created; + } + + set created(value: Date | null) { + this._data.created = value; + } + + get modified(): Date | null { + return this._data.modified; + } + + set modified(value: Date | null) { + this._data.modified = value; + } + + get label(): string | null { + return this._data.label; + } + + set label(value: string | null) { + this._data.label = value; + } + + get names(): IndividualName { + return this._data.names; + } + + set names(value: IndividualName) { + this._data.names = value; + } + + get titles(): Record { + return this._data.titles; + } + + set titles(value: Record) { + this._data.titles = value; + } + + get anniversaries(): IndividualAnniversary[] { + return this._data.anniversaries; + } + + set anniversaries(value: IndividualAnniversary[]) { + this._data.anniversaries = value; + } + + get physicalLocations(): Record { + return this._data.physicalLocations; + } + + set physicalLocations(value: Record) { + this._data.physicalLocations = value; + } + + get phones(): Record { + return this._data.phones; + } + + set phones(value: Record) { + this._data.phones = value; + } + + get emails(): Record { + return this._data.emails; + } + + set emails(value: Record) { + this._data.emails = value; + } + + get virtualLocations(): Record { + return this._data.virtualLocations; + } + + set virtualLocations(value: Record) { + this._data.virtualLocations = value; + } + + get media(): Record { + return this._data.media; + } + + set media(value: Record) { + this._data.media = value; + } + + get organizations(): Record { + return this._data.organizations; + } + + set organizations(value: Record) { + this._data.organizations = value; + } + + get tags(): string[] { + return this._data.tags; + } + + set tags(value: string[]) { + this._data.tags = value; + } + + get notes(): Record { + return this._data.notes; + } + + set notes(value: Record) { + this._data.notes = value; + } + + get language(): string | null { + return this._data.language; + } + + set language(value: string | null) { + this._data.language = value; + } + + get languages(): IndividualLanguage[] { + return this._data.languages; + } + + set languages(value: IndividualLanguage[]) { + this._data.languages = value; + } + + get crypto(): Record { + return this._data.crypto; + } + + set crypto(value: Record) { + this._data.crypto = value; + } + + /** + * Mutation helpers for complex properties + */ + + addTitle(initial?: Partial): string { + if (!this._data.titles) { + this._data.titles = {}; + } + const key = generateKey(); + this._data.titles[key] = { + kind: null, + label: null, + relation: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeTitle(key: string): void { + if (this._data.titles) { + delete this._data.titles[key]; + } + } + + addAnniversary(initial?: Partial): number { + const anniversaries = this._data.anniversaries || (this._data.anniversaries = []); + const entry: IndividualAnniversary = { + type: null, + when: null, + location: null, + ...initial, + }; + anniversaries.push(entry); + return anniversaries.length - 1; + } + + removeAnniversary(index: number): void { + const anniversaries = this._data.anniversaries; + if (index >= 0 && index < anniversaries.length) { + anniversaries.splice(index, 1); + } + } + + addPhysicalLocation(initial?: Partial): string { + const locations = this._data.physicalLocations || (this._data.physicalLocations = {}); + const key = generateKey(); + locations[key] = { + box: null, + unit: null, + street: null, + locality: null, + region: null, + code: null, + country: null, + label: null, + coordinates: null, + timeZone: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removePhysicalLocation(key: string): void { + if (this._data.physicalLocations) { + delete this._data.physicalLocations[key]; + } + } + + addPhone(initial?: Partial): string { + const phones = this._data.phones || (this._data.phones = {}); + const key = generateKey(); + phones[key] = { + number: null, + label: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removePhone(key: string): void { + if (this._data.phones) { + delete this._data.phones[key]; + } + } + + addEmail(initial?: Partial): string { + const emails = this._data.emails || (this._data.emails = {}); + const key = generateKey(); + emails[key] = { + address: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeEmail(key: string): void { + if (this._data.emails) { + delete this._data.emails[key]; + } + } + + addVirtualLocation(initial?: Partial): string { + const virtualLocations = this._data.virtualLocations || (this._data.virtualLocations = {}); + const key = generateKey(); + virtualLocations[key] = { + location: null, + label: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeVirtualLocation(key: string): void { + if (this._data.virtualLocations) { + delete this._data.virtualLocations[key]; + } + } + + addMedia(initial?: Partial): string { + const media = this._data.media || (this._data.media = {}); + const key = generateKey(); + media[key] = { + type: "photo", + kind: "photo", + uri: "", + mediaType: null, + contexts: null, + pref: null, + label: null, + ...initial, + }; + return key; + } + + removeMedia(key: string): void { + if (this._data.media) { + delete this._data.media[key]; + } + } + + addOrganization(initial?: Partial): string { + const organizations = this._data.organizations || (this._data.organizations = {}); + const key = generateKey(); + organizations[key] = { + Label: null, + Units: [], + sortName: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeOrganization(key: string): void { + if (this._data.organizations) { + delete this._data.organizations[key]; + } + } + + addNote(initial?: Partial): string { + const notes = this._data.notes || (this._data.notes = {}); + const key = generateKey(); + notes[key] = { + content: null, + date: null, + authorUri: null, + authorName: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeNote(key: string): void { + if (this._data.notes) { + delete this._data.notes[key]; + } + } + + addLanguage(initial?: Partial): number { + const languages = this._data.languages || (this._data.languages = []); + const entry: IndividualLanguage = { + Data: null, + Id: null, + Priority: null, + Context: null, + ...initial, + }; + languages.push(entry); + return languages.length - 1; + } + + removeLanguage(index: number): void { + const languages = this._data.languages; + if (index >= 0 && index < languages.length) { + languages.splice(index, 1); + } + } + + addCrypto(initial?: Partial): string { + const cryptoEntries = this._data.crypto || (this._data.crypto = {}); + const key = generateKey(); + cryptoEntries[key] = { + data: null, + type: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeCrypto(key: string): void { + if (this._data.crypto) { + delete this._data.crypto[key]; + } + } + + addTag(value: string): void { + const tags = this._data.tags || (this._data.tags = []); + tags.push(value); + } + + removeTag(value: string): void { + const tags = this._data.tags; + const index = tags.indexOf(value); + if (index >= 0) { + tags.splice(index, 1); + } + } + +} \ No newline at end of file diff --git a/src/models/organization.ts b/src/models/organization.ts new file mode 100644 index 0000000..379ff5f --- /dev/null +++ b/src/models/organization.ts @@ -0,0 +1,359 @@ +/** + * Class model for Organization Interface + */ + +import { reactive } from 'vue' +import { generateUrid, generateKey } from "../utils/key-generator"; +import type { + OrganizationInterface, + OrganizationName, + OrganizationPhysicalLocation, + OrganizationPhone, + OrganizationEmail, + OrganizationVirtualLocation, + OrganizationMedia, + OrganizationNote, + OrganizationCrypto +} from "@/types/organization"; + +export class OrganizationObject implements OrganizationInterface { + + _data: OrganizationInterface; + + constructor() { + this._data = reactive({ + type: 'organization', + version: 1, + urid: generateUrid(), + created: null, + modified: null, + label: '', + names: { + full: null, + sort: null, + aliases: [] + }, + physicalLocations: {}, + phones: {}, + emails: {}, + virtualLocations: {}, + media: {}, + tags: [], + notes: {}, + crypto: {}, + }); + } + + fromJson(data: OrganizationInterface): OrganizationObject { + // Normalize arrays to Records for properties that should be Record types + const normalized = { + ...data, + physicalLocations: Array.isArray(data.physicalLocations) ? {} : (data.physicalLocations || {}), + phones: Array.isArray(data.phones) ? {} : (data.phones || {}), + emails: Array.isArray(data.emails) ? {} : (data.emails || {}), + virtualLocations: Array.isArray(data.virtualLocations) ? {} : (data.virtualLocations || {}), + media: Array.isArray(data.media) ? {} : (data.media || {}), + notes: Array.isArray(data.notes) ? {} : (data.notes || {}), + crypto: Array.isArray(data.crypto) ? {} : (data.crypto || {}), + }; + this._data = reactive(normalized); + return this; + } + + toJson(): OrganizationInterface { + return this._data; + } + + clone(): OrganizationObject { + const cloned = new OrganizationObject() + cloned._data = JSON.parse(JSON.stringify(this._data)) + return cloned + } + + /** Immutable Properties */ + get type(): string { + return this._data.type; + } + + get version(): number { + return this._data.version; + } + + get urid(): string | null { + return this._data.urid; + } + + /** Mutable Properties */ + + get created(): Date | null { + return this._data.created; + } + + set created(value: Date | null) { + this._data.created = value; + } + + get modified(): Date | null { + return this._data.modified; + } + + set modified(value: Date | null) { + this._data.modified = value; + } + + get label(): string | null { + return this._data.label; + } + + set label(value: string | null) { + this._data.label = value; + } + + get names(): OrganizationName { + return this._data.names; + } + + set names(value: OrganizationName) { + this._data.names = value; + } + + get physicalLocations(): Record { + return this._data.physicalLocations; + } + + set physicalLocations(value: Record) { + this._data.physicalLocations = value; + } + + get phones(): Record { + return this._data.phones; + } + + set phones(value: Record) { + this._data.phones = value; + } + + get emails(): Record { + return this._data.emails; + } + + set emails(value: Record) { + this._data.emails = value; + } + + get virtualLocations(): Record { + return this._data.virtualLocations; + } + + set virtualLocations(value: Record) { + this._data.virtualLocations = value; + } + + get media(): Record { + return this._data.media; + } + + set media(value: Record) { + this._data.media = value; + } + + get tags(): string[] { + return this._data.tags; + } + + set tags(value: string[]) { + this._data.tags = value; + } + + get notes(): Record { + return this._data.notes; + } + + set notes(value: Record) { + this._data.notes = value; + } + + get crypto(): Record { + return this._data.crypto; + } + + set crypto(value: Record) { + this._data.crypto = value; + } + + /** + * Mutation helpers for complex properties + */ + + addPhysicalLocation(initial?: Partial): string { + const locations = this._data.physicalLocations || (this._data.physicalLocations = {}); + const key = generateKey(); + locations[key] = { + box: null, + unit: null, + street: null, + locality: null, + region: null, + code: null, + country: null, + label: null, + coordinates: null, + timeZone: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removePhysicalLocation(key: string): void { + if (this._data.physicalLocations) { + delete this._data.physicalLocations[key]; + } + } + + addPhone(initial?: Partial): string { + const phones = this._data.phones || (this._data.phones = {}); + const key = generateKey(); + phones[key] = { + number: null, + label: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removePhone(key: string): void { + if (this._data.phones) { + delete this._data.phones[key]; + } + } + + addEmail(initial?: Partial): string { + const emails = this._data.emails || (this._data.emails = {}); + const key = generateKey(); + emails[key] = { + address: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeEmail(key: string): void { + if (this._data.emails) { + delete this._data.emails[key]; + } + } + + addVirtualLocation(initial?: Partial): string { + const virtualLocations = this._data.virtualLocations || (this._data.virtualLocations = {}); + const key = generateKey(); + virtualLocations[key] = { + location: null, + label: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeVirtualLocation(key: string): void { + if (this._data.virtualLocations) { + delete this._data.virtualLocations[key]; + } + } + + addMedia(initial?: Partial): string { + const media = this._data.media || (this._data.media = {}); + const key = generateKey(); + media[key] = { + type: "logo", + kind: "logo", + uri: "", + mediaType: null, + contexts: null, + pref: null, + label: null, + ...initial, + }; + return key; + } + + removeMedia(key: string): void { + if (this._data.media) { + delete this._data.media[key]; + } + } + + addNote(initial?: Partial): string { + const notes = this._data.notes || (this._data.notes = {}); + const key = generateKey(); + notes[key] = { + content: null, + date: null, + authorUri: null, + authorName: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeNote(key: string): void { + if (this._data.notes) { + delete this._data.notes[key]; + } + } + + addCrypto(initial?: Partial): string { + const cryptoEntries = this._data.crypto || (this._data.crypto = {}); + const key = generateKey(); + cryptoEntries[key] = { + data: null, + type: null, + context: null, + priority: null, + ...initial, + }; + return key; + } + + removeCrypto(key: string): void { + if (this._data.crypto) { + delete this._data.crypto[key]; + } + } + + addTag(value: string): void { + const tags = this._data.tags || (this._data.tags = []); + tags.push(value); + } + + removeTag(value: string): void { + const tags = this._data.tags; + const index = tags.indexOf(value); + if (index >= 0) { + tags.splice(index, 1); + } + } + + addAlias(value: string): void { + const aliases = this._data.names.aliases || (this._data.names.aliases = []); + aliases.push(value); + } + + removeAlias(value: string): void { + const aliases = this._data.names.aliases; + const index = aliases.indexOf(value); + if (index >= 0) { + aliases.splice(index, 1); + } + } + +} diff --git a/src/models/provider.ts b/src/models/provider.ts new file mode 100644 index 0000000..9aefd56 --- /dev/null +++ b/src/models/provider.ts @@ -0,0 +1,61 @@ +/** + * Class model for Provider Interface + */ + +import type { + ProviderInterface, + ProviderCapabilitiesInterface +} from "@/types/provider"; + +export class ProviderObject implements ProviderInterface { + + _data!: ProviderInterface; + + constructor() { + this._data = { + '@type': 'people:provider', + id: '', + label: '', + capabilities: {}, + }; + } + + fromJson(data: ProviderInterface): ProviderObject { + this._data = data; + return this; + } + + toJson(): ProviderInterface { + return this._data; + } + + capable(capability: keyof ProviderCapabilitiesInterface): boolean { + return !!(this._data.capabilities && this._data.capabilities[capability]); + } + + capability(capability: keyof ProviderCapabilitiesInterface): any | null { + if (this._data.capabilities) { + return this._data.capabilities[capability]; + } + return null; + } + + /** Immutable Properties */ + + get '@type'(): string { + return this._data['@type']; + } + + get id(): string { + return this._data.id; + } + + get label(): string { + return this._data.label; + } + + get capabilities(): ProviderCapabilitiesInterface { + return this._data.capabilities; + } + +} \ No newline at end of file diff --git a/src/models/service.ts b/src/models/service.ts new file mode 100644 index 0000000..c223d8e --- /dev/null +++ b/src/models/service.ts @@ -0,0 +1,81 @@ +/** + * Class model for Service Interface + */ + +import type { + ServiceInterface, + ServiceCapabilitiesInterface +} from "@/types/service"; + +export class ServiceObject implements ServiceInterface { + + _data!: ServiceInterface; + + constructor() { + this._data = { + '@type': 'people:service', + provider: '', + id: '', + label: '', + capabilities: {}, + enabled: false, + }; + } + + fromJson(data: ServiceInterface): ServiceObject { + this._data = data; + return this; + } + + toJson(): ServiceInterface { + return this._data; + } + + capable(capability: keyof ServiceCapabilitiesInterface): boolean { + return !!(this._data.capabilities && this._data.capabilities[capability]); + } + + capability(capability: keyof ServiceCapabilitiesInterface): any | null { + if (this._data.capabilities) { + return this._data.capabilities[capability]; + } + return null; + } + + /** Immutable Properties */ + + get '@type'(): string { + return this._data['@type']; + } + + get provider(): string { + return this._data.provider; + } + + get id(): string { + return this._data.id; + } + + get capabilities(): ServiceCapabilitiesInterface | undefined { + return this._data.capabilities; + } + + /** Mutable Properties */ + + get label(): string { + return this._data.label; + } + + set label(value: string) { + this._data.label = value; + } + + get enabled(): boolean { + return this._data.enabled; + } + + set enabled(value: boolean) { + this._data.enabled = value; + } + +} \ No newline at end of file diff --git a/src/services/collectionService.ts b/src/services/collectionService.ts new file mode 100644 index 0000000..01615d8 --- /dev/null +++ b/src/services/collectionService.ts @@ -0,0 +1,84 @@ +/** + * Collection management service + */ + +import { transceivePost } from './transceive'; +import type { + CollectionListRequest, + CollectionListResponse, + CollectionExtantRequest, + CollectionExtantResponse, + CollectionFetchRequest, + CollectionFetchResponse, + CollectionCreateRequest, + CollectionCreateResponse, + CollectionModifyRequest, + CollectionModifyResponse, + CollectionDestroyRequest, + CollectionDestroyResponse, +} from '../types/collection'; + +export const collectionService = { + + /** + * List all available collections + * + * @param request - Collection list request parameters + * @returns Promise with collection list grouped by provider and service + */ + async list(request: CollectionListRequest = {}): Promise { + return await transceivePost('collection.list', request); + }, + + /** + * Check which collections exist/are available + * + * @param request - Collection extant request with source selector + * @returns Promise with collection availability status + */ + async extant(request: CollectionExtantRequest): Promise { + return await transceivePost('collection.extant', request); + }, + + /** + * Fetch a specific collection + * + * @param request - Collection fetch request + * @returns Promise with collection details + */ + async fetch(request: CollectionFetchRequest): Promise { + return await transceivePost('collection.fetch', request); + }, + + /** + * Create a new collection + * + * @param request - Collection create request + * @returns Promise with created collection + */ + async create(request: CollectionCreateRequest): Promise { + return await transceivePost('collection.create', request); + }, + + /** + * Modify an existing collection + * + * @param request - Collection modify request + * @returns Promise with modified collection + */ + async modify(request: CollectionModifyRequest): Promise { + return await transceivePost('collection.modify', request); + }, + + /** + * Delete a collection + * + * @param request - Collection destroy request + * @returns Promise with deletion result + */ + async destroy(request: CollectionDestroyRequest): Promise { + return await transceivePost('collection.destroy', request); + }, +}; + +export default collectionService; \ No newline at end of file diff --git a/src/services/entityService.ts b/src/services/entityService.ts new file mode 100644 index 0000000..e9bd89d --- /dev/null +++ b/src/services/entityService.ts @@ -0,0 +1,96 @@ +/** + * Entity management service + */ + +import { transceivePost } from './transceive'; +import type { + EntityListRequest, + EntityListResponse, + EntityDeltaRequest, + EntityDeltaResponse, + EntityExtantRequest, + EntityExtantResponse, + EntityFetchRequest, + EntityFetchResponse, + EntityCreateRequest, + EntityCreateResponse, + EntityModifyRequest, + EntityModifyResponse, + EntityDestroyRequest, + EntityDestroyResponse, +} from '../types/entity'; + +export const entityService = { + + /** + * List all available entities (events, tasks, journals) + * + * @param request - Entity list request parameters + * @returns Promise with entity list grouped by provider, service, and collection + */ + async list(request: EntityListRequest = {}): Promise { + return await transceivePost('entity.list', request); + }, + + /** + * Get delta changes for entities + * + * @param request - Entity delta request with source selector + * @returns Promise with delta changes (created, modified, deleted) + */ + async delta(request: EntityDeltaRequest): Promise { + return await transceivePost('entity.delta', request); + }, + + /** + * Check which entities exist/are available + * + * @param request - Entity extant request with source selector + * @returns Promise with entity availability status + */ + async extant(request: EntityExtantRequest): Promise { + return await transceivePost('entity.extant', request); + }, + + /** + * Fetch specific entities + * + * @param request - Entity fetch request + * @returns Promise with entity details + */ + async fetch(request: EntityFetchRequest): Promise { + return await transceivePost('entity.fetch', request); + }, + + /** + * Create a new entity + * + * @param request - Entity create request + * @returns Promise with created entity + */ + async create(request: EntityCreateRequest): Promise { + return await transceivePost('entity.create', request); + }, + + /** + * Modify an existing entity + * + * @param request - Entity modify request + * @returns Promise with modified entity + */ + async modify(request: EntityModifyRequest): Promise { + return await transceivePost('entity.modify', request); + }, + + /** + * Delete an entity + * + * @param request - Entity destroy request + * @returns Promise with deletion result + */ + async destroy(request: EntityDestroyRequest): Promise { + return await transceivePost('entity.destroy', request); + }, +}; + +export default entityService; diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..60793c5 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,16 @@ +/** + * Central export point for all People Manager services + */ + +// Services +export { providerService } from './providerService'; +export { serviceService } from './serviceService'; +export { collectionService } from './collectionService'; +export { entityService } from './entityService'; + +// Type exports +export type * from '../types/common'; +export type * from '../types/provider'; +export type * from '../types/service'; +export type * from '../types/collection'; +export type * from '../types/entity'; diff --git a/src/services/providerService.ts b/src/services/providerService.ts new file mode 100644 index 0000000..4642322 --- /dev/null +++ b/src/services/providerService.ts @@ -0,0 +1,32 @@ +/** + * Provider management service + */ + +import { transceivePost } from './transceive'; +import type { ProviderListResponse, ProviderExtantResponse } from '../types/provider'; +import type { SourceSelector } from '../types/common'; + +export const providerService = { + + /** + * List all available providers + * + * @returns Promise with provider list keyed by provider ID + */ + async list(): Promise { + return await transceivePost<{}, ProviderListResponse>('provider.list', {}); + }, + + /** + * Check which providers exist/are available + * + * @param sources - Source selector with provider IDs to check + * + * @returns Promise with provider availability status + */ + async extant(sources: SourceSelector): Promise { + return await transceivePost('provider.extant', { sources }); + }, +}; + +export default providerService; diff --git a/src/services/serviceService.ts b/src/services/serviceService.ts new file mode 100644 index 0000000..4b64b31 --- /dev/null +++ b/src/services/serviceService.ts @@ -0,0 +1,48 @@ +/** + * Service management service + */ + +import { transceivePost } from './transceive'; +import type { + ServiceListRequest, + ServiceListResponse, + ServiceExtantRequest, + ServiceExtantResponse, + ServiceFetchRequest, + ServiceFetchResponse, +} from '../types/service'; + +export const serviceService = { + + /** + * List all available services + * + * @param request - Service list request parameters + * @returns Promise with service list grouped by provider + */ + async list(request: ServiceListRequest = {}): Promise { + return await transceivePost('service.list', request); + }, + + /** + * Check which services exist/are available + * + * @param request - Service extant request with source selector + * @returns Promise with service availability status + */ + async extant(request: ServiceExtantRequest): Promise { + return await transceivePost('service.extant', request); + }, + + /** + * Fetch a specific service + * + * @param request - Service fetch request with provider and service IDs + * @returns Promise with service details + */ + async fetch(request: ServiceFetchRequest): Promise { + return await transceivePost('service.fetch', request); + }, +}; + +export default serviceService; diff --git a/src/services/transceive.ts b/src/services/transceive.ts new file mode 100644 index 0000000..5d28efd --- /dev/null +++ b/src/services/transceive.ts @@ -0,0 +1,50 @@ +/** + * API Client for People Manager + * Provides a centralized way to make API calls with envelope wrapping/unwrapping + */ + +import { createFetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper-core'; +import type { ApiRequest, ApiResponse } from '../types/common'; + +const fetchWrapper = createFetchWrapper(); +const API_URL = '/m/people_manager/v1'; +const API_VERSION = 1; + +/** + * Generate a unique transaction ID + */ +export function generateTransactionId(): string { + return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; +} + +/** + * Make an API call with automatic envelope wrapping and unwrapping + * + * @param operation - Operation name (e.g., 'provider.list', 'collection.fetch') + * @param data - Operation-specific request data + * @param user - Optional user identifier override + * @returns Promise with unwrapped response data + * @throws Error if the API returns an error status + */ +export async function transceivePost( + operation: string, + data: TRequest, + user?: string +): Promise { + const request: ApiRequest = { + version: API_VERSION, + transaction: generateTransactionId(), + operation, + data, + user + }; + + const response: ApiResponse = await fetchWrapper.post(API_URL, request); + + if (response.status === 'error') { + const errorMessage = `[${operation}] ${response.data.message}${response.data.code ? ` (code: ${response.data.code})` : ''}`; + throw new Error(errorMessage); + } + + return response.data; +} diff --git a/src/stores/collectionsStore.ts b/src/stores/collectionsStore.ts new file mode 100644 index 0000000..41312f0 --- /dev/null +++ b/src/stores/collectionsStore.ts @@ -0,0 +1,203 @@ +/** + * People Manager - Collections Store + */ + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { collectionService } from '../services/collectionService'; +import type { + SourceSelector, + ListFilter, + ListSort, +} from '../types/common'; +import { CollectionObject } from '../models/collection'; +import type { ServiceObject } from '../models/service'; +import type { CollectionInterface } from '../types/collection'; + +export const useCollectionsStore = defineStore('collectionsStore', () => { + // State + const collections = ref([]); + + // Actions + + /** + * Retrieve collections from the server + */ + async function list( + sources?: SourceSelector, + filter?: ListFilter, + sort?: ListSort, + uid?: string + ): Promise { + try { + const response = await collectionService.list({ sources, filter, sort, uid }); + + // Flatten the nested response into a flat array + const flatCollections: CollectionObject[] = []; + Object.entries(response).forEach(([_providerId, providerCollections]) => { + Object.entries(providerCollections).forEach(([_serviceId, serviceCollections]) => { + Object.values(serviceCollections).forEach((collection: CollectionInterface) => { + flatCollections.push(new CollectionObject().fromJson(collection)); + }); + }); + }); + + console.debug('[People Manager](Store) - Successfully retrieved', flatCollections.length, 'collections:', flatCollections.map(c => ({ + id: c.id, + label: c.label, + service: c.service, + provider: c.provider + }))); + + collections.value = flatCollections; + return flatCollections; + } catch (error: any) { + console.error('[People Manager](Store) - Failed to retrieve collections:', error); + throw error; + } + } + + /** + * Fetch a specific collection + */ + async function fetch( + provider: string, + service: string, + identifier: string | number, + uid?: string + ): Promise { + try { + const response = await collectionService.fetch({ provider, service, identifier, uid }); + + return new CollectionObject().fromJson(response); + } catch (error: any) { + console.error('[People Manager](Store) - Failed to fetch collection:', error); + throw error; + } + } + + /** + * Create a fresh collection object with default values + */ + function fresh(): CollectionObject { + return new CollectionObject(); + } + + /** + * Create a new collection + */ + async function create( + service: ServiceObject, + collection: CollectionObject, + options?: string[], + uid?: string + ): Promise { + try { + if (service.provider === null || service.id === null) { + throw new Error('Invalid service object, must have a provider and identifier'); + } + + const response = await collectionService.create({ + provider: service.provider, + service: service.id, + data: collection.toJson(), + options, + uid + }); + + const createdCollection = new CollectionObject().fromJson(response); + collections.value.push(createdCollection); + + console.debug('[People Manager](Store) - Successfully created collection'); + + return createdCollection; + } catch (error: any) { + console.error('[People Manager](Store) - Failed to create collection:', error); + throw error; + } + } + + /** + * Modify an existing collection + */ + async function modify( + collection: CollectionObject, + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + + const response = await collectionService.modify({ + provider: collection.provider, + service: collection.service, + identifier: collection.id, + data: collection.toJson(), + uid + }); + + const modifiedCollection = new CollectionObject().fromJson(response); + const index = collections.value.findIndex(c => c.id === collection.id); + if (index !== -1) { + collections.value[index] = modifiedCollection; + } + + console.debug('[People Manager](Store) - Successfully modified collection'); + + return modifiedCollection; + } catch (error: any) { + console.error('[People Manager](Store) - Failed to modify collection:', error); + throw error; + } + } + +/** + * Delete a collection + */ + async function destroy( + collection: CollectionObject, + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + + const response = await collectionService.destroy({ + provider: collection.provider, + service: collection.service, + identifier: collection.id, + uid + }); + + if (response.success) { + const index = collections.value.findIndex(c => c.id === collection.id); + if (index !== -1) { + collections.value.splice(index, 1); + } + } + + console.debug('[People Manager](Store) - Successfully destroyed collection'); + + return response.success; + } catch (error: any) { + console.error('[People Manager](Store) - Failed to destroy collection:', error); + throw error; + } + } + + + return { + // State + collections, + + // Actions + list, + fetch, + fresh, + create, + modify, + destroy, + }; +}); diff --git a/src/stores/entitiesStore.ts b/src/stores/entitiesStore.ts new file mode 100644 index 0000000..90c713a --- /dev/null +++ b/src/stores/entitiesStore.ts @@ -0,0 +1,276 @@ +/** + * People Manager - Entities Store + */ + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { entityService } from '../services/entityService'; +import type { + SourceSelector, + ListFilter, + ListSort, + ListRange, +} from '../types/common'; +import type { + EntityInterface, +} from '../types/entity'; +import type { CollectionObject } from '../models/collection'; +import { EntityObject } from '../models/entity' +import { IndividualObject } from '../models/individual'; +import { OrganizationObject } from '../models/organization'; +import { GroupObject } from '../models/group'; + +export const useEntitiesStore = defineStore('peopleEntitiesStore', () => { + // State + const entities = ref([]); + + // Actions + + /** + * Reset the store to initial state + */ + function reset(): void { + entities.value = []; + } +/** + * List entities for all or specific collection + */ + async function list( + provider: string | null, + service: string | null, + collection: string | number | null, + filter?: ListFilter, + sort?: ListSort, + range?: ListRange, + uid?: string + ): Promise { + try { + // Validate hierarchical requirements + if (collection !== null && (service === null || provider === null)) { + throw new Error('Collection requires both service and provider'); + } + if (service !== null && provider === null) { + throw new Error('Service requires provider'); + } + + // Build sources object level by level + const sources: SourceSelector = {}; + if (provider !== null) { + if (service !== null) { + if (collection !== null) { + sources[provider] = { [service]: { [collection]: true } }; + } else { + sources[provider] = { [service]: true }; + } + } else { + sources[provider] = true; + } + } + + // Transmit + const response = await entityService.list({ sources, filter, sort, range, uid }); + + // Flatten the nested response into a flat array + const flatEntities: EntityObject[] = []; + Object.entries(response).forEach(([, providerEntities]) => { + Object.entries(providerEntities).forEach(([, serviceEntities]) => { + Object.entries(serviceEntities).forEach(([, collectionEntities]) => { + Object.values(collectionEntities).forEach((entity: EntityInterface) => { + flatEntities.push(new EntityObject().fromJson(entity)); + }); + }); + }); + }); + + console.debug('[People Manager](Store) - Successfully retrieved', flatEntities.length, 'entities'); + + entities.value = flatEntities; + return flatEntities; + } catch (error: any) { + console.error('[People Manager](Store) - Failed to retrieve entities:', error); + throw error; + } + } + + /** + * Fetch entities for a specific collection + */ + async function fetch( + collection: CollectionObject, + identifiers: (string | number)[], + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + + const response = await entityService.fetch({ + provider: collection.provider, + service: collection.service, + collection: collection.id, + identifiers, + uid + }); + + return Object.values(response).map(entity => new EntityObject().fromJson(entity)); + } catch (error: any) { + console.error('[People Manager](Store) - Failed to fetch entities:', error); + throw error; + } + } + + /** + * Create a fresh entity object + */ + function fresh(type: string): EntityObject { + const entity = new EntityObject(); + + if (type === 'organization') { + entity.data = new OrganizationObject(); + } else if (type === 'group') { + entity.data = new GroupObject(); + } else { + entity.data = new IndividualObject(); + } + + entity.data.created = new Date(); + + return entity; + } + + /** + * Create a new entity + */ + async function create( + collection: CollectionObject, + entity: EntityObject, + options?: string[], + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + + const response = await entityService.create({ + provider: collection.provider, + service: collection.service, + collection: collection.id, + data: entity.toJson(), + options, + uid + }); + + const createdEntity = new EntityObject().fromJson(response); + entities.value.push(createdEntity); + + console.debug('[People Manager](Store) - Successfully created entity'); + + return createdEntity; + } catch (error: any) { + console.error('[People Manager](Store) - Failed to create entity:', error); + throw error; + } + } + + /** + * Modify an existing entity + */ + async function modify( + collection: CollectionObject, + entity: EntityObject, + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + if (!entity.in || !entity.id) { + throw new Error('Invalid entity object, must have an collection and entity identifier'); + } + if (collection.id !== entity.in) { + throw new Error('Invalid entity object, does not belong to the specified collection'); + } + + const response = await entityService.modify({ + provider: collection.provider, + service: collection.service, + collection: collection.id, + identifier: entity.id, + data: entity.toJson(), + uid + }); + + const modifiedEntity = new EntityObject().fromJson(response); + const index = entities.value.findIndex(e => e.id === entity.id); + if (index !== -1) { + entities.value[index] = modifiedEntity; + } + + console.debug('[People Manager](Store) - Successfully modified entity'); + + return modifiedEntity; + } catch (error: any) { + console.error('[People Manager](Store) - Failed to modify entity:', error); + throw error; + } + } + + /** + * Delete an entity + */ + async function destroy( + collection: CollectionObject, + entity: EntityObject, + uid?: string + ): Promise { + try { + if (!collection.provider || !collection.service || !collection.id) { + throw new Error('Collection must have provider, service, and id'); + } + if (!entity.in || !entity.id) { + throw new Error('Invalid entity object, must have an collection and entity identifier'); + } + if (collection.id !== entity.in) { + throw new Error('Invalid entity object, does not belong to the specified collection'); + } + + const response = await entityService.destroy({ + provider: collection.provider, + service: collection.service, + collection: collection.id, + identifier: entity.id, + uid + }); + + if (response.success) { + const index = entities.value.findIndex(e => e.id === entity.id); + if (index !== -1) { + entities.value.splice(index, 1); + } + } + + console.debug('[People Manager](Store) - Successfully destroyed entity'); + + return response.success; + } catch (error: any) { + console.error('[People Manager](Store) - Failed to destroy entity:', error); + throw error; + } + } + + return { + // State + entities, + + // Actions + reset, + list, + fetch, + fresh, + create, + modify, + destroy, + } +}); diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 0000000..4d03b97 --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,8 @@ +/** + * Central export point for all People Manager stores + */ + +export { useCollectionsStore } from './collectionsStore'; +export { useEntitiesStore } from './entitiesStore'; +export { useProvidersStore } from './providersStore'; +export { useServicesStore } from './servicesStore'; diff --git a/src/stores/providersStore.ts b/src/stores/providersStore.ts new file mode 100644 index 0000000..4fddf0a --- /dev/null +++ b/src/stores/providersStore.ts @@ -0,0 +1,62 @@ +/** + * People Manager - Providers Store + */ + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { providerService } from '../services/providerService'; +import type { + SourceSelector, + ProviderInterface, +} from '../types'; + +export const useProvidersStore = defineStore('providersStore', () => { + // State + const providers = ref>({}); + + // Actions + + /** + * List all available providers + * + * @returns Promise with provider list keyed by provider ID + */ + async function list(): Promise> { + try { + const response = await providerService.list(); + + console.debug('[People Manager](Store) - Successfully retrieved', Object.keys(response).length, 'providers:', Object.keys(response)); + + providers.value = response; + return response; + } catch (error: any) { + console.error('[People Manager](Store) - Failed to retrieve providers:', error); + throw error; + } + } + + /** + * Check which providers exist/are available + * + * @param sources - Source selector with provider IDs to check + * @returns Promise with provider availability status + */ + async function extant(sources: SourceSelector): Promise> { + try { + const response = await providerService.extant(sources); + return response; + } catch (error: any) { + console.error('[People Manager](Store) - Failed to check provider existence:', error); + throw error; + } + } + + return { + // State + providers, + + // Actions + list, + extant, + }; +}); diff --git a/src/stores/servicesStore.ts b/src/stores/servicesStore.ts new file mode 100644 index 0000000..818c8b4 --- /dev/null +++ b/src/stores/servicesStore.ts @@ -0,0 +1,95 @@ +/** + * People Manager - Services Store + */ + +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { serviceService } from '../services/serviceService'; +import { ServiceObject } from '../models/service'; +import type { ServiceInterface } from '../types/service'; +import type { + SourceSelector, + ListFilter, + ListSort, +} from '../types/common'; + +export const useServicesStore = defineStore('peopleServicesStore', () => { + // State + const services = ref([]); + // Actions + + /** + * Retrieve services from the server + */ + async function list( + sources?: SourceSelector, + filter?: ListFilter, + sort?: ListSort, + uid?: string + ): Promise { + try { + const response = await serviceService.list({ sources, filter, sort, uid }); + + // Flatten the nested response into a flat array + const flatServices: ServiceObject[] = []; + Object.entries(response).forEach(([providerId, providerServices]) => { + Object.values(providerServices).forEach((service: ServiceInterface) => { + // Ensure provider is set on the service object + service.provider = service.provider || providerId; + flatServices.push(new ServiceObject().fromJson(service)); + }); + }); + + console.debug('[People Manager](Store) - Successfully retrieved', flatServices.length, 'services:', flatServices.map(s => ({ + id: s.id, + label: s.label, + provider: s.provider + }))); + + services.value = flatServices; + return flatServices; + } catch (error: any) { + console.error('[People Manager](Store) - Failed to retrieve services:', error); + throw error; + } + } + + /** + * Fetch a specific service + * + * @param provider - Provider identifier + * @param identifier - Service identifier + * @param uid - Optional user identifier + * @returns Promise with service object + */ + async function fetch( + provider: string, + identifier: string, + uid?: string + ): Promise { + try { + const response = await serviceService.fetch({ provider, service: identifier, uid }); + return new ServiceObject().fromJson(response); + } catch (error: any) { + console.error('[People Manager](Store) - Failed to fetch service:', error); + throw error; + } + } + + /** + * Create a fresh service object with default values + */ + function fresh(): ServiceObject { + return new ServiceObject(); + } + + return { + // State + services, + + // Actions + list, + fetch, + fresh, + }; +}); diff --git a/src/types/collection.ts b/src/types/collection.ts new file mode 100644 index 0000000..2c16000 --- /dev/null +++ b/src/types/collection.ts @@ -0,0 +1,161 @@ +/** + * Collection-related type definitions for People Manager + */ + +import type { ListFilter, ListSort, SourceSelector } from "./common"; + +/** + * Permission settings for a collection + */ +export interface CollectionPermissionInterface { + view: boolean; + create: boolean; + modify: boolean; + destroy: boolean; + share: boolean; +} + +/** + * Permissions settings for multiple users in a collection + */ +export interface CollectionPermissionsInterface { + [userId: string]: CollectionPermissionInterface; +} + +/** + * Role settings for a collection + */ +export interface CollectionRolesInterface { + individual?: boolean; + [roleType: string]: boolean | undefined; +} + +/** + * Content type settings for a collection + */ +export interface CollectionContentsInterface { + individual?: boolean; + organization?: boolean; + group?: boolean; + [contentType: string]: boolean | undefined; +} + +/** + * Represents a collection within a service + */ +export interface CollectionInterface { + '@type': string; + provider: string | null; + service: string | null; + in: number | string | null; + id: number | string | null; + label: string | null; + description: string | null; + priority: number | null; + visibility: string | null; + color: string | null; + enabled: boolean; + signature: string | null; + permissions: CollectionPermissionsInterface; + roles: CollectionRolesInterface; + contents: CollectionContentsInterface; +} + +/** + * Request to collection list endpoint + */ +export interface CollectionListRequest { + sources?: SourceSelector; + filter?: ListFilter; + sort?: ListSort; +} + +/** + * Response from collection list endpoint + */ +export interface CollectionListResponse { + [providerId: string]: { + [serviceId: string]: { + [collectionId: string]: CollectionInterface; + }; + }; +} + +/** + * Request to collection extant endpoint + */ +export interface CollectionExtantRequest { + sources: SourceSelector; +} + +/** + * Response from collection extant endpoint + */ +export interface CollectionExtantResponse { + [providerId: string]: { + [serviceId: string]: { + [collectionId: string]: boolean; + }; + }; +} + +/** + * Request to collection fetch endpoint + */ +export interface CollectionFetchRequest { + provider: string; + service: string; + identifier: string | number; +} + +/** + * Response from collection fetch endpoint + */ +export interface CollectionFetchResponse extends CollectionInterface {} + +/** + * Request to collection create endpoint + */ +export interface CollectionCreateRequest { + provider: string; + service: string; + data: CollectionInterface; + options?: (string)[]; +} + +/** + * Response from collection create endpoint + */ +export interface CollectionCreateResponse extends CollectionInterface {} + +/** + * Request to collection modify endpoint + */ +export interface CollectionModifyRequest { + provider: string; + service: string; + identifier: string | number; + data: CollectionInterface; +} + +/** + * Response from collection modify endpoint + */ +export interface CollectionModifyResponse extends CollectionInterface {} + +/** + * Request to collection destroy endpoint + */ +export interface CollectionDestroyRequest { + provider: string; + service: string; + identifier: string | number; +} + + +/** + * Response from collection destroy endpoint + */ +export interface CollectionDestroyResponse { + success: boolean; +} diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 0000000..8b667a8 --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1,107 @@ +/** + * Common types shared across People Manager services + */ + +import type { FilterComparisonOperator, FilterConjunctionOperator } from './service'; + +/** + * Base API request envelope + */ +export interface ApiRequest { + version: number; + transaction: string; + operation: string; + data: T; + user?: string; +} + +/** + * Success response envelope + */ +export interface ApiSuccessResponse { + version: number; + transaction: string; + operation: string; + status: 'success'; + data: T; +} + +/** + * Error response envelope + */ +export interface ApiErrorResponse { + version: number; + transaction: string; + operation: string; + status: 'error'; + data: { + code: number; + message: string; + }; +} + +/** + * Combined response type + */ +export type ApiResponse = ApiSuccessResponse | ApiErrorResponse; + +/** + * Source selector structure for hierarchical resource selection + * Structure: Provider -> Service -> Collection -> Entity + * + * Examples: + * - Simple boolean: { "local": true } + * - Nested services: { "system": { "personal": true, "recents": true } } + * - Collection IDs: { "system": { "personal": { "299": true, "176": true } } } + * - Entity IDs: { "system": { "personal": { "299": [1350, 1353, 5000] } } } + */ +export type SourceSelector = { + [provider: string]: boolean | ServiceSelector; +}; + +export type ServiceSelector = { + [service: string]: boolean | CollectionSelector; +}; + +export type CollectionSelector = { + [collection: string | number]: boolean | EntitySelector; +}; + +export type EntitySelector = (string | number)[]; + +/** + * Filter condition for building complex queries + */ +export interface FilterCondition { + attribute: string; + value: string | number | boolean | any[]; + comparator?: FilterComparisonOperator; + conjunction?: FilterConjunctionOperator; +} + +/** + * Filter criteria for list operations + * Can be simple key-value pairs or complex filter conditions + */ +export interface ListFilter { + label?: string; + [key: string]: any; +} + +/** + * Sort options for list operations + */ +export interface ListSort { + [key: string]: boolean; +} + +/** + * Range specification for pagination/limiting results + */ +export interface ListRange { + type: 'tally'; + anchor: 'absolute' | 'relative'; + position: number; + tally: number; +} + diff --git a/src/types/entity.ts b/src/types/entity.ts new file mode 100644 index 0000000..248995e --- /dev/null +++ b/src/types/entity.ts @@ -0,0 +1,159 @@ +import type { ListFilter, ListRange, ListSort, SourceSelector } from './common'; +import type { IndividualInterface } from './individual'; +import type { OrganizationInterface } from './organization'; +import type { GroupInterface } from './group'; + +/** + * Entity-related type definitions for People Manager + */ + +/** + * Represents a person entity (contact) + */ +export interface EntityInterface { + '@type': string; + version: number; + in: string | number | null; + id: string | number | null; + createdOn: Date | null; + createdBy: string | null; + modifiedOn: Date | null; + modifiedBy: string | null; + signature: string | null; + data: IndividualInterface | OrganizationInterface | GroupInterface | null; +} + +/** + * Request to entity list endpoint + */ +export interface EntityListRequest { + sources?: SourceSelector; + filter?: ListFilter; + sort?: ListSort; + range?: ListRange; +} + +/** + * Response from entity list endpoint + */ +export interface EntityListResponse { + [providerId: string]: { + [serviceId: string]: { + [collectionId: string]: { + [entityId: string]: EntityInterface; + }; + }; + }; +} + +/** + * Request to entity delta endpoint + */ +export interface EntityDeltaRequest { + sources: SourceSelector; +} + +/** + * Response from entity delta endpoint + */ +export interface EntityDeltaResponse { + [providerId: string]: { + [serviceId: string]: { + [collectionId: string]: { + signature: string; + created?: { + [entityId: string]: EntityInterface; + }; + modified?: { + [entityId: string]: EntityInterface; + }; + deleted?: string[]; // Array of deleted entity IDs + }; + }; + }; +} + +/** + * Request to entity extant endpoint + */ +export interface EntityExtantRequest { + sources: SourceSelector; +} + +/** + * Response from entity extant endpoint + */ +export interface EntityExtantResponse { + [providerId: string]: { + [serviceId: string]: { + [collectionId: string]: { + [entityId: string]: boolean; + }; + }; + }; +} + +/** + * Request to entity fetch endpoint + */ +export interface EntityFetchRequest { + provider: string; + service: string; + collection: string | number; + identifiers: (string | number)[]; +} + +/** + * Response from entity fetch endpoint + */ +export interface EntityFetchResponse extends Record {} + +/** + * Request to entity create endpoint + */ +export interface EntityCreateRequest { + provider: string; + service: string; + collection: string | number; + data: EntityInterface; + options?: (string)[]; +} + +/** + * Response from entity create endpoint + */ +export interface EntityCreateResponse extends EntityInterface {} + +/** + * Request to entity modify endpoint + */ +export interface EntityModifyRequest { + provider: string; + service: string; + collection: string | number; + identifier: string | number; + data: EntityInterface; + options?: (string)[]; +} + +/** + * Response from entity modify endpoint + */ +export interface EntityModifyResponse extends EntityInterface {} + +/** + * Request to entity destroy endpoint + */ +export interface EntityDestroyRequest { + provider: string; + service: string; + collection: string | number; + identifier: string | number; +} + +/** + * Response from entity destroy endpoint + */ +export interface EntityDestroyResponse { + success: boolean; +} diff --git a/src/types/group.ts b/src/types/group.ts new file mode 100644 index 0000000..cca7339 --- /dev/null +++ b/src/types/group.ts @@ -0,0 +1,75 @@ +/** + * Group-related type definitions + */ + +/** + * Group name information + */ +export interface GroupName { + full: string | null; + sort: string | null; + aliases: string[]; +} + +/** + * Group member reference + */ +export interface GroupMember { + entityId: string | number | null; + role: string | null; + context: string | null; + priority: number | null; +} + +/** + * Group virtual location + */ +export interface GroupVirtualLocation { + location: string | null; + label: string | null; + context: string | null; + priority: number | null; +} + +/** + * Group media + */ +export interface GroupMedia { + type: string; + kind: string; // 'logo', 'photo' + uri: string; + mediaType?: string | null; + contexts?: string[] | null; + pref?: number | null; + label?: string | null; +} + +/** + * Group note + */ +export interface GroupNote { + content: string | null; + date: Date | null; + authorUri: string | null; + authorName: string | null; + context: string | null; + priority: number | null; +} + +/** + * Data for a group entity + */ +export interface GroupInterface { + type: string; + version: number; + urid: string | null; + created: Date | null; + modified: Date | null; + label: string | null; + names: GroupName; + members: Record; + virtualLocations: Record; + media: Record; + tags: string[]; + notes: Record; +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..34f147f --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,12 @@ +/** + * Central export point for all People Manager types + */ + +export type * from './collection'; +export type * from './common'; +export type * from './entity'; +export type * from './group'; +export type * from './individual'; +export type * from './organization'; +export type * from './provider'; +export type * from './service'; diff --git a/src/types/individual.ts b/src/types/individual.ts new file mode 100644 index 0000000..7867944 --- /dev/null +++ b/src/types/individual.ts @@ -0,0 +1,176 @@ +/** + * Serialized DateTime from backend + */ + +/** + * Individual alias + */ +export interface IndividualAlias { + label: string | null; + context: string | null; + priority: number | null; +} + +/** + * Individual name information + */ +export interface IndividualName { + family: string | null; + given: string | null; + additional: string | null; + prefix: string | null; + suffix: string | null; + phoneticFamily: string | null; + phoneticGiven: string | null; + phoneticAdditional: string | null; + aliases: IndividualAlias[]; +} + +/** + * Individual title + */ +export interface IndividualTitle { + kind: string | null; // 't' or 'r' + label: string | null; + relation: string | null; + context: string | null; + priority: number | null; +} + +/** + * Individual anniversary + */ +export interface IndividualAnniversary { + type: string | null; // 'birth', 'death', 'nuptial' + when: Date | null; + location: string | null; +} + +/** + * Individual physical location + */ +export interface IndividualPhysicalLocation { + box: string | null; + unit: string | null; + street: string | null; + locality: string | null; + region: string | null; + code: string | null; + country: string | null; + label: string | null; + coordinates: string | null; + timeZone: string | null; + context: string | null; + priority: number | null; +} + +/** + * Individual phone + */ +export interface IndividualPhone { + number: string | null; + label: string | null; + context: string | null; + priority: number | null; +} + +/** + * Individual email + */ +export interface IndividualEmail { + address: string | null; + context: string | null; + priority: number | null; +} + +/** + * Individual virtual location + */ +export interface IndividualVirtualLocation { + location: string | null; + label: string | null; + context: string | null; + priority: number | null; +} + +/** + * Individual organization + */ +export interface IndividualOrganization { + Label: string | null; + Units: string[]; + sortName: string | null; + context: string | null; + priority: number | null; +} + +/** + * Individual note + */ +export interface IndividualNote { + content: string | null; + date: Date | null; + authorUri: string | null; + authorName: string | null; + context: string | null; + priority: number | null; +} + +/** + * Individual language + */ +export interface IndividualLanguage { + Data: string | null; + Id: string | null; + Priority: number | null; + Context: string | null; +} + +/** + * Individual crypto + */ +export interface IndividualCrypto { + data: string | null; + type: string | null; + context: string | null; + priority: number | null; +} + +/** + * Individual media + */ +export interface IndividualMedia { + type: string; + kind: string; // 'photo', 'sound', 'logo' + uri: string; + mediaType?: string | null; + contexts?: string[] | null; + pref?: number | null; + label?: string | null; +} + +/** + * Data for an individual entity + */ +export interface IndividualInterface { + type: string; + version: number; + urid: string | null; + created: Date | null; + modified: Date | null; + label: string | null; + names: IndividualName; + titles: Record; + anniversaries: IndividualAnniversary[]; + physicalLocations: Record; + phones: Record; + emails: Record; + virtualLocations: Record; + media: Record; + organizations: Record; + tags: string[]; + notes: Record; + language: string | null; + languages: IndividualLanguage[]; + crypto: Record; +} diff --git a/src/types/organization.ts b/src/types/organization.ts new file mode 100644 index 0000000..08dc92c --- /dev/null +++ b/src/types/organization.ts @@ -0,0 +1,115 @@ +/** + * Organization-related type definitions + */ + +/** + * Organization name information + */ +export interface OrganizationName { + full: string | null; + sort: string | null; + aliases: string[]; +} + +/** + * Organization physical location + */ +export interface OrganizationPhysicalLocation { + box: string | null; + unit: string | null; + street: string | null; + locality: string | null; + region: string | null; + code: string | null; + country: string | null; + label: string | null; + coordinates: string | null; + timeZone: string | null; + context: string | null; + priority: number | null; +} + +/** + * Organization phone + */ +export interface OrganizationPhone { + number: string | null; + label: string | null; + context: string | null; + priority: number | null; +} + +/** + * Organization email + */ +export interface OrganizationEmail { + address: string | null; + context: string | null; + priority: number | null; +} + +/** + * Organization virtual location + */ +export interface OrganizationVirtualLocation { + location: string | null; + label: string | null; + context: string | null; + priority: number | null; +} + +/** + * Organization media + */ +export interface OrganizationMedia { + type: string; + kind: string; // 'logo', 'photo' + uri: string; + mediaType?: string | null; + contexts?: string[] | null; + pref?: number | null; + label?: string | null; +} + +/** + * Organization note + */ +export interface OrganizationNote { + content: string | null; + date: Date | null; + authorUri: string | null; + authorName: string | null; + context: string | null; + priority: number | null; +} + +/** + * Organization crypto + */ +export interface OrganizationCrypto { + data: string | null; + type: string | null; + context: string | null; + priority: number | null; +} + +/** + * Data for an organization entity + */ +export interface OrganizationInterface { + type: string; + version: number; + urid: string | null; + created: Date | null; + modified: Date | null; + label: string | null; + names: OrganizationName; + physicalLocations: Record; + phones: Record; + emails: Record; + virtualLocations: Record; + media: Record; + tags: string[]; + notes: Record; + crypto: Record; +} diff --git a/src/types/provider.ts b/src/types/provider.ts new file mode 100644 index 0000000..2db5be0 --- /dev/null +++ b/src/types/provider.ts @@ -0,0 +1,53 @@ +/** + * Provider-specific types + */ +import type { SourceSelector } from "./common"; + +/** + * Provider capabilities + */ +export interface ProviderCapabilitiesInterface { + ServiceList?: boolean; + ServiceFetch?: boolean; + ServiceExtant?: boolean; + ServiceCreate?: boolean; + ServiceModify?: boolean; + ServiceDelete?: boolean; + [key: string]: boolean | undefined; +} + +/** + * Provider information + */ +export interface ProviderInterface { + '@type': string; + id: string; + label: string; + capabilities: ProviderCapabilitiesInterface; +} + +/** + * Request to provider list endpoint + */ +export interface ProviderListRequest {} + +/** + * Response from provider list endpoint + */ +export interface ProviderListResponse { + [providerId: string]: ProviderInterface; +} + +/** + * Request to provider extant endpoint + */ +export interface ProviderExtantRequest { + sources: SourceSelector; +} + +/** + * Response from provider extant endpoint + */ +export interface ProviderExtantResponse { + [providerId: string]: boolean; +} diff --git a/src/types/service.ts b/src/types/service.ts new file mode 100644 index 0000000..6932a6b --- /dev/null +++ b/src/types/service.ts @@ -0,0 +1,211 @@ +/** + * Service-related type definitions for People Manager + */ + +import type { ListFilter, ListSort, SourceSelector } from "./common"; + +/** + * Filter comparison operators (bitmask values) + */ +export const FilterComparisonOperator = { + EQ: 1, // Equal + NEQ: 2, // Not Equal + GT: 4, // Greater Than + LT: 8, // Less Than + GTE: 16, // Greater Than or Equal + LTE: 32, // Less Than or Equal + IN: 64, // In Array + NIN: 128, // Not In Array + LIKE: 256, // Like (pattern matching) + NLIKE: 512, // Not Like +} as const; + +export type FilterComparisonOperator = typeof FilterComparisonOperator[keyof typeof FilterComparisonOperator]; + +/** + * Filter conjunction operators + */ +export const FilterConjunctionOperator = { + NONE: '', + AND: 'AND', + OR: 'OR', +} as const; + +export type FilterConjunctionOperator = typeof FilterConjunctionOperator[keyof typeof FilterConjunctionOperator]; + +/** + * Filter specification format + * Format: "type:length:defaultComparator:supportedComparators" + * + * Examples: + * - "s:200:256:771" = String field, max 200 chars, default LIKE, supports EQ|NEQ|LIKE|NLIKE + * - "a:10:64:192" = Array field, max 10 items, default IN, supports IN|NIN + * - "i:0:1:31" = Integer field, default EQ, supports EQ|NEQ|GT|LT|GTE|LTE + * + * Type codes: + * - s = string + * - i = integer + * - b = boolean + * - a = array + * + * Comparator values are bitmasks that can be combined + */ +export type FilterSpec = string; + +/** + * Parsed filter specification + */ +export interface ParsedFilterSpec { + type: 'string' | 'integer' | 'boolean' | 'array'; + length: number; + defaultComparator: FilterComparisonOperator; + supportedComparators: FilterComparisonOperator[]; +} + +/** + * Parse a filter specification string into its components + * + * @param spec - Filter specification string (e.g., "s:200:256:771") + * @returns Parsed filter specification object + * + * @example + * parseFilterSpec("s:200:256:771") + * // Returns: { + * // type: 'string', + * // length: 200, + * // defaultComparator: 256 (LIKE), + * // supportedComparators: [1, 2, 256, 512] (EQ, NEQ, LIKE, NLIKE) + * // } + */ +export function parseFilterSpec(spec: FilterSpec): ParsedFilterSpec { + const [typeCode, lengthStr, defaultComparatorStr, supportedComparatorsStr] = spec.split(':'); + + const typeMap: Record = { + 's': 'string', + 'i': 'integer', + 'b': 'boolean', + 'a': 'array', + }; + + const type = typeMap[typeCode]; + if (!type) { + throw new Error(`Invalid filter type code: ${typeCode}`); + } + + const length = parseInt(lengthStr, 10); + const defaultComparator = parseInt(defaultComparatorStr, 10) as FilterComparisonOperator; + + // Parse supported comparators from bitmask + const supportedComparators: FilterComparisonOperator[] = []; + const supportedBitmask = parseInt(supportedComparatorsStr, 10); + + if (supportedBitmask !== 0) { + const allComparators = Object.values(FilterComparisonOperator).filter(v => typeof v === 'number') as number[]; + for (const comparator of allComparators) { + if ((supportedBitmask & comparator) === comparator) { + supportedComparators.push(comparator as FilterComparisonOperator); + } + } + } + + return { + type, + length, + defaultComparator, + supportedComparators, + }; +} + +/** + * Capabilities available for a service + */ +export interface ServiceCapabilitiesInterface { + // Collection capabilities + CollectionList?: boolean; + CollectionListFilter?: { + [key: string]: FilterSpec; + }; + CollectionListSort?: string[]; + CollectionExtant?: boolean; + CollectionFetch?: boolean; + CollectionCreate?: boolean; + CollectionModify?: boolean; + CollectionDestroy?: boolean; + + // Entity capabilities + EntityList?: boolean; + EntityListFilter?: { + [key: string]: FilterSpec; + }; + EntityListSort?: string[]; + EntityListRange?: { + [rangeType: string]: string[]; // e.g., { "tally": ["absolute", "relative"] } + }; + EntityDelta?: boolean; + EntityExtant?: boolean; + EntityFetch?: boolean; + EntityCreate?: boolean; + EntityModify?: boolean; + EntityDestroy?: boolean; + EntityCopy?: boolean; + EntityMove?: boolean; +} + +/** + * Represents a service within a provider + */ +export interface ServiceInterface { + '@type': string; + provider: string; + id: string; + label: string; + capabilities?: ServiceCapabilitiesInterface; + enabled: boolean; +} + +/** + * Request to service list endpoint + */ +export interface ServiceListRequest { + sources?: SourceSelector; + filter?: ListFilter; + sort?: ListSort; +} + +/** + * Response from service list endpoint + */ +export interface ServiceListResponse { + [providerId: string]: { + [serviceId: string]: ServiceInterface; + }; +} + +/** + * Request to service extant endpoint + */ +export interface ServiceExtantRequest { + sources: SourceSelector; +} + +/** + * Response from service extant endpoint + */ +export interface ServiceExtantResponse { + [providerId: string]: { + [serviceId: string]: boolean; + }; +} + +/** + * Request to service fetch endpoint + */ +export interface ServiceFetchRequest { + provider: string; + service: string; +} + +/** + * Response from service fetch endpoint + */ +export interface ServiceFetchResponse extends ServiceInterface {} diff --git a/src/utils/key-generator.ts b/src/utils/key-generator.ts new file mode 100644 index 0000000..2422c71 --- /dev/null +++ b/src/utils/key-generator.ts @@ -0,0 +1,31 @@ +/** + * Utility functions for generating unique identifiers + */ + +const globalCrypto = typeof globalThis !== "undefined" ? globalThis.crypto : undefined; + +export const generateUrid = (): string => { + if (globalCrypto?.randomUUID) { + return globalCrypto.randomUUID(); + } + + const template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"; + return template.replace(/[xy]/g, char => { + const randomNibble = Math.floor(Math.random() * 16); + const value = char === "x" ? randomNibble : (randomNibble & 0x3) | 0x8; + return value.toString(16); + }); +}; + +export const generateKey = (): string => { + if (globalCrypto?.randomUUID) { + return globalCrypto.randomUUID().replace(/-/g, ""); + } + + if (globalCrypto?.getRandomValues) { + const [value] = globalCrypto.getRandomValues(new Uint32Array(1)); + return value.toString(16); + } + + return Math.random().toString(16).slice(2); +}; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..88d256c --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,19 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": ["./src/*"], + "@KTXC/*": ["../../core/src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..f85a399 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..43a7d67 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vite' +import path from 'path' + +// https://vite.dev/config/ +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@KTXC': path.resolve(__dirname, '../../core/src') + }, + }, + build: { + outDir: 'static', + sourcemap: true, + lib: { + entry: path.resolve(__dirname, 'src/main.ts'), + formats: ['es'], + fileName: () => 'module.mjs', + }, + rollupOptions: { + external: [ + 'pinia', + 'vue', + 'vue-router', + // Externalize shared utilities from core to avoid duplication + /^@KTXC\/utils\//, + ], + }, + }, +})