refactor: use new documents interfaces

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-09 19:39:29 -04:00
parent a3566405d0
commit 24d5410a09
7 changed files with 337 additions and 333 deletions
+278 -273
View File
@@ -9,9 +9,23 @@ declare(strict_types=1);
namespace KTXM\ProviderLocalDocuments\Providers\Personal;
use Generator;
use KTXF\Blob\Signature;
use KTXF\Documents\Collection\CollectionBaseInterface;
use KTXF\Documents\Collection\CollectionPropertiesBaseInterface;
use KTXF\Documents\Entity\EntityPropertiesMutableInterface;
use KTXF\Documents\Service\ServiceBaseInterface;
use KTXF\Documents\Service\ServiceCollectionMutableInterface;
use KTXF\Documents\Service\ServiceEntityMutableInterface;
use KTXF\Files\Node\NodeType;
use KTXF\Resource\Delta\Delta;
use KTXF\Resource\Exceptions\InvalidParameterException;
use KTXF\Resource\Filter\Filter;
use KTXF\Resource\Filter\IFilter;
use KTXF\Resource\Identifier\CollectionIdentifier;
use KTXF\Resource\Identifier\CollectionIdentifierInterface;
use KTXF\Resource\Identifier\EntityIdentifier;
use KTXF\Resource\Identifier\EntityIdentifierInterface;
use KTXF\Resource\Range\IRange;
use KTXF\Resource\Range\RangeTally;
use KTXF\Resource\Range\RangeType;
@@ -19,15 +33,6 @@ use KTXF\Resource\Sort\ISort;
use KTXF\Resource\Sort\Sort;
use KTXM\ProviderLocalDocuments\Store\BlobStore;
use KTXM\ProviderLocalDocuments\Store\MetaStore;
use KTXF\Blob\Signature;
use KTXF\Files\Node\NodeType;
use KTXF\Resource\Delta\Delta;
use KTXF\Resource\Documents\Collection\CollectionMutableInterface;
use KTXF\Resource\Documents\Entity\EntityMutableInterface;
use KTXF\Resource\Documents\Service\ServiceBaseInterface;
use KTXF\Resource\Documents\Service\ServiceCollectionMutableInterface;
use KTXF\Resource\Documents\Service\ServiceEntityMutableInterface;
use KTXM\ProviderLocalDocuments\Providers\Personal\CollectionResource;
class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface {
@@ -58,6 +63,8 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
self::CAPABILITY_COLLECTION_CREATE => true,
self::CAPABILITY_COLLECTION_UPDATE => true,
self::CAPABILITY_COLLECTION_DELETE => true,
self::CAPABILITY_COLLECTION_MOVE => true,
self::CAPABILITY_COLLECTION_COPY => true,
self::CAPABILITY_ENTITY_LIST => true,
self::CAPABILITY_ENTITY_LIST_FILTER => [
self::CAPABILITY_ENTITY_FILTER_ALL => 's:200:256:256',
@@ -74,14 +81,15 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
self::CAPABILITY_ENTITY_RANGE_TALLY_ABSOLUTE,
self::CAPABILITY_ENTITY_RANGE_TALLY_RELATIVE
],
self::CAPABILITY_ENTITY_RANGE_DATE => true,
],
self::CAPABILITY_ENTITY_DELTA => true,
self::CAPABILITY_ENTITY_EXTANT => true,
self::CAPABILITY_ENTITY_FETCH => true,
self::CAPABILITY_ENTITY_CREATE => true,
self::CAPABILITY_ENTITY_UPDATE => true,
self::CAPABILITY_ENTITY_MODIFY => true,
self::CAPABILITY_ENTITY_DELETE => true,
self::CAPABILITY_ENTITY_MOVE => true,
self::CAPABILITY_ENTITY_COPY => true,
self::CAPABILITY_ENTITY_READ => true,
self::CAPABILITY_ENTITY_WRITE => true,
];
@@ -114,18 +122,18 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
return $this;
}
public function jsonSerialize(): array {
return array_filter([
self::JSON_PROPERTY_TYPE => self::JSON_TYPE,
self::JSON_PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER,
self::JSON_PROPERTY_IDENTIFIER => self::SERVICE_IDENTIFIER,
self::JSON_PROPERTY_LABEL => self::SERVICE_LABEL,
self::JSON_PROPERTY_ENABLED => $this->serviceEnabled,
self::JSON_PROPERTY_CAPABILITIES => $this->serviceAbilities,
self::JSON_PROPERTY_LOCATION => null,
self::JSON_PROPERTY_IDENTITY => null,
self::JSON_PROPERTY_AUXILIARY => [],
self::PROPERTY_TYPE => self::JSON_TYPE,
self::PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER,
self::PROPERTY_IDENTIFIER => self::SERVICE_IDENTIFIER,
self::PROPERTY_LABEL => self::SERVICE_LABEL,
self::PROPERTY_ENABLED => $this->serviceEnabled,
self::PROPERTY_CAPABILITIES => $this->serviceAbilities,
self::PROPERTY_LOCATION => null,
self::PROPERTY_IDENTITY => null,
self::PROPERTY_AUXILIARY => [],
], fn($v) => $v !== null);
}
@@ -173,9 +181,17 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
return [];
}
/**
* Normalize a collection location, treating null and '' as the root collection
*/
private function normalizeLocation(string|int|null $location): string|int {
return ($location === null || $location === '') ? self::ROOT_ID : $location;
}
// Collection operations
public function collectionList(string|int|null $location = null, ?IFilter $filter = null, ?ISort $sort = null): array {
public function collectionList(string|int|null $location, ?IFilter $filter = null, ?ISort $sort = null): array {
$location = $this->normalizeLocation($location);
$entries = $this->metaStore->collectionList($this->serviceTenantId, $this->serviceUserId, $location, $filter, $sort);
// cache collections
foreach ($entries as $id => $collection) {
@@ -209,12 +225,18 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
return array_merge($cached, $fromStore);
}
/**
* Determine whether a single collection exists and belongs to the current user
*/
private function collectionAccessible(string|int $collection): bool {
$response = $this->collectionExtant(null, $collection);
return ($response[$collection] ?? false) === true;
}
public function collectionFetch(string|int|null $identifier): ?CollectionResource {
// null is root
if ($identifier === null) {
$identifier = self::ROOT_ID;
}
// check cache first
$identifier = $this->normalizeLocation($identifier);
// check cache first
if (isset($this->serviceCollectionCache[$identifier])) {
return $this->serviceCollectionCache[$identifier];
}
@@ -226,7 +248,7 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
}
return null;
}
public function collectionFresh(): CollectionResource {
$collection = new CollectionResource();
$collection->fromStore([
@@ -242,11 +264,27 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
return $collection;
}
public function collectionCreate(string|int|null $location, CollectionMutableInterface $collection, array $options = []): CollectionResource {
// null is root
if ($location === null) {
$location = self::ROOT_ID;
/**
* Convert incoming collection properties into a native CollectionResource
*/
private function collectionNative(CollectionPropertiesBaseInterface $properties, string|int|null $identifier = null): CollectionResource {
if ($properties instanceof CollectionResource) {
$native = clone $properties;
} else {
$native = $this->collectionFresh();
$native->jsonDeserialize([
CollectionResource::PROPERTY_IDENTIFIER => $identifier,
CollectionResource::PROPERTY_PROPERTIES => $properties->jsonSerialize(),
]);
}
return $native;
}
public function collectionCreate(CollectionIdentifierInterface|null $target, CollectionPropertiesBaseInterface $properties, array $options = []): CollectionResource {
// null target is root
$location = $this->normalizeLocation($target?->collection());
// convert properties to a native collection
$collection = $this->collectionNative($properties);
// Create in meta store
$node = $this->metaStore->collectionCreate($this->serviceTenantId, $this->serviceUserId, $location, $collection, $options);
// cache collection
@@ -254,23 +292,27 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
return $node;
}
public function collectionUpdate(string|int $identifier, CollectionMutableInterface $collection): CollectionResource {
public function collectionUpdate(CollectionIdentifierInterface $target, CollectionPropertiesBaseInterface $properties): CollectionResource {
$identifier = $target->collection();
// convert properties to a native collection
$collection = $this->collectionNative($properties, $identifier);
// Modify in meta store
$node = $this->metaStore->collectionModify($this->serviceTenantId, $this->serviceUserId, $identifier, $collection);
// update cache
$this->serviceCollectionCache[$node->identifier()] = $node;
return $node;
}
public function collectionDelete(string|int $identifier, bool $force = false, bool $recursive = false): bool {
public function collectionDelete(CollectionIdentifierInterface $target, bool $force = false): CollectionBaseInterface | true {
$identifier = $target->collection();
// Protect root collection
if ($identifier === self::ROOT_ID) {
throw new InvalidParameterException("Cannot destroy root collection");
}
// If not forcing and not recursive, ensure the collection is empty
if (!$force && !$recursive) {
// If not forcing, ensure the collection is empty
if (!$force) {
$children = $this->metaStore->nodeList($this->serviceTenantId, $this->serviceUserId, $identifier, false);
if (!empty($children)) {
throw new InvalidParameterException("Collection is not empty: $identifier");
@@ -280,34 +322,32 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
// Delete blob files for all entities within the collection tree
$descendants = $this->metaStore->nodeList($this->serviceTenantId, $this->serviceUserId, $identifier, true);
foreach ($descendants as $nodeId => $node) {
if ($node->isEntity()) {
if ($node instanceof EntityResource) {
$this->blobStore->blobDelete((string)$nodeId);
}
}
// Delete from meta store (handles recursive collection/entity meta deletion internally)
$result = $this->metaStore->collectionDestroy($this->serviceTenantId, $this->serviceUserId, $identifier);
$this->metaStore->collectionDestroy($this->serviceTenantId, $this->serviceUserId, $identifier);
// remove from cache
unset($this->serviceCollectionCache[$identifier]);
return $result;
return true;
}
public function collectionCopy(string|int $identifier, string|int|null $location): CollectionResource {
public function collectionCopy(CollectionIdentifierInterface|null $target, CollectionIdentifierInterface $source): CollectionResource {
$identifier = $source->collection();
// Protect root collection
if ($identifier === self::ROOT_ID) {
throw new InvalidParameterException("Cannot copy root collection");
}
// Verify collection exists
$extant = $this->collectionExtant($identifier, $identifier);
if (!($extant[$identifier] ?? false)) {
if ($this->collectionAccessible($identifier) === false) {
throw new InvalidParameterException("Collection not found: $identifier");
}
// null location is root
if ($location === null) {
$location = self::ROOT_ID;
}
// null target is root
$location = $this->normalizeLocation($target?->collection());
// Copy in meta store
$node = $this->metaStore->collectionCopy($this->serviceTenantId, $this->serviceUserId, $identifier, $location);
@@ -316,24 +356,22 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
return $node;
}
public function collectionMove(string|int $identifier, string|int|null $location): CollectionResource {
public function collectionMove(CollectionIdentifierInterface|null $target, CollectionIdentifierInterface $source): CollectionResource {
$identifier = $source->collection();
// Protect root collection
if ($identifier === self::ROOT_ID) {
throw new InvalidParameterException("Cannot move root collection");
}
// Verify collection exists
$extant = $this->collectionExtant($identifier, $identifier);
if (!($extant[$identifier] ?? false)) {
if ($this->collectionAccessible($identifier) === false) {
throw new InvalidParameterException("Collection not found: $identifier");
}
// null location is root
if ($location === null) {
$location = self::ROOT_ID;
}
// null target is root
$location = $this->normalizeLocation($target?->collection());
// Move in meta store
$node = $this->metaStore->collectionMove($this->serviceTenantId, $this->serviceUserId, $identifier, $location);
// update cache
$this->serviceCollectionCache[$node->identifier()] = $node;
return $node;
@@ -341,20 +379,22 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
// Entity operations
public function entityList(string|int|null $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array {
public function entityListBulk(string|int|null $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
$collection = $this->normalizeLocation($collection);
$entries = $this->metaStore->entityList($this->serviceTenantId, $this->serviceUserId, $collection, $filter, $sort, $range);
// cache entities
foreach ($entries as $id => $entity) {
foreach ($entries as $entity) {
$this->serviceEntityCache[$entity->identifier()] = $entity;
}
return $entries ?? [];
}
public function entityListStream(string|int|null $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): Generator {
yield from $this->entityListBulk($collection, $filter, $sort, $range, $properties);
}
public function entityListFilter(): IFilter {
return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []);
}
@@ -370,41 +410,37 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
return new RangeTally();
}
public function entityFetch(string|int|null $collection, string|int ...$identifiers): array {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
public function entityFetchBulk(EntityIdentifierInterface ...$identifiers): array {
$result = [];
$toFetch = [];
// check cache first
foreach ($identifiers as $id) {
// group identifiers by collection
$byCollection = [];
foreach ($identifiers as $identifier) {
$id = $identifier->entity();
// check cache first
if (isset($this->serviceEntityCache[$id])) {
$result[$id] = $this->serviceEntityCache[$id];
} else {
$toFetch[] = $id;
continue;
}
$byCollection[$this->normalizeLocation($identifier->collection())][] = $id;
}
// fetch remaining from store
if (!empty($toFetch)) {
$fetched = $this->metaStore->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, ...$toFetch);
foreach ($byCollection as $collection => $entityIds) {
$fetched = $this->metaStore->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, ...$entityIds);
foreach ($fetched as $id => $entity) {
$this->serviceEntityCache[$id] = $entity;
$result[$id] = $entity;
}
}
return $result;
}
public function entityFetchStream(EntityIdentifierInterface ...$identifiers): Generator {
yield from $this->entityFetchBulk(...$identifiers);
}
public function entityExtant(string|int|null $collection, string|int ...$identifiers): array {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
$collection = $this->normalizeLocation($collection);
$result = [];
foreach ($identifiers as $id) {
@@ -427,202 +463,209 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
'uid' => $this->serviceUserId,
'cid' => null,
'nid' => null,
'created' => (new \DateTimeImmutable())->format('c'),
'created' => (int) round(microtime(true) * 1000),
'properties' => [],
]);
return $entity;
}
public function entityCreate(string|int|null $collection, EntityMutableInterface $entity, array $options = []): EntityResource {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
/**
* Convert incoming entity properties into a native EntityResource
*/
private function entityNative(EntityPropertiesMutableInterface $properties, string|int|null $identifier = null): EntityResource {
if ($properties instanceof EntityResource) {
$native = clone $properties;
} else {
$native = $this->entityFresh();
$native->jsonDeserialize([
EntityResource::PROPERTY_IDENTIFIER => $identifier,
EntityResource::PROPERTY_PROPERTIES => $properties->jsonSerialize(),
]);
}
return $native;
}
public function entityCreate(CollectionIdentifierInterface $target, EntityPropertiesMutableInterface $properties, array $options = []): EntityResource {
// null collection is root
$collection = $this->normalizeLocation($target->collection());
// convert properties to a native entity
$entity = $this->entityNative($properties);
// Create in meta store
$result = $this->metaStore->entityCreate($this->serviceTenantId, $this->serviceUserId, $collection, $entity, $options);
// Write meta file for recovery
$this->blobStore->metaWrite((string)$result->identifier(), $this->buildEntityMeta($result));
// cache entity
$this->serviceEntityCache[$result->identifier()] = $result;
return $result;
}
public function entityUpdate(string|int|null $collection, string|int $identifier, EntityMutableInterface $entity): EntityResource {
public function entityModify(EntityIdentifierInterface $target, EntityPropertiesMutableInterface $properties): EntityResource {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
$collection = $this->normalizeLocation($target->collection());
$identifier = $target->entity();
// convert properties to a native entity
$entity = $this->entityNative($properties, $identifier);
// Modify in meta store
$result = $this->metaStore->entityModify($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $entity);
// Update meta file for recovery
$this->blobStore->metaWrite((string)$result->identifier(), $this->buildEntityMeta($result));
// update cache
$this->serviceEntityCache[$result->identifier()] = $result;
return $result;
}
public function entityDelete(string|int|null $collection, string|int $identifier): EntityResource {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
public function entityDelete(EntityIdentifierInterface ...$targets): array {
$results = [];
foreach ($targets as $target) {
$key = (string) $target;
$collection = $this->normalizeLocation($target->collection());
$identifier = $target->entity();
// validate entity extant and ownership
$extant = $this->entityExtant($collection, $identifier);
if (($extant[$identifier] ?? false) === false) {
$results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $target];
continue;
}
// Delete from blob store
$this->blobStore->blobDelete((string)$identifier);
// Delete from meta store
$this->metaStore->entityDestroy($this->serviceTenantId, $this->serviceUserId, $collection, $identifier);
// remove from cache
unset($this->serviceEntityCache[$identifier]);
$results[$key] = ['disposition' => 'deleted', 'destination' => null, 'mutation' => $target];
}
/** @var EntityResource[] $entities */
$entities = $this->entityFetch($collection, $identifier);
if (!isset($entities[$identifier])) {
throw new InvalidParameterException("Entity not found: $identifier");
}
$entity = $entities[$identifier];
// Delete from blob store
$this->blobStore->blobDelete($entity->identifier());
// Delete from meta store
$this->metaStore->entityDestroy($this->serviceTenantId, $this->serviceUserId, $collection, $identifier);
// remove from cache
unset($this->serviceEntityCache[$entity->identifier()]);
return $entity;
return $results;
}
public function entityDelta(string|int|null $collection, string $signature, string $detail = 'ids'): Delta {
public function entityDelta(string|int|null $collection, string $signature): Delta {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
return new Delta(); // $this->metaStore->entityDelta($this->serviceTenantId, $this->serviceUserId, $collection, $signature, $detail);
$collection = $this->normalizeLocation($collection);
return new Delta(); // $this->metaStore->entityDelta($this->serviceTenantId, $this->serviceUserId, $collection, $signature);
}
public function entityCopy(string|int|null $collection, string|int $identifier, string|int|null $destination): EntityResource {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
// null destination is root
if ($destination === null) {
$destination = self::ROOT_ID;
}
public function entityMove(CollectionIdentifierInterface $target, EntityIdentifierInterface ...$sources): array {
return $this->entityRelocate($target, true, ...$sources);
}
// Verify entity exists
public function entityCopy(CollectionIdentifierInterface $target, EntityIdentifierInterface ...$sources): array {
return $this->entityRelocate($target, false, ...$sources);
}
/**
* Relocate (move or copy) entities into a target collection.
*
* Moves retain the entity identifier (blob path is keyed by entity id and never
* changes); copies receive a fresh identifier and a duplicated blob.
*
* @param CollectionIdentifierInterface $target destination collection identifier
* @param bool $remove true to move (relocate the source), false to copy
* @param EntityIdentifierInterface ...$sources source entity identifiers
*
* @return array<string, array{disposition: string, destination: ?CollectionIdentifierInterface, mutation: EntityIdentifierInterface}>
*/
private function entityRelocate(CollectionIdentifierInterface $target, bool $remove, EntityIdentifierInterface ...$sources): array {
$results = [];
$disposition = $remove ? 'moved' : 'copied';
$targetCollection = $this->normalizeLocation($target->collection());
// validate target collection extant and ownership
$targetValid = $targetCollection === self::ROOT_ID || $this->collectionAccessible($targetCollection);
foreach ($sources as $source) {
$key = (string) $source;
$sourceCollection = $this->normalizeLocation($source->collection());
$identifier = $source->entity();
// validate target collection
if ($targetValid === false) {
$results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $source];
continue;
}
// validate source entity extant and ownership
$extant = $this->entityExtant($sourceCollection, $identifier);
if (($extant[$identifier] ?? false) === false) {
$results[$key] = ['disposition' => 'error', 'destination' => null, 'mutation' => $source];
continue;
}
if ($remove) {
// Move in meta store (blob path never changes since it is keyed by entity id)
$node = $this->metaStore->entityMove($this->serviceTenantId, $this->serviceUserId, $sourceCollection, $identifier, $targetCollection);
} else {
// Copy in meta store (creates new entity with new id)
$node = $this->metaStore->entityCopy($this->serviceTenantId, $this->serviceUserId, $sourceCollection, $identifier, $targetCollection);
// Copy blob content (new entity has new id = new file)
$content = $this->blobStore->blobRead((string)$identifier);
if ($content !== null) {
$this->blobStore->blobWrite((string)$node->identifier(), $content);
}
}
// Write meta file for recovery
$this->blobStore->metaWrite((string)$node->identifier(), $this->buildEntityMeta($node));
// cache entity
$this->serviceEntityCache[$node->identifier()] = $node;
// construct destination and mutation identifiers
$destination = new CollectionIdentifier($this->provider(), $this->identifier(), (string)$targetCollection);
$mutation = new EntityIdentifier($this->provider(), $this->identifier(), (string)$targetCollection, (string)$node->identifier());
$results[$key] = ['disposition' => $disposition, 'destination' => $destination, 'mutation' => $mutation];
}
return $results;
}
// Entity content (blob) operations
/**
* Confirm an entity exists, returning its normalized collection identifier
*/
private function entityAccessible(EntityIdentifierInterface $target): string|int|false {
$collection = $this->normalizeLocation($target->collection());
$identifier = $target->entity();
$extant = $this->entityExtant($collection, $identifier);
if (!($extant[$identifier] ?? false)) {
throw new InvalidParameterException("Entity not found: $identifier");
}
// Copy in meta store (creates new entity with new ID)
$node = $this->metaStore->entityCopy($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $destination);
// Copy file content (new entity has new UUID = new file)
$content = $this->blobStore->blobRead((string)$identifier);
if ($content !== null) {
$this->blobStore->blobWrite((string)$node->identifier(), $content);
}
// Write meta file for recovery
$this->blobStore->metaWrite((string)$node->identifier(), $this->buildEntityMeta($node));
// cache entity
$this->serviceEntityCache[$node->identifier()] = $node;
return $node;
return ($extant[$identifier] ?? false) ? $collection : false;
}
public function entityMove(string|int|null $collection, string|int $identifier, string|int|null $destination): EntityResource {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
// null destination is root
if ($destination === null) {
$destination = self::ROOT_ID;
}
// Verify entity exists
$extant = $this->entityExtant($collection, $identifier);
if (!($extant[$identifier] ?? false)) {
throw new InvalidParameterException("Entity not found: $identifier");
}
// Move in meta store (file path never changes since it's based on entity ID)
$node = $this->metaStore->entityMove($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $destination);
// Update meta file for recovery (parent changed)
$this->blobStore->metaWrite((string)$node->identifier(), $this->buildEntityMeta($node));
// update cache
$this->serviceEntityCache[$node->identifier()] = $node;
return $node;
}
public function entityRead(string|int|null $collection, string|int $identifier): ?string {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
$entities = $this->entityFetch($collection, $identifier);
if (!isset($entities[$identifier])) {
public function entityRead(EntityIdentifierInterface $target): ?string {
if ($this->entityAccessible($target) === false) {
return null;
}
return $this->blobStore->blobRead((string)$identifier);
return $this->blobStore->blobRead((string)$target->entity());
}
public function entityReadStream(string|int|null $collection, string|int $identifier) {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
$entities = $this->entityFetch($collection, $identifier);
if (!isset($entities[$identifier])) {
public function entityReadStream(EntityIdentifierInterface $target) {
if ($this->entityAccessible($target) === false) {
return null;
}
return $this->blobStore->blobReadStream((string)$identifier);
return $this->blobStore->blobReadStream((string)$target->entity());
}
public function entityReadChunk(string|int|null $collection, string|int $identifier, int $offset, int $length): ?string {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
$entities = $this->entityFetch($collection, $identifier);
if (!isset($entities[$identifier])) {
public function entityReadChunk(EntityIdentifierInterface $target, int $offset, int $length): ?string {
if ($this->entityAccessible($target) === false) {
return null;
}
return $this->blobStore->blobReadChunk((string)$identifier, $offset, $length);
return $this->blobStore->blobReadChunk((string)$target->entity(), $offset, $length);
}
public function entityWrite(string|int|null $collection, string|int $identifier, string $data): int {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
// verify entity exists
$extant = $this->entityExtant($collection, $identifier);
if (!($extant[$identifier] ?? false)) {
public function entityWrite(EntityIdentifierInterface $target, string $data): int {
$collection = $this->entityAccessible($target);
$identifier = $target->entity();
if ($collection === false) {
throw new InvalidParameterException("Entity not found: $identifier");
}
// detect MIME type and format from content header
$signature = Signature::detect(substr($data, 0, Signature::HEADER_SIZE));
// Write blob data
$size = $this->blobStore->blobWrite((string)$identifier, $data);
if ($size === null) {
throw new \RuntimeException("Failed to write to entity: $identifier");
}
// Update meta store
$result = $this->metaStore->entityModify($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, [
'properties' => [
@@ -631,25 +674,20 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
'format' => $signature['format'],
],
], true);
// Update meta blob
$this->blobStore->metaWrite($result->identifier(), $this->buildEntityMeta($result));
// update cache
$this->serviceEntityCache[$result->identifier()] = $result;
return $size;
}
public function entityWriteStream(string|int|null $collection, string|int $identifier) {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
// Verify entity exists
$extant = $this->entityExtant($collection, $identifier);
if (!($extant[$identifier] ?? false)) {
public function entityWriteStream(EntityIdentifierInterface $target) {
$collection = $this->entityAccessible($target);
$identifier = $target->entity();
if ($collection === false) {
throw new InvalidParameterException("Entity not found: $identifier");
}
@@ -669,36 +707,31 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
// update meta blob
$this->blobStore->metaWrite($result->identifier(), $this->buildEntityMeta($result));
// update cache
$this->serviceEntityCache[$result->identifier()] = $result;
return $stream;
}
public function entityWriteChunk(string|int|null $collection, string|int $identifier, int $offset, string $data): int {
// null collection is root
if ($collection === null) {
$collection = self::ROOT_ID;
}
// Verify entity exists
$extant = $this->entityExtant($collection, $identifier);
if (!($extant[$identifier] ?? false)) {
public function entityWriteChunk(EntityIdentifierInterface $target, int $offset, string $data): int {
$collection = $this->entityAccessible($target);
$identifier = $target->entity();
if ($collection === false) {
throw new InvalidParameterException("Entity not found: $identifier");
}
// Detect MIME type and format from first chunk (offset === 0)
$signature = null;
if ($offset === 0) {
$signature = Signature::detect(substr($data, 0, Signature::HEADER_SIZE));
}
$bytes = $this->blobStore->blobWriteChunk((string)$identifier, $offset, $data);
if ($bytes === null) {
throw new \RuntimeException("Failed to write chunk to entity: $identifier");
}
// update meta store
$size = $this->blobStore->blobSize((string)$identifier) ?? 0;
$result = $this->metaStore->entityModify($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, [
@@ -711,41 +744,13 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
// Update meta blob
$this->blobStore->metaWrite($result->identifier(), $this->buildEntityMeta($result));
// update cache
$this->serviceEntityCache[$result->identifier()] = $result;
return $bytes;
}
// Node operations
public function nodeList(string|int|null $collection = null, bool $recursive = false, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array {
if ($collection === null) {
$collection = self::ROOT_ID;
}
return $this->metaStore->nodeList($this->serviceTenantId, $this->serviceUserId, $collection, $recursive, $filter, $sort, $range, $properties);
}
public function nodeListFilter(): IFilter {
return new Filter($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_FILTER] ?? []);
}
public function nodeListSort(): ISort {
return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []);
}
public function nodeListRange(RangeType $type): IRange {
if ($type !== RangeType::TALLY) {
throw new InvalidParameterException("Invalid: Node range of type '{$type->value}' is not supported");
}
return new RangeTally();
}
public function nodeDelta(string|int|null $collection, string $signature, string $detail = 'ids'): Delta {
return new Delta();
}
/**
* Build metadata array for an entity to store in .meta file
*
@@ -758,8 +763,8 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
'uid' => $this->serviceUserId,
'cid' => $node->collection(),
'nid' => $node->identifier(),
'created' => $node->created()?->format('c'),
'modified' => $node->modified()?->format('c'),
'created' => $node->created()?->getTimestamp() !== null ? (((int) $node->created()?->format('U')) * 1000) + (int) $node->created()?->format('v') : null,
'modified' => $node->modified()?->getTimestamp() !== null ? (((int) $node->modified()?->format('U')) * 1000) + (int) $node->modified()?->format('v') : null,
'size' => $node->getProperties()->size(),
'label' => $node->getProperties()->getLabel(),
'mime' => $node->getProperties()->getMime(),