23 Commits

Author SHA1 Message Date
8093c031d9 chore(deps): update dependency phpunit/phpunit to v13
Some checks failed
renovate/artifacts Artifact file update failure
2026-05-19 03:03:48 +00:00
49712b5f87 Merge pull request 'refactor: service entity list and fetch' (#27) from refactor/entity-list-fetch into main
Some checks failed
Renovate / renovate (push) Failing after 1m22s
Reviewed-on: #27
2026-05-17 21:50:00 +00:00
86c93e8d3e refactor: service entity list and fetch
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-05-17 17:49:01 -04:00
94ca156fdb Merge pull request 'fix: merge conflicts' (#26) from fix/merge-conflicts into main
Some checks failed
Renovate / renovate (push) Failing after 1m34s
Reviewed-on: #26
2026-05-15 14:24:47 +00:00
5b513424a6 fix: merge conflicts
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-05-15 10:24:27 -04:00
55614b55f0 Merge pull request 'chore(deps): update dependency @vitejs/plugin-vue to v6.0.7' (#25) from renovate/vitejs-plugin-vue-6.x-lockfile into main
Reviewed-on: #25
2026-05-15 13:39:45 +00:00
766438c291 chore(deps): update dependency @vitejs/plugin-vue to v6.0.7 2026-05-15 13:39:35 +00:00
fd7004fe6a Merge pull request 'chore(deps): update dependency vue-tsc to v3.2.9' (#16) from renovate/vue-tsc-3.x-lockfile into main
Reviewed-on: #16
2026-05-15 03:36:04 +00:00
c6ffa02bdc Merge pull request 'chore(deps): update dependency vite to v8' (#21) from renovate/vite-8.x into main
Reviewed-on: #21
2026-05-15 03:35:54 +00:00
48a3e33a81 chore(deps): update dependency vite to v8 2026-05-15 03:35:39 +00:00
98de03bd39 chore(deps): update dependency vue-tsc to v3.2.9 2026-05-15 03:35:36 +00:00
53b9368e48 Merge pull request 'chore(deps): update dependency typescript to v6' (#20) from renovate/typescript-6.x into main
Reviewed-on: #20
2026-05-15 03:34:12 +00:00
76168bda18 Merge pull request 'fix(deps): update dependency vue-router to v5' (#23) from renovate/vue-router-5.x into main
Reviewed-on: #23
2026-05-15 03:34:00 +00:00
adcbbc34f9 fix(deps): update dependency vue-router to v5 2026-05-15 03:33:42 +00:00
b455e07e86 chore(deps): update dependency typescript to v6 2026-05-15 03:33:36 +00:00
bed8652bd9 Merge pull request 'chore(deps): update dependency @vitejs/plugin-vue to v6.0.6' (#12) from renovate/vitejs-plugin-vue-6.x-lockfile into main
Reviewed-on: #12
2026-05-15 03:07:14 +00:00
420ed06020 Merge pull request 'fix(deps): update dependency pinia to v3' (#22) from renovate/pinia-3.x into main
Reviewed-on: #22
2026-05-15 03:06:58 +00:00
cc291fa86f Merge pull request 'fix(deps): update dependency vuetify to v4' (#24) from renovate/vuetify-4.x into main
Reviewed-on: #24
2026-05-15 03:06:42 +00:00
396210352a Merge pull request 'chore(deps): update dependency @vue/tsconfig to ^0.9.0' (#19) from renovate/vue-tsconfig-0.x into main
Reviewed-on: #19
2026-05-15 03:06:27 +00:00
d31d21d9e4 fix(deps): update dependency vuetify to v4 2026-05-15 03:05:30 +00:00
ab88864327 fix(deps): update dependency pinia to v3 2026-05-15 03:05:25 +00:00
f691c21e1a chore(deps): update dependency @vue/tsconfig to ^0.9.0 2026-05-15 03:05:16 +00:00
631819c282 chore(deps): update dependency @vitejs/plugin-vue to v6.0.6 2026-05-15 03:05:06 +00:00
6 changed files with 1227 additions and 919 deletions

View File

@@ -23,7 +23,7 @@
"doctrine/lexer": "^3.0" "doctrine/lexer": "^3.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.0" "phpunit/phpunit": "^13.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View File

@@ -28,9 +28,9 @@ final class StoreCommand implements CommandInterface
*/ */
public function __construct( public function __construct(
FetchTarget|string|SequenceSet|null $target = null, FetchTarget|string|SequenceSet|null $target = null,
private readonly array $flags = [], private array $flags = [],
private readonly string $action = '', private string $action = '',
private readonly bool $silent = true, private bool $silent = true,
) { ) {
$resolvedTarget = match (true) { $resolvedTarget = match (true) {
$target instanceof FetchTarget => $target, $target instanceof FetchTarget => $target,

View File

@@ -27,7 +27,6 @@ use KTXM\ProviderImap\Stores\ServiceStore;
class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface, ProviderServiceDiscoverInterface, ProviderServiceTestInterface class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface, ProviderServiceDiscoverInterface, ProviderServiceTestInterface
{ {
public const JSON_TYPE = ProviderBaseInterface::JSON_TYPE;
protected const PROVIDER_IDENTIFIER = 'imap'; protected const PROVIDER_IDENTIFIER = 'imap';
protected const PROVIDER_LABEL = 'IMAP Mail Provider'; protected const PROVIDER_LABEL = 'IMAP Mail Provider';
protected const PROVIDER_DESCRIPTION = 'Provides mail services via the IMAP protocol'; protected const PROVIDER_DESCRIPTION = 'Provides mail services via the IMAP protocol';

View File

@@ -11,7 +11,6 @@ namespace KTXM\ProviderImap\Providers;
use Generator; use Generator;
use KTXF\Mail\Collection\CollectionBaseInterface; use KTXF\Mail\Collection\CollectionBaseInterface;
use KTXF\Mail\Collection\CollectionMutableInterface;
use KTXF\Mail\Collection\CollectionPropertiesBaseInterface; use KTXF\Mail\Collection\CollectionPropertiesBaseInterface;
use KTXF\Mail\Object\Address; use KTXF\Mail\Object\Address;
use KTXF\Mail\Object\AddressInterface; use KTXF\Mail\Object\AddressInterface;
@@ -40,7 +39,7 @@ use KTXM\ProviderImap\Service\Remote\RemoteService;
use KTXM\ProviderImap\Providers\CollectionResource; use KTXM\ProviderImap\Providers\CollectionResource;
use KTXF\Mail\Collection\CollectionRoles; use KTXF\Mail\Collection\CollectionRoles;
use KTXF\Mail\Object\MessagePropertiesMutableInterface; use KTXF\Mail\Object\MessagePropertiesMutableInterface;
use KTXF\Mail\Service\ServiceEntityMutableInterface; use KTXF\Resource\Identifier\EntityIdentifierInterface;
use KTXM\ProviderImap\Providers\EntityResource; use KTXM\ProviderImap\Providers\EntityResource;
/** /**
@@ -499,7 +498,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
} }
$result = match ($deleteMode) { $result = match ($deleteMode) {
'soft' => $this->collectionMove($target, new CollectionIdentifier($target->provider(), $target->service(), $deleteTarget)), 'soft' => $this->collectionMove(new CollectionIdentifier($target->provider(), $target->service(), $deleteTarget), $target),
'hard' => $this->mailService->collectionDestroy((string) $target->collection()), 'hard' => $this->mailService->collectionDestroy((string) $target->collection()),
}; };
return $result; return $result;
@@ -521,8 +520,9 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
$sourceDelimiter = $sourceMailbox->delimiter() ?? '/'; $sourceDelimiter = $sourceMailbox->delimiter() ?? '/';
$targetDelimiter = $targetMailbox->delimiter() ?? '/'; $targetDelimiter = $targetMailbox->delimiter() ?? '/';
$targetPath = rtrim($targetMailbox->name(), $targetDelimiter) . $targetDelimiter . end(explode($sourceDelimiter, $sourceMailbox->name())); $extantPath = $sourceMailbox->name();
$mutatedMailbox = $this->mailService->collectionRename($sourceMailbox->name(), $targetPath); $freshPath = rtrim($targetMailbox->name(), $targetDelimiter) . $targetDelimiter . end(explode($sourceDelimiter, $extantPath));
$mutatedMailbox = $this->mailService->collectionRename($extantPath, $freshPath);
$collection = $this->collectionFresh(); $collection = $this->collectionFresh();
$collection->fromImap($mutatedMailbox, ['delimiter' => $targetDelimiter]); $collection->fromImap($mutatedMailbox, ['delimiter' => $targetDelimiter]);
@@ -531,9 +531,9 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
// ── Entity operations ───────────────────────────────────────────────────── // ── Entity operations ─────────────────────────────────────────────────────
public function entityList(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array public function entityListBulk(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array
{ {
return iterator_to_array($this->entityList((string) $collection, $filter, $sort, $range), true); return iterator_to_array($this->entityListStream((string) $collection, $filter, $sort, $range), true);
} }
public function entityListStream(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): Generator public function entityListStream(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): Generator
@@ -543,7 +543,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
foreach ($this->mailService->entityList((string) $collection, $filter, $sort, $range) as $identifier => $message) { foreach ($this->mailService->entityList((string) $collection, $filter, $sort, $range) as $identifier => $message) {
$resource = $this->entityFresh(); $resource = $this->entityFresh();
$resource->fromImap($message, $collection); $resource->fromImap($message, $collection);
yield $identifier => $resource; yield $resource->urn() => $resource;
} }
} }
@@ -565,12 +565,25 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
}; };
} }
public function entityFetch(string|int $collection, string|int ...$identifiers): array public function entityFetchBulk(EntityIdentifierInterface ...$identifiers): array
{
return iterator_to_array($this->entityFetchStream(...$identifiers), true);
}
public function entityFetchStream(EntityIdentifierInterface ...$identifiers): Generator
{ {
$this->initialize(); $this->initialize();
$uids = array_map('intval', $identifiers); $identifiers = $this->groupEntitiesByCollection(...$identifiers);
return $this->mailService->entityFetch((string) $collection, ...$uids);
foreach ($identifiers as $collection => $entities) {
$uids = array_keys($entities);
foreach ($this->mailService->entityFetch((string) $collection, ...$uids) as $uid => $message) {
$resource = $this->entityFresh();
$resource->fromImap($message, $collection);
yield $resource->urn() => $resource;
}
}
} }
public function entityDelta(string|int $collection, string $signature, string $detail = 'ids'): Delta public function entityDelta(string|int $collection, string $signature, string $detail = 'ids'): Delta
@@ -606,6 +619,29 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
throw new \RuntimeException('Entity modification is not supported in this service'); throw new \RuntimeException('Entity modification is not supported in this service');
} }
public function entityPatch(MessagePropertiesMutableInterface $properties, EntityIdentifier ...$targets): array
{
// validate identifiers and group by collection
$targets = $this->groupEntitiesByCollection(...$targets);
// move entities on remote store and construct result map
$this->initialize();
$list = [];
foreach ($targets as $targetCollection => $targetIdentifiers) {
$uids = array_keys($targetIdentifiers);
$mutations = $this->mailService->entityPatch($targetCollection, $properties, ...$uids);
foreach ($uids as $uid) {
$list[(string)$targetIdentifiers[$uid]] = ['disposition' => 'patched'];
}
}
return $list;
}
public function entityDelete(EntityIdentifier ...$targets): array public function entityDelete(EntityIdentifier ...$targets): array
{ {
// validate identifiers and group by collection // validate identifiers and group by collection
@@ -644,6 +680,11 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
$deleteTargetIdentifier = new CollectionIdentifier($this->provider(), (string) $this->identifier(), $deleteTargetNative); $deleteTargetIdentifier = new CollectionIdentifier($this->provider(), (string) $this->identifier(), $deleteTargetNative);
} }
// if all targets are already in the delete target collection, we should hard delete instead of moving to avoid duplicates in the trash
if (array_keys($targets) === [$deleteTargetNative]) {
$deleteMode = 'hard';
}
// entities need to be moved or deleted by collection // entities need to be moved or deleted by collection
$list = []; $list = [];
foreach ($targets as $sourceCollection => $sourceEntities) { foreach ($targets as $sourceCollection => $sourceEntities) {
@@ -659,11 +700,11 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
}; };
foreach ($uids as $uid) { foreach ($uids as $uid) {
$mutatedUid = $mutations[$uid] ?? null; $mutatedUid = !isset($mutations[$uid]) || $mutations[$uid] === true ? null : $mutations[$uid];
$list[(string)$sourceEntities[$uid]] = [ $list[(string)$sourceEntities[$uid]] = [
'disposition' => $deleteMode === 'soft' ? 'moved' : 'deleted', 'disposition' => $deleteMode === 'soft' ? 'moved' : 'deleted',
'destination' => $deleteMode === 'soft' ? $deleteTargetIdentifier : null, 'destination' => $deleteMode === 'soft' ? $deleteTargetIdentifier : null,
'mutation' => $mutatedUid !== null ? new EntityIdentifier($this->provider(), $this->identifier(), $deleteTargetIdentifier->collection(), $mutatedUid) : null, 'mutation' => $deleteMode === 'soft' && $mutatedUid !== null ? new EntityIdentifier($this->provider(), $this->identifier(), $deleteTargetIdentifier->collection(), $mutatedUid) : null,
]; ];
} }
} }
@@ -671,34 +712,6 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
return $list; return $list;
} }
public function entityPatch(MessagePropertiesMutableInterface $properties, EntityIdentifier ...$targets): array
{
throw new \RuntimeException('Entity patching is not supported in this service');
}
public function entityPatch(MessagePropertiesMutableInterface $properties, EntityIdentifier ...$targets): array
{
// validate identifiers and group by collection
$targets = $this->groupEntitiesByCollection(...$targets);
// move entities on remote store and construct result map
$this->initialize();
$list = [];
foreach ($targets as $targetCollection => $targetIdentifiers) {
$uids = array_keys($targetIdentifiers);
$mutations = $this->mailService->entityPatch($targetCollection, $properties, ...$uids);
foreach ($uids as $uid) {
$list[(string)$targetIdentifiers[$uid]] = ['disposition' => 'patched'];
}
}
return $list;
}
public function entityCopy(CollectionIdentifier $target, EntityIdentifier ...$sources): array public function entityCopy(CollectionIdentifier $target, EntityIdentifier ...$sources): array
{ {
// validate target belongs to this service // validate target belongs to this service
@@ -765,11 +778,6 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
return $list; return $list;
} }
public function entityCopy(CollectionIdentifier $target, EntityIdentifier ...$sources): array
{
throw new \RuntimeException('Entity copying is not supported in this service');
}
private function groupEntitiesByCollection(EntityIdentifier ...$identifiers): array private function groupEntitiesByCollection(EntityIdentifier ...$identifiers): array
{ {
$list = []; $list = [];

2025
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,16 +14,16 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"pinia": "^2.3.1", "pinia": "^3.0.0",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^4.5.1", "vue-router": "^5.0.0",
"vuetify": "^3.10.2" "vuetify": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.9.0",
"typescript": "~5.8.3", "typescript": "~6.0.0",
"vite": "^7.1.2", "vite": "^8.0.0",
"vue-tsc": "^3.0.5" "vue-tsc": "^3.0.5"
} }
} }