From 3a499a22f588b62b3190ff4d3a39cf11a0a8a18e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 15 Jan 2025 21:38:53 +0100 Subject: [PATCH 01/31] Add locking mechanism to ObjectEntity and ObjectService - Introduced locking functionality in ObjectEntity with methods to lock, unlock, check lock status, and retrieve lock information. - Enhanced ObjectService to include methods for locking and unlocking objects, along with appropriate exception handling for authorization and locking conflicts. - Added new properties for locked state, owner, and authorization in ObjectEntity. - Updated documentation for new methods and parameters. --- docs/object/object.md | 71 +++++++++++++++++ lib/Db/ObjectEntity.php | 127 ++++++++++++++++++++++++++++++ lib/Event/ObjectLockedEvent.php | 22 ++++++ lib/Event/ObjectUnlockedEvent.php | 22 ++++++ lib/Service/ObjectService.php | 103 ++++++++++++++++++++++++ 5 files changed, 345 insertions(+) create mode 100644 docs/object/object.md create mode 100644 lib/Event/ObjectLockedEvent.php create mode 100644 lib/Event/ObjectUnlockedEvent.php diff --git a/docs/object/object.md b/docs/object/object.md new file mode 100644 index 0000000..4bccea7 --- /dev/null +++ b/docs/object/object.md @@ -0,0 +1,71 @@ +# Objects in OpenRegister + +Objects are the core data entities in OpenRegister that store and manage structured information. This document explains everything you need to know about working with objects. + +## Overview + +An object in OpenRegister represents a single record of data that: +- Conforms to a defined schema +- Belongs to a specific register +- Has a unique UUID identifier +- Can contain nested objects and file attachments +- Maintains version history through audit logs +- Can be linked to other objects via relations + +## Object Structure + +Each object contains: + +- `id`: Unique UUID identifier +- `uri`: Absolute URL to access the object +- `version`: Semantic version number (e.g. 1.0.0) +- `register`: The register this object belongs to +- `schema`: The schema this object must conform to +- `object`: The actual data payload as JSON +- `files`: Array of related file IDs +- `relations`: Array of related object IDs +- `textRepresentation`: Text representation of the object +- `locked`: Lock status and details +- `owner`: Nextcloud user that owns the object +- `authorization`: JSON object describing access permissions +- `updated`: Last modification timestamp +- `created`: Creation timestamp + +## Key Features + +### Schema Validation +- Objects are validated against their schema definition +- Supports both soft and hard validation modes +- Ensures data integrity and consistency + +### Relations & Nesting +- Objects can reference other objects via UUIDs or URLs +- Supports nested object structures up to configured depth +- Maintains bidirectional relationship tracking + +### Version Control +- Automatic version incrementing +- Full audit trail of changes +- Historical version access + +### Access Control +- Object-level ownership +- Granular authorization rules +- Lock mechanism for concurrent access control + +### File Attachments +- Support for file attachments +- Secure file storage integration +- File metadata tracking + +## API Operations + +Objects support standard CRUD operations: +- Create new objects +- Read existing objects +- Update object data +- Delete objects +- Search and filter objects +- Export object data + +For detailed API documentation and examples, see the API reference section. \ No newline at end of file diff --git a/lib/Db/ObjectEntity.php b/lib/Db/ObjectEntity.php index dc73772..2bab53d 100644 --- a/lib/Db/ObjectEntity.php +++ b/lib/Db/ObjectEntity.php @@ -5,6 +5,7 @@ use DateTime; use JsonSerializable; use OCP\AppFramework\Db\Entity; +use OCP\IUserSession; class ObjectEntity extends Entity implements JsonSerializable { @@ -17,6 +18,9 @@ class ObjectEntity extends Entity implements JsonSerializable protected ?array $files = []; // array of file ids that are related to this object protected ?array $relations = []; // array of object ids that this object is related to protected ?string $textRepresentation = null; + protected ?array $locked = null; // Contains the locked object if the object is locked + protected ?string $owner = null; // The Nextcloud user that owns this object + protected ?array $authorization = []; // JSON object describing authorizations protected ?DateTime $updated = null; protected ?DateTime $created = null; @@ -30,6 +34,9 @@ public function __construct() { $this->addType(fieldName:'files', type: 'json'); $this->addType(fieldName:'relations', type: 'json'); $this->addType(fieldName:'textRepresentation', type: 'text'); + $this->addType(fieldName:'locked', type: 'json'); + $this->addType(fieldName:'owner', type: 'string'); + $this->addType(fieldName:'authorization', type: 'json'); $this->addType(fieldName:'updated', type: 'datetime'); $this->addType(fieldName:'created', type: 'datetime'); } @@ -86,9 +93,129 @@ public function getObjectArray(): array 'files' => $this->files, 'relations' => $this->relations, 'textRepresentation' => $this->textRepresentation, + 'locked' => $this->locked, + 'owner' => $this->owner, + 'authorization' => $this->authorization, 'updated' => isset($this->updated) ? $this->updated->format('c') : null, 'created' => isset($this->created) ? $this->created->format('c') : null ]; } + /** + * Lock the object for a specific duration + * + * @param IUserSession $userSession Current user session + * @param string|null $process Optional process identifier + * @param int|null $duration Lock duration in seconds (default: 1 hour) + * @return bool True if lock was successful + * @throws \Exception If object is already locked by another user + */ + public function lock(IUserSession $userSession, ?string $process = null, ?int $duration = 3600): bool + { + $currentUser = $userSession->getUser(); + if (!$currentUser) { + throw new \Exception('No user logged in'); + } + + $userId = $currentUser->getUID(); + $now = new \DateTime(); + + // If already locked, check if it's the same user and not expired + if ($this->isLocked()) { + $lock = $this->locked; + + // If locked by different user + if ($lock['user'] !== $userId) { + throw new \Exception('Object is locked by another user'); + } + + // If same user, extend the lock + $expirationDate = new \DateTime($lock['expiration']); + $newExpiration = clone $now; + $newExpiration->add(new \DateInterval('PT' . $duration . 'S')); + + $this->locked = [ + 'user' => $userId, + 'process' => $process ?? $lock['process'], + 'created' => $lock['created'], + 'duration' => $duration, + 'expiration' => $newExpiration->format('c') + ]; + } else { + // Create new lock + $expiration = clone $now; + $expiration->add(new \DateInterval('PT' . $duration . 'S')); + + $this->locked = [ + 'user' => $userId, + 'process' => $process, + 'created' => $now->format('c'), + 'duration' => $duration, + 'expiration' => $expiration->format('c') + ]; + } + + return true; + } + + /** + * Unlock the object + * + * @param IUserSession $userSession Current user session + * @return bool True if unlock was successful + * @throws \Exception If object is locked by another user + */ + public function unlock(IUserSession $userSession): bool + { + if (!$this->isLocked()) { + return true; + } + + $currentUser = $userSession->getUser(); + if (!$currentUser) { + throw new \Exception('No user logged in'); + } + + $userId = $currentUser->getUID(); + + // Check if locked by different user + if ($this->locked['user'] !== $userId) { + throw new \Exception('Object is locked by another user'); + } + + $this->locked = null; + return true; + } + + /** + * Check if the object is currently locked + * + * @return bool True if object is locked and lock hasn't expired + */ + public function isLocked(): bool + { + if (!$this->locked) { + return false; + } + + // Check if lock has expired + $now = new \DateTime(); + $expiration = new \DateTime($this->locked['expiration']); + + return $now < $expiration; + } + + /** + * Get lock information + * + * @return array|null Lock information or null if not locked + */ + public function getLockInfo(): ?array + { + if (!$this->isLocked()) { + return null; + } + + return $this->locked; + } } diff --git a/lib/Event/ObjectLockedEvent.php b/lib/Event/ObjectLockedEvent.php new file mode 100644 index 0000000..1fe97ab --- /dev/null +++ b/lib/Event/ObjectLockedEvent.php @@ -0,0 +1,22 @@ +object = $object; + } + + public function getObject(): ObjectEntity { + return $this->object; + } +} \ No newline at end of file diff --git a/lib/Event/ObjectUnlockedEvent.php b/lib/Event/ObjectUnlockedEvent.php new file mode 100644 index 0000000..c8c7bc4 --- /dev/null +++ b/lib/Event/ObjectUnlockedEvent.php @@ -0,0 +1,22 @@ +object = $object; + } + + public function getObject(): ObjectEntity { + return $this->object; + } +} \ No newline at end of file diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 1d10d99..6f56f17 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -1765,4 +1765,107 @@ public function getUses(string $id, ?int $register = null, ?int $schema = null): return $referencedObjects; } + + /** + * Lock an object + * + * @param string|int $identifier Object ID, UUID, or URI + * @param string|null $process Optional process identifier + * @param int|null $duration Lock duration in seconds (default: 1 hour) + * @return ObjectEntity The locked object + * @throws NotFoundException If object not found + * @throws NotAuthorizedException If user not authorized + * @throws LockedException If object already locked by another user + */ + public function lockObject($identifier, ?string $process = null, ?int $duration = 3600): ObjectEntity + { + try { + $object = $this->objectEntityMapper->find($identifier); + + // Check if user has permission to lock + if (!$this->userSession->isLoggedIn()) { + throw new NotAuthorizedException('Must be logged in to lock objects'); + } + + // Attempt to lock the object + try { + $object->lock($this->userSession, $process, $duration); + } catch (\Exception $e) { + throw new LockedException($e->getMessage()); + } + + // Save the locked object + $object = $this->objectEntityMapper->update($object); + + // Dispatch lock event + $this->eventDispatcher->dispatch( + ObjectLockedEvent::class, + new ObjectLockedEvent($object) + ); + + return $object; + + } catch (DoesNotExistException $e) { + throw new NotFoundException('Object not found'); + } + } + + /** + * Unlock an object + * + * @param string|int $identifier Object ID, UUID, or URI + * @return ObjectEntity The unlocked object + * @throws NotFoundException If object not found + * @throws NotAuthorizedException If user not authorized + * @throws LockedException If object locked by another user + */ + public function unlockObject($identifier): ObjectEntity + { + try { + $object = $this->objectEntityMapper->find($identifier); + + // Check if user has permission to unlock + if (!$this->userSession->isLoggedIn()) { + throw new NotAuthorizedException('Must be logged in to unlock objects'); + } + + // Attempt to unlock the object + try { + $object->unlock($this->userSession); + } catch (\Exception $e) { + throw new LockedException($e->getMessage()); + } + + // Save the unlocked object + $object = $this->objectEntityMapper->update($object); + + // Dispatch unlock event + $this->eventDispatcher->dispatch( + ObjectUnlockedEvent::class, + new ObjectUnlockedEvent($object) + ); + + return $object; + + } catch (DoesNotExistException $e) { + throw new NotFoundException('Object not found'); + } + } + + /** + * Check if an object is locked + * + * @param string|int $identifier Object ID, UUID, or URI + * @return bool True if object is locked, false otherwise + * @throws NotFoundException If object not found + */ + public function isLocked($identifier): bool + { + try { + $object = $this->objectEntityMapper->find($identifier); + return $object->isLocked(); + } catch (DoesNotExistException $e) { + throw new NotFoundException('Object not found'); + } + } } From aa1f2b74b1c951f8af45adb2b9ef8c5ee5806634 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 15 Jan 2025 21:50:21 +0100 Subject: [PATCH 02/31] Enhance ObjectEntityMapper and ObjectService with locking functionality - Added methods for locking and unlocking objects in ObjectEntityMapper, including user session checks and event dispatching for lock/unlock actions. - Refactored ObjectService to utilize the new locking methods from ObjectEntityMapper, improving code organization and exception handling. - Updated constructor of ObjectEntityMapper to inject dependencies for event dispatching and user session management. - Ensured that the current user is set as the owner when creating or updating objects. - Added documentation for new methods and parameters related to object locking. --- lib/Db/ObjectEntityMapper.php | 105 +++++++++++++++++++++++++++++++++- lib/Service/ObjectService.php | 67 ++++++---------------- 2 files changed, 119 insertions(+), 53 deletions(-) diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php index 1b3f0a8..a251877 100644 --- a/lib/Db/ObjectEntityMapper.php +++ b/lib/Db/ObjectEntityMapper.php @@ -12,7 +12,11 @@ use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use OCP\IUserSession; use Symfony\Component\Uid\Uuid; +use OCP\EventDispatcher\IEventDispatcher; +use OCA\OpenRegister\Event\ObjectLockedEvent; +use OCA\OpenRegister\Event\ObjectUnlockedEvent; /** * The ObjectEntityMapper class @@ -22,6 +26,8 @@ class ObjectEntityMapper extends QBMapper { private IDatabaseJsonService $databaseJsonService; + private IEventDispatcher $eventDispatcher; + private IUserSession $userSession; public const MAIN_FILTERS = ['register', 'schema', 'uuid', 'created', 'updated']; @@ -30,14 +36,22 @@ class ObjectEntityMapper extends QBMapper * * @param IDBConnection $db The database connection * @param MySQLJsonService $mySQLJsonService The MySQL JSON service + * @param IEventDispatcher $eventDispatcher The event dispatcher + * @param IUserSession $userSession The user session */ - public function __construct(IDBConnection $db, MySQLJsonService $mySQLJsonService) - { + public function __construct( + IDBConnection $db, + MySQLJsonService $mySQLJsonService, + IEventDispatcher $eventDispatcher, + IUserSession $userSession + ) { parent::__construct($db, 'openregister_objects'); if ($db->getDatabasePlatform() instanceof MySQLPlatform === true) { $this->databaseJsonService = $mySQLJsonService; } + $this->eventDispatcher = $eventDispatcher; + $this->userSession = $userSession; } /** @@ -251,6 +265,10 @@ public function createFromArray(array $object): ObjectEntity if ($obj->getUuid() === null) { $obj->setUuid(Uuid::v4()); } + // Set current user as owner when creating new object + if ($this->userSession->isLoggedIn()) { + $obj->setOwner($this->userSession->getUser()->getUID()); + } return $this->insert($obj); } @@ -273,6 +291,10 @@ public function updateFromArray(int $id, array $object): ObjectEntity $obj->setVersion(implode('.', $version)); } + // Set current user as owner if not already set + if ($obj->getOwner() === null && $this->userSession->isLoggedIn()) { + $obj->setOwner($this->userSession->getUser()->getUID()); + } return $this->update($obj); } @@ -347,4 +369,83 @@ public function findByRelationUri(string $search, bool $partialMatch = false): a return $this->findEntities($qb); } + + /** + * Lock an object + * + * @param string|int $identifier Object ID, UUID, or URI + * @param string|null $process Optional process identifier + * @param int|null $duration Lock duration in seconds + * @return ObjectEntity The locked object + * @throws DoesNotExistException If object not found + * @throws \Exception If locking fails + */ + public function lockObject($identifier, ?string $process = null, ?int $duration = 3600): ObjectEntity + { + $object = $this->find($identifier); + + // Check if user has permission to lock + if (!$this->userSession->isLoggedIn()) { + throw new \Exception('Must be logged in to lock objects'); + } + + // Attempt to lock the object + $object->lock($this->userSession, $process, $duration); + + // Save the locked object + $object = $this->update($object); + + // Dispatch lock event + $this->eventDispatcher->dispatch( + ObjectLockedEvent::class, + new ObjectLockedEvent($object) + ); + + return $object; + } + + /** + * Unlock an object + * + * @param string|int $identifier Object ID, UUID, or URI + * @return ObjectEntity The unlocked object + * @throws DoesNotExistException If object not found + * @throws \Exception If unlocking fails + */ + public function unlockObject($identifier): ObjectEntity + { + $object = $this->find($identifier); + + // Check if user has permission to unlock + if (!$this->userSession->isLoggedIn()) { + throw new \Exception('Must be logged in to unlock objects'); + } + + // Attempt to unlock the object + $object->unlock($this->userSession); + + // Save the unlocked object + $object = $this->update($object); + + // Dispatch unlock event + $this->eventDispatcher->dispatch( + ObjectUnlockedEvent::class, + new ObjectUnlockedEvent($object) + ); + + return $object; + } + + /** + * Check if an object is locked + * + * @param string|int $identifier Object ID, UUID, or URI + * @return bool True if object is locked, false otherwise + * @throws DoesNotExistException If object not found + */ + public function isObjectLocked($identifier): bool + { + $object = $this->find($identifier); + return $object->isLocked(); + } } diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 6f56f17..cd07e38 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -1780,33 +1780,18 @@ public function getUses(string $id, ?int $register = null, ?int $schema = null): public function lockObject($identifier, ?string $process = null, ?int $duration = 3600): ObjectEntity { try { - $object = $this->objectEntityMapper->find($identifier); - - // Check if user has permission to lock - if (!$this->userSession->isLoggedIn()) { - throw new NotAuthorizedException('Must be logged in to lock objects'); - } - - // Attempt to lock the object - try { - $object->lock($this->userSession, $process, $duration); - } catch (\Exception $e) { - throw new LockedException($e->getMessage()); - } - - // Save the locked object - $object = $this->objectEntityMapper->update($object); - - // Dispatch lock event - $this->eventDispatcher->dispatch( - ObjectLockedEvent::class, - new ObjectLockedEvent($object) + return $this->objectEntityMapper->lockObject( + $identifier, + $process, + $duration ); - - return $object; - } catch (DoesNotExistException $e) { throw new NotFoundException('Object not found'); + } catch (\Exception $e) { + if (str_contains($e->getMessage(), 'Must be logged in')) { + throw new NotAuthorizedException($e->getMessage()); + } + throw new LockedException($e->getMessage()); } } @@ -1822,33 +1807,14 @@ public function lockObject($identifier, ?string $process = null, ?int $duration public function unlockObject($identifier): ObjectEntity { try { - $object = $this->objectEntityMapper->find($identifier); - - // Check if user has permission to unlock - if (!$this->userSession->isLoggedIn()) { - throw new NotAuthorizedException('Must be logged in to unlock objects'); - } - - // Attempt to unlock the object - try { - $object->unlock($this->userSession); - } catch (\Exception $e) { - throw new LockedException($e->getMessage()); - } - - // Save the unlocked object - $object = $this->objectEntityMapper->update($object); - - // Dispatch unlock event - $this->eventDispatcher->dispatch( - ObjectUnlockedEvent::class, - new ObjectUnlockedEvent($object) - ); - - return $object; - + return $this->objectEntityMapper->unlockObject($identifier); } catch (DoesNotExistException $e) { throw new NotFoundException('Object not found'); + } catch (\Exception $e) { + if (str_contains($e->getMessage(), 'Must be logged in')) { + throw new NotAuthorizedException($e->getMessage()); + } + throw new LockedException($e->getMessage()); } } @@ -1862,8 +1828,7 @@ public function unlockObject($identifier): ObjectEntity public function isLocked($identifier): bool { try { - $object = $this->objectEntityMapper->find($identifier); - return $object->isLocked(); + return $this->objectEntityMapper->isObjectLocked($identifier); } catch (DoesNotExistException $e) { throw new NotFoundException('Object not found'); } From 75eed8b1d26c45970e415e51bd72e9c6621ed482 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 15 Jan 2025 21:52:30 +0100 Subject: [PATCH 03/31] Why --- appinfo/info.xml | 0 lib/Service/ObjectService.php | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 appinfo/info.xml mode change 100755 => 100644 lib/Service/ObjectService.php diff --git a/appinfo/info.xml b/appinfo/info.xml old mode 100755 new mode 100644 diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php old mode 100755 new mode 100644 From 62c8e5f712902ea160d1af13260264bdb9938122 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 15 Jan 2025 22:25:41 +0100 Subject: [PATCH 04/31] Add object locking, unlocking, and reverting functionality - Introduced new API endpoints for locking, unlocking, and reverting objects in the ObjectsController. - Enhanced ObjectEntityMapper and ObjectService to support reverting objects to previous states based on datetime, audit trail ID, or semantic version. - Updated documentation to reflect new capabilities, including detailed descriptions of locking and version control features. - Implemented error handling for locking and reverting operations, ensuring robust responses for various scenarios. --- appinfo/routes.php | 3 + docs/object/object.md | 29 ++++- lib/Controller/ObjectsController.php | 151 +++++++++++++++++++++++++++ lib/Db/AuditTrailMapper.php | 66 ++++++++++++ lib/Db/ObjectAuditLogMapper.php | 47 +++++++++ lib/Db/ObjectEntityMapper.php | 73 ++++++++++++- lib/Event/ObjectRevertedEvent.php | 32 ++++++ lib/Service/ObjectService.php | 42 ++++++++ 8 files changed, 441 insertions(+), 2 deletions(-) create mode 100644 lib/Event/ObjectRevertedEvent.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 63b8637..22ae0e0 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -20,5 +20,8 @@ ['name' => 'registers#upload', 'url' => '/api/registers/upload', 'verb' => 'POST'], ['name' => 'registers#uploadUpdate', 'url' => '/api/registers/{id}/upload', 'verb' => 'PUT', 'requirements' => ['id' => '[^/]+']], ['name' => 'registers#download', 'url' => '/api/registers/{id}/download', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], + ['name' => 'objects#lock', 'url' => '/api/objects/{id}/lock', 'verb' => 'POST'], + ['name' => 'objects#unlock', 'url' => '/api/objects/{id}/unlock', 'verb' => 'POST'], + ['name' => 'objects#revert', 'url' => '/api/objects/{id}/revert', 'verb' => 'POST'], ], ]; diff --git a/docs/object/object.md b/docs/object/object.md index 4bccea7..dcc429a 100644 --- a/docs/object/object.md +++ b/docs/object/object.md @@ -47,6 +47,8 @@ Each object contains: - Automatic version incrementing - Full audit trail of changes - Historical version access +- Ability to revert to any previous version +- Detailed change tracking between versions ### Access Control - Object-level ownership @@ -67,5 +69,30 @@ Objects support standard CRUD operations: - Delete objects - Search and filter objects - Export object data +- Revert to previous versions -For detailed API documentation and examples, see the API reference section. \ No newline at end of file +## Object Locking and Versioning + +### Version Control and Revert Functionality + +OpenRegister provides comprehensive version control capabilities: + +- Every change creates a new version with full audit trail +- Changes are tracked at the field level +- Previous versions can be viewed and compared +- Objects can be reverted to any historical version +- Revert operation creates new version rather than overwriting +- Audit logs maintain full history of all changes including reverts +- Revert includes all object properties including relations and files +- Batch revert operations supported for related objects + +### Locking Objects + +Objects can be locked to prevent concurrent modifications. This is useful when: +- Long-running processes need exclusive access +- Multiple users/systems are working on the same object +- Ensuring data consistency during complex updates + +#### Lock API Endpoints + +To lock an object via the REST API: \ No newline at end of file diff --git a/lib/Controller/ObjectsController.php b/lib/Controller/ObjectsController.php index 6556d33..f25ff17 100644 --- a/lib/Controller/ObjectsController.php +++ b/lib/Controller/ObjectsController.php @@ -380,4 +380,155 @@ public function getOpenConnectorMappingService(): ?\OCA\OpenConnector\Service\Ma return null; } + + /** + * Lock an object + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $id The ID of the object to lock + * @return JSONResponse A JSON response containing the locked object + */ + public function lock(int $id): JSONResponse + { + try { + $data = $this->request->getParams(); + $process = $data['process'] ?? null; + $duration = isset($data['duration']) ? (int)$data['duration'] : null; + + $object = $this->objectEntityMapper->lockObject( + $id, + $process, + $duration + ); + + return new JSONResponse($object->getObjectArray()); + + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Object not found'], 404); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], 403); + } catch (LockedException $e) { + return new JSONResponse(['error' => $e->getMessage()], 423); // 423 Locked + } catch (\Exception $e) { + return new JSONResponse(['error' => $e->getMessage()], 500); + } + } + + /** + * Unlock an object + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $id The ID of the object to unlock + * @return JSONResponse A JSON response containing the unlocked object + */ + public function unlock(int $id): JSONResponse + { + try { + $object = $this->objectEntityMapper->unlockObject($id); + return new JSONResponse($object->getObjectArray()); + + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Object not found'], 404); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], 403); + } catch (LockedException $e) { + return new JSONResponse(['error' => $e->getMessage()], 423); + } catch (\Exception $e) { + return new JSONResponse(['error' => $e->getMessage()], 500); + } + } + + /** + * Revert an object to a previous state + * + * This endpoint allows reverting an object to a previous state based on different criteria: + * 1. DateTime - Revert to the state at a specific point in time + * 2. Audit Trail ID - Revert to the state after a specific audit trail entry + * 3. Semantic Version - Revert to a specific version of the object + * + * Request body should contain one of: + * - datetime: ISO 8601 datetime string (e.g., "2024-03-01T12:00:00Z") + * - auditTrailId: UUID of an audit trail entry + * - version: Semantic version string (e.g., "1.0.0") + * + * Optional parameters: + * - overwriteVersion: boolean (default: false) - If true, keeps the version number, + * if false, increments the patch version + * + * Example requests: + * ```json + * { + * "datetime": "2024-03-01T12:00:00Z" + * } + * ``` + * ```json + * { + * "auditTrailId": "550e8400-e29b-41d4-a716-446655440000" + * } + * ``` + * ```json + * { + * "version": "1.0.0", + * "overwriteVersion": true + * } + * ``` + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $id The ID of the object to revert + * @return JSONResponse A JSON response containing the reverted object + * @throws NotFoundException If object not found + * @throws NotAuthorizedException If user not authorized + * @throws BadRequestException If no valid reversion point specified + * @throws LockedException If object is locked + */ + public function revert(int $id): JSONResponse + { + try { + $data = $this->request->getParams(); + + // Parse the revert point + $until = null; + if (isset($data['datetime'])) { + $until = new \DateTime($data['datetime']); + } elseif (isset($data['auditTrailId'])) { + $until = $data['auditTrailId']; + } elseif (isset($data['version'])) { + $until = $data['version']; + } + + if ($until === null) { + return new JSONResponse( + ['error' => 'Must specify either datetime, auditTrailId, or version'], + 400 + ); + } + + $overwriteVersion = $data['overwriteVersion'] ?? false; + + // Get the reverted object + $revertedObject = $this->objectEntityMapper->revertObject( + $id, + $until, + $overwriteVersion + ); + + // Save the reverted object + $savedObject = $this->objectEntityMapper->update($revertedObject); + + return new JSONResponse($savedObject->getObjectArray()); + + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Object not found'], 404); + } catch (NotAuthorizedException $e) { + return new JSONResponse(['error' => $e->getMessage()], 403); + } catch (\Exception $e) { + return new JSONResponse(['error' => $e->getMessage()], 500); + } + } } diff --git a/lib/Db/AuditTrailMapper.php b/lib/Db/AuditTrailMapper.php index 974351e..c3976fa 100644 --- a/lib/Db/AuditTrailMapper.php +++ b/lib/Db/AuditTrailMapper.php @@ -198,5 +198,71 @@ public function createAuditTrail(?ObjectEntity $old = null, ?ObjectEntity $new = return $this->insert(entity: $auditTrail); } + /** + * Get audit trails for an object until a specific point or version + * + * @param int $objectId The object ID + * @param string $objectUuid The object UUID + * @param DateTime|string|null $until DateTime, AuditTrail ID, or semantic version to get trails until + * @return array Array of AuditTrail objects + */ + public function findByObjectUntil(int $objectId, string $objectUuid, $until = null): array + { + $qb = $this->db->getQueryBuilder(); + + // Base query + $qb->select('*') + ->from('openregister_audit_trails') + ->where( + $qb->expr()->eq('object_id', $qb->createNamedParameter($objectId, IQueryBuilder::PARAM_INT)) + ) + ->andWhere( + $qb->expr()->eq('object_uuid', $qb->createNamedParameter($objectUuid, IQueryBuilder::PARAM_STR)) + ) + ->orderBy('created', 'DESC'); + + // Add condition based on until parameter + if ($until instanceof \DateTime) { + $qb->andWhere( + $qb->expr()->gte('created', $qb->createNamedParameter( + $until->format('Y-m-d H:i:s'), + IQueryBuilder::PARAM_STR + )) + ); + } elseif (is_string($until)) { + if ($this->isSemanticVersion($until)) { + // Handle semantic version + $qb->andWhere( + $qb->expr()->eq('version', $qb->createNamedParameter($until, IQueryBuilder::PARAM_STR)) + ); + } else { + // Handle audit trail ID + $qb->andWhere( + $qb->expr()->eq('id', $qb->createNamedParameter($until, IQueryBuilder::PARAM_STR)) + ); + // We want all entries up to and including this ID + $qb->orWhere( + $qb->expr()->gt('created', + $qb->createFunction('(SELECT created FROM `*PREFIX*openregister_audit_trails` WHERE id = ' . + $qb->createNamedParameter($until, IQueryBuilder::PARAM_STR) . ')') + ) + ); + } + } + + return $this->findEntities($qb); + } + + /** + * Check if a string is a semantic version + * + * @param string $version The version string to check + * @return bool True if string is a semantic version + */ + private function isSemanticVersion(string $version): bool + { + return preg_match('/^\d+\.\d+\.\d+$/', $version) === 1; + } + // We dont need update as we dont change the log } diff --git a/lib/Db/ObjectAuditLogMapper.php b/lib/Db/ObjectAuditLogMapper.php index 387fb6f..4f89a54 100644 --- a/lib/Db/ObjectAuditLogMapper.php +++ b/lib/Db/ObjectAuditLogMapper.php @@ -85,4 +85,51 @@ public function updateFromArray(int $id, array $object): ObjectAuditLog return $this->update($obj); } + + /** + * Get audit trails for an object until a specific point + * + * @param int $objectId The object ID + * @param string $objectUuid The object UUID + * @param DateTime|string|null $until DateTime or AuditTrail ID to get trails until + * @return array Array of AuditTrail objects + */ + public function findByObjectUntil(int $objectId, string $objectUuid, $until = null): array + { + $qb = $this->db->getQueryBuilder(); + + // Base query + $qb->select('*') + ->from('openregister_auditlog') + ->where( + $qb->expr()->eq('object_id', $qb->createNamedParameter($objectId, IQueryBuilder::PARAM_INT)) + ) + ->andWhere( + $qb->expr()->eq('object_uuid', $qb->createNamedParameter($objectUuid, IQueryBuilder::PARAM_STR)) + ) + ->orderBy('created', 'DESC'); + + // Add condition based on until parameter + if ($until instanceof \DateTime) { + $qb->andWhere( + $qb->expr()->gte('created', $qb->createNamedParameter( + $until->format('Y-m-d H:i:s'), + IQueryBuilder::PARAM_STR + )) + ); + } elseif (is_string($until)) { + $qb->andWhere( + $qb->expr()->eq('id', $qb->createNamedParameter($until, IQueryBuilder::PARAM_STR)) + ); + // We want all entries up to and including this ID + $qb->orWhere( + $qb->expr()->gt('created', + $qb->createFunction('(SELECT created FROM `*PREFIX*openregister_auditlog` WHERE id = ' . + $qb->createNamedParameter($until, IQueryBuilder::PARAM_STR) . ')') + ) + ); + } + + return $this->findEntities($qb); + } } diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php index 1c83e9c..bdc84d3 100644 --- a/lib/Db/ObjectEntityMapper.php +++ b/lib/Db/ObjectEntityMapper.php @@ -20,6 +20,7 @@ use OCA\OpenRegister\Event\ObjectDeletedEvent; use OCA\OpenRegister\Event\ObjectLockedEvent; use OCA\OpenRegister\Event\ObjectUnlockedEvent; +use OCA\OpenRegister\Db\AuditTrailMapper; /** * The ObjectEntityMapper class @@ -31,6 +32,7 @@ class ObjectEntityMapper extends QBMapper private IDatabaseJsonService $databaseJsonService; private IEventDispatcher $eventDispatcher; private IUserSession $userSession; + private AuditTrailMapper $auditTrailMapper; public const MAIN_FILTERS = ['register', 'schema', 'uuid', 'created', 'updated']; @@ -41,12 +43,14 @@ class ObjectEntityMapper extends QBMapper * @param MySQLJsonService $mySQLJsonService The MySQL JSON service * @param IEventDispatcher $eventDispatcher The event dispatcher * @param IUserSession $userSession The user session + * @param AuditTrailMapper $auditTrailMapper The audit trail mapper */ public function __construct( IDBConnection $db, MySQLJsonService $mySQLJsonService, IEventDispatcher $eventDispatcher, - IUserSession $userSession + IUserSession $userSession, + AuditTrailMapper $auditTrailMapper ) { parent::__construct($db, 'openregister_objects'); @@ -55,6 +59,7 @@ public function __construct( } $this->eventDispatcher = $eventDispatcher; $this->userSession = $userSession; + $this->auditTrailMapper = $auditTrailMapper; } /** @@ -499,4 +504,70 @@ public function isObjectLocked($identifier): bool $object = $this->find($identifier); return $object->isLocked(); } + + /** + * Revert an object to a previous state + * + * @param string|int $identifier Object ID, UUID, or URI + * @param DateTime|string|null $until DateTime or AuditTrail ID to revert to + * @param bool $overwriteVersion Whether to overwrite the version or increment it + * @return ObjectEntity The reverted object (unsaved) + * @throws DoesNotExistException If object not found + * @throws \Exception If revert fails + */ + public function revertObject($identifier, $until = null, bool $overwriteVersion = false): ObjectEntity + { + // Get the current object + $object = $this->find($identifier); + + // Get audit trail entries until the specified point + $auditTrails = $this->auditTrailMapper->findByObjectUntil( + $object->getId(), + $object->getUuid(), + $until + ); + + if (empty($auditTrails) && $until !== null) { + throw new \Exception('No audit trail entries found for the specified reversion point'); + } + + // Create a clone of the current object to apply reversions + $revertedObject = clone $object; + + // Apply changes in reverse + foreach ($auditTrails as $audit) { + $this->revertChanges($revertedObject, $audit); + } + + // Handle versioning + if (!$overwriteVersion) { + $version = explode('.', $revertedObject->getVersion()); + $version[2] = (int) $version[2] + 1; + $revertedObject->setVersion(implode('.', $version)); + } + + return $revertedObject; + } + + /** + * Helper function to revert changes from an audit trail entry + * + * @param ObjectEntity $object The object to apply reversions to + * @param AuditTrail $audit The audit trail entry + */ + private function revertChanges(ObjectEntity $object, AuditTrail $audit): void + { + $changes = $audit->getChanges(); + + // Iterate through each change and apply the reverse + foreach ($changes as $field => $change) { + if (isset($change['old'])) { + // Use reflection to set the value if it's a protected property + $reflection = new \ReflectionClass($object); + $property = $reflection->getProperty($field); + $property->setAccessible(true); + $property->setValue($object, $change['old']); + } + } + } } diff --git a/lib/Event/ObjectRevertedEvent.php b/lib/Event/ObjectRevertedEvent.php new file mode 100644 index 0000000..5986603 --- /dev/null +++ b/lib/Event/ObjectRevertedEvent.php @@ -0,0 +1,32 @@ +object = $object; + $this->until = $until; + } + + public function getObject(): ObjectEntity { + return $this->object; + } + + public function getRevertPoint() { + return $this->until; + } +} \ No newline at end of file diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index dd28c67..5b83570 100644 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -1867,4 +1867,46 @@ public function isLocked($identifier): bool throw new NotFoundException('Object not found'); } } + + /** + * Revert an object to a previous state + * + * @param string|int $identifier Object ID, UUID, or URI + * @param DateTime|string|null $until DateTime or AuditTrail ID to revert to + * @param bool $overwriteVersion Whether to overwrite the version or increment it + * @return ObjectEntity The reverted object + * @throws NotFoundException If object not found + * @throws NotAuthorizedException If user not authorized + * @throws \Exception If revert fails + */ + public function revertObject($identifier, $until = null, bool $overwriteVersion = false): ObjectEntity + { + try { + // Get the reverted object (unsaved) + $revertedObject = $this->objectEntityMapper->revertObject( + $identifier, + $until, + $overwriteVersion + ); + + // Save the reverted object + $revertedObject = $this->objectEntityMapper->update($revertedObject); + + // Dispatch revert event + $this->eventDispatcher->dispatch( + ObjectRevertedEvent::class, + new ObjectRevertedEvent($revertedObject, $until) + ); + + return $revertedObject; + + } catch (DoesNotExistException $e) { + throw new NotFoundException('Object not found'); + } catch (\Exception $e) { + if (str_contains($e->getMessage(), 'Must be logged in')) { + throw new NotAuthorizedException($e->getMessage()); + } + throw $e; + } + } } From 742f677f4c7b23c690f24523e47fd58df0337f1c Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 15 Jan 2025 23:03:15 +0100 Subject: [PATCH 05/31] Add locking and ownership features to ObjectEntity and related components - Enhanced ObjectEntity to include 'locked' state and 'owner' properties. - Updated mock data and tests to reflect new attributes for object locking and ownership. - Introduced LockObject modal for locking functionality in the UI. - Added methods in the object store for locking and unlocking objects, with appropriate error handling. - Updated ObjectDetails and ObjectsList views to support locking and unlocking actions. --- src/entities/object/object.mock.ts | 4 + src/entities/object/object.spec.ts | 13 ++++ src/entities/object/object.ts | 13 ++++ src/entities/object/object.types.ts | 2 + src/modals/Modals.vue | 3 + src/modals/object/LockObject.vue | 113 ++++++++++++++++++++++++++++ src/store/modules/object.js | 102 +++++++++++++++++++++++++ src/views/object/ObjectDetails.vue | 30 ++++++++ src/views/object/ObjectsList.vue | 29 +++++++ 9 files changed, 309 insertions(+) create mode 100644 src/modals/object/LockObject.vue diff --git a/src/entities/object/object.mock.ts b/src/entities/object/object.mock.ts index 0eaab47..5240942 100644 --- a/src/entities/object/object.mock.ts +++ b/src/entities/object/object.mock.ts @@ -13,6 +13,8 @@ export const mockObjectData = (): TObject[] => [ files: JSON.stringify({ key: 'value' }), created: new Date().toISOString(), updated: new Date().toISOString(), + locked: ['token1', 'token2'], // Array of lock tokens + owner: 'user1', // Owner of the object }, { id: '5678a1e5-b54d-43ad-abd1-4b5bff5fcd3f', @@ -25,6 +27,8 @@ export const mockObjectData = (): TObject[] => [ files: JSON.stringify({ key: 'value' }), created: new Date().toISOString(), updated: new Date().toISOString(), + locked: null, // Not locked + owner: 'user2', // Owner of the object }, ] diff --git a/src/entities/object/object.spec.ts b/src/entities/object/object.spec.ts index f66583e..9b90242 100644 --- a/src/entities/object/object.spec.ts +++ b/src/entities/object/object.spec.ts @@ -25,6 +25,19 @@ describe('Object Entity', () => { expect(object.files).toBe(mockObjectData()[0].files) expect(object.updated).toBe(mockObjectData()[0].updated) expect(object.created).toBe(mockObjectData()[0].created) + expect(object.locked).toBe(null) + expect(object.owner).toBe('') + expect(object.validate().success).toBe(true) + }) + + it('should handle locked array and owner string', () => { + const mockData = mockObjectData()[0] + mockData.locked = ['token1', 'token2'] + mockData.owner = 'user1' + const object = new ObjectEntity(mockData) + + expect(object.locked).toEqual(['token1', 'token2']) + expect(object.owner).toBe('user1') expect(object.validate().success).toBe(true) }) diff --git a/src/entities/object/object.ts b/src/entities/object/object.ts index cec2b84..6a3b98b 100644 --- a/src/entities/object/object.ts +++ b/src/entities/object/object.ts @@ -1,6 +1,9 @@ import { SafeParseReturnType, z } from 'zod' import { TObject } from './object.types' +/** + * Entity class representing an Object with validation + */ export class ObjectEntity implements TObject { public id: string @@ -13,6 +16,8 @@ export class ObjectEntity implements TObject { public files: string public updated: string public created: string + public locked: string[] | null // Array of lock tokens or null if not locked + public owner: string // Owner of the object constructor(object: TObject) { this.id = object.id || '' @@ -25,8 +30,14 @@ export class ObjectEntity implements TObject { this.files = object.files this.updated = object.updated || '' this.created = object.created || '' + this.locked = object.locked || null + this.owner = object.owner || '' } + /** + * Validates the object against a schema + * @returns {SafeParseReturnType} Object containing validation result with success/error status + */ public validate(): SafeParseReturnType { const schema = z.object({ id: z.string().min(1), @@ -38,6 +49,8 @@ export class ObjectEntity implements TObject { files: z.string(), updated: z.string(), created: z.string(), + locked: z.array(z.string()).nullable(), + owner: z.string(), }) return schema.safeParse(this) diff --git a/src/entities/object/object.types.ts b/src/entities/object/object.types.ts index b37dc3c..aa018c7 100644 --- a/src/entities/object/object.types.ts +++ b/src/entities/object/object.types.ts @@ -9,4 +9,6 @@ export type TObject = { files: string updated: string created: string + locked: string[] | null // Array of lock tokens or null if not locked + owner: string // Owner of the object } diff --git a/src/modals/Modals.vue b/src/modals/Modals.vue index fc22305..1c9825e 100644 --- a/src/modals/Modals.vue +++ b/src/modals/Modals.vue @@ -17,6 +17,7 @@ import { navigationStore } from '../store/store.js' + @@ -37,6 +38,7 @@ import EditObject from './object/EditObject.vue' import DeleteObject from './object/DeleteObject.vue' import UploadObject from './object/UploadObject.vue' import ViewObjectAuditTrail from './objectAuditTrail/ViewObjectAuditTrail.vue' +import LockObject from './object/LockObject.vue' export default { name: 'Modals', @@ -55,6 +57,7 @@ export default { DeleteObject, UploadObject, ViewObjectAuditTrail, + LockObject, }, } diff --git a/src/modals/object/LockObject.vue b/src/modals/object/LockObject.vue new file mode 100644 index 0000000..6b0d9ee --- /dev/null +++ b/src/modals/object/LockObject.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/store/modules/object.js b/src/store/modules/object.js index b8befb9..c49d888 100644 --- a/src/store/modules/object.js +++ b/src/store/modules/object.js @@ -206,5 +206,107 @@ export const useObjectStore = defineStore('object', { return { response, data } }, + /** + * Lock an object + * + * @param {number} id Object ID + * @param {string|null} process Optional process identifier + * @param {number|null} duration Lock duration in seconds + * @return {Promise} Promise that resolves when the object is locked + */ + async lockObject(id, process = null, duration = null) { + const endpoint = `/index.php/apps/openregister/api/objects/${id}/lock` + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + process, + duration, + }), + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + this.setObjectItem(data) + this.refreshObjectList() + + return { response, data } + } catch (error) { + console.error('Error locking object:', error) + throw new Error(`Failed to lock object: ${error.message}`) + } + }, + /** + * Unlock an object + * + * @param {number} id Object ID + * @return {Promise} Promise that resolves when the object is unlocked + */ + async unlockObject(id) { + const endpoint = `/index.php/apps/openregister/api/objects/${id}/unlock` + + try { + const response = await fetch(endpoint, { + method: 'POST', + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + this.setObjectItem(data) + this.refreshObjectList() + + return { response, data } + } catch (error) { + console.error('Error unlocking object:', error) + throw new Error(`Failed to unlock object: ${error.message}`) + } + }, + /** + * Revert an object to a previous state + * + * @param {number} id Object ID + * @param {object} options Revert options + * @param {string} [options.datetime] ISO datetime string + * @param {string} [options.auditTrailId] Audit trail ID + * @param {string} [options.version] Semantic version + * @param {boolean} [options.overwriteVersion] Whether to overwrite version + * @return {Promise} Promise that resolves when the object is reverted + */ + async revertObject(id, options) { + const endpoint = `/index.php/apps/openregister/api/objects/${id}/revert` + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(options), + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + this.setObjectItem(data) + this.refreshObjectList() + + return { response, data } + } catch (error) { + console.error('Error reverting object:', error) + throw new Error(`Failed to revert object: ${error.message}`) + } + }, }, }) diff --git a/src/views/object/ObjectDetails.vue b/src/views/object/ObjectDetails.vue index 4160618..932525f 100644 --- a/src/views/object/ObjectDetails.vue +++ b/src/views/object/ObjectDetails.vue @@ -21,6 +21,18 @@ import { objectStore, navigationStore } from '../../store/store.js' Edit + + + Lock + + + + Unlock + Edit + + + Lock + + + + Unlock + Delete + + + Open Folder + @@ -65,6 +73,10 @@ import { objectStore, navigationStore } from '../../store/store.js' Schema:

{{ objectStore.objectItem.schema }}

+
+ Folder: +

{{ objectStore.objectItem.folder || '-' }}

+
Updated:

{{ objectStore.objectItem.updated }}

@@ -186,6 +198,7 @@ import Eye from 'vue-material-design-icons/Eye.vue' import CubeOutline from 'vue-material-design-icons/CubeOutline.vue' import LockOutline from 'vue-material-design-icons/LockOutline.vue' import LockOpenOutline from 'vue-material-design-icons/LockOpenOutline.vue' +import FolderOutline from 'vue-material-design-icons/FolderOutline.vue' export default { name: 'ObjectDetails', @@ -204,6 +217,7 @@ export default { Eye, LockOutline, LockOpenOutline, + FolderOutline, }, data() { return { @@ -247,6 +261,17 @@ export default { this.relationsLoading = false }) }, + /** + * Opens the folder URL in a new tab after parsing the encoded URL + * @param {string} url - The encoded folder URL to open + */ + openFolder(url) { + // Parse the encoded URL by replacing escaped characters + const decodedUrl = url.replace(/\\\//g, '/') + + // Open URL in new tab + window.open(decodedUrl, '_blank') + }, }, } From 4287b83f22eff189d7b939a5b42fbbe3daf9242f Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 21 Jan 2025 13:53:52 +0100 Subject: [PATCH 18/31] Make files visable in Open Register file detail view --- appinfo/routes.php | 1 + lib/Controller/ObjectsController.php | 33 +++++++++++++++++-- lib/Service/ObjectService.php | 4 +-- src/store/modules/object.js | 47 +++++++++++++++++++++++++++- src/views/object/ObjectDetails.vue | 46 +++++++++++++++++++++++++-- 5 files changed, 123 insertions(+), 8 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 22ae0e0..11bef22 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -14,6 +14,7 @@ ['name' => 'objects#mappings', 'url' => '/api/objects/mappings', 'verb' => 'GET'], ['name' => 'objects#auditTrails', 'url' => '/api/objects/audit-trails/{id}', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], ['name' => 'objects#relations', 'url' => '/api/objects/relations/{id}', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], + ['name' => 'objects#files', 'url' => '/api/objects/files/{id}', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], ['name' => 'schemas#upload', 'url' => '/api/schemas/upload', 'verb' => 'POST'], ['name' => 'schemas#uploadUpdate', 'url' => '/api/schemas/{id}/upload', 'verb' => 'PUT', 'requirements' => ['id' => '[^/]+']], ['name' => 'schemas#download', 'url' => '/api/schemas/{id}/download', 'verb' => 'GET', 'requirements' => ['id' => '[^/]+']], diff --git a/lib/Controller/ObjectsController.php b/lib/Controller/ObjectsController.php index 1ab242c..45e6f05 100644 --- a/lib/Controller/ObjectsController.php +++ b/lib/Controller/ObjectsController.php @@ -40,7 +40,9 @@ public function __construct( private readonly ContainerInterface $container, private readonly ObjectEntityMapper $objectEntityMapper, private readonly AuditTrailMapper $auditTrailMapper, - private readonly ObjectAuditLogMapper $objectAuditLogMapper + private readonly ObjectAuditLogMapper $objectAuditLogMapper, + private readonly ObjectService $objectService, + ) { parent::__construct($appName, $request); @@ -343,8 +345,6 @@ public function logs(int $id): JSONResponse } } - - /** * Retrieves all available mappings * @@ -546,4 +546,31 @@ public function revert(int $id): JSONResponse return new JSONResponse(['error' => $e->getMessage()], 500); } } + + /** + * Retrieves files associated with an object + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $id The ID of the object to get files for + * @return JSONResponse A JSON response containing the object's files + */ + public function files(string $id, ObjectService $objectService): JSONResponse + { + try { + // Get the object with files included + $object = $this->objectEntityMapper->find((int) $id); + $files = $objectService->getFiles($object); + $object = $objectService->hydrateFiles($object, $files); + + // Return just the files array from the object + return new JSONResponse($object->getFiles()); + + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Object not found'], 404); + } catch (\Exception $e) { + return new JSONResponse(['error' => $e->getMessage()], 500); + } + } } diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 0dca209..8462d62 100644 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -1316,7 +1316,7 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s * @return Node[] The files found. * @throws \OCP\Files\NotFoundException */ - private function getFiles(ObjectEntity $object): array + public function getFiles(ObjectEntity $object): array { $folder = $this->fileService->getObjectFolder(objectEntity: $object, register: $object->getRegister(), schema: $object->getSchema()); @@ -1335,7 +1335,7 @@ private function getFiles(ObjectEntity $object): array * * @return ObjectEntity The object with hydrated files array. */ - private function hydrateFiles(ObjectEntity $object, array $files): ObjectEntity + public function hydrateFiles(ObjectEntity $object, array $files): ObjectEntity { $formattedFiles = []; diff --git a/src/store/modules/object.js b/src/store/modules/object.js index c49d888..f267470 100644 --- a/src/store/modules/object.js +++ b/src/store/modules/object.js @@ -10,11 +10,18 @@ export const useObjectStore = defineStore('object', { auditTrails: [], relationItem: false, relations: [], + fileItem: false, // Single file item + files: [], // List of files }), actions: { - setObjectItem(objectItem) { + async setObjectItem(objectItem) { this.objectItem = objectItem && new ObjectEntity(objectItem) console.log('Active object item set to ' + objectItem) + + // Get files when object is set + if (objectItem && objectItem.id) { + await this.getFiles(objectItem.id) + } }, setObjectList(objectList) { this.objectList = objectList.map( @@ -38,6 +45,12 @@ export const useObjectStore = defineStore('object', { (relation) => new ObjectEntity(relation), ) }, + setFileItem(fileItem) { + this.fileItem = fileItem + }, + setFiles(files) { + this.files = files + }, /* istanbul ignore next */ // ignore this for Jest until moved into a service async refreshObjectList(search = null) { // @todo this might belong in a service? @@ -194,6 +207,38 @@ export const useObjectStore = defineStore('object', { return { response, data } }, + // FILES + /** + * Get files for an object + * + * @param {number} id Object ID + * @return {Promise} Promise that resolves with the object's files + */ + async getFiles(id) { + if (!id) { + throw new Error('No object id to get files for') + } + + const endpoint = `/index.php/apps/openregister/api/objects/files/${id}` + + try { + const response = await fetch(endpoint, { + method: 'GET', + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + this.setFiles(data || []) + + return { response, data } + } catch (error) { + console.error('Error getting files:', error) + throw new Error(`Failed to get files: ${error.message}`) + } + }, // mappings async getMappings() { const endpoint = '/index.php/apps/openregister/api/objects/mappings' diff --git a/src/views/object/ObjectDetails.vue b/src/views/object/ObjectDetails.vue index 5c089ef..09f97c7 100644 --- a/src/views/object/ObjectDetails.vue +++ b/src/views/object/ObjectDetails.vue @@ -135,8 +135,31 @@ import { objectStore, navigationStore } from '../../store/store.js'
-
- {{ JSON.stringify(objectStore.objectItem.files, null, 2) }} +
+ + + + + +
+
+ No files found
@@ -199,6 +222,7 @@ import CubeOutline from 'vue-material-design-icons/CubeOutline.vue' import LockOutline from 'vue-material-design-icons/LockOutline.vue' import LockOpenOutline from 'vue-material-design-icons/LockOpenOutline.vue' import FolderOutline from 'vue-material-design-icons/FolderOutline.vue' +import FileOutline from 'vue-material-design-icons/FileOutline.vue' export default { name: 'ObjectDetails', @@ -218,6 +242,7 @@ export default { LockOutline, LockOpenOutline, FolderOutline, + FileOutline, }, data() { return { @@ -272,6 +297,23 @@ export default { // Open URL in new tab window.open(decodedUrl, '_blank') }, + /** + * Opens a file in the Nextcloud Files app + * @param {Object} file - The file object containing id, path, and other metadata + */ + openFile(file) { + // Extract the directory path without the filename + const dirPath = file.path.substring(0, file.path.lastIndexOf('/')) + + // Remove the '/admin/files/' prefix if it exists + const cleanPath = dirPath.replace(/^\/admin\/files\//, '/') + + // Construct the proper Nextcloud Files app URL with file ID and openfile parameter + const filesAppUrl = `/index.php/apps/files/files/${file.id}?dir=${encodeURIComponent(cleanPath)}&openfile=true` + + // Open URL in new tab + window.open(filesAppUrl, '_blank') + }, }, } From e67bafa9712357fcda90d5901c0a1e5b8a68d2ca Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Tue, 21 Jan 2025 14:23:21 +0100 Subject: [PATCH 19/31] format more like DCAT --- lib/Service/FileService.php | 60 ++++++++++++++++++++++------------- lib/Service/ObjectService.php | 26 ++++++++------- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 658f9a8..03b0d7d 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -46,15 +46,17 @@ class FileService * @param IManager $shareManager The share manager interface */ public function __construct( - private readonly IUserSession $userSession, + private readonly IUserSession $userSession, private readonly LoggerInterface $logger, - private readonly IRootFolder $rootFolder, - private readonly IManager $shareManager, - private readonly IURLGenerator $urlGenerator, - private readonly IConfig $config, + private readonly IRootFolder $rootFolder, + private readonly IManager $shareManager, + private readonly IURLGenerator $urlGenerator, + private readonly IConfig $config, private readonly RegisterMapper $registerMapper, private readonly SchemaMapper $schemaMapper, - ) {} + ) + { + } /** * Creates a folder for a Register (used for storing files of Schemas/Objects). @@ -72,9 +74,9 @@ public function createRegisterFolder(Register|int $register): string $registerFolderName = $this->getRegisterFolderName($register); // @todo maybe we want to use ShareLink here for register->folder as well? - $register->setFolder($this->getFolderLink($this::ROOT_FOLDER."/$registerFolderName")); + $register->setFolder($this->getFolderLink($this::ROOT_FOLDER . "/$registerFolderName")); - $folderPath = $this::ROOT_FOLDER."/$registerFolderName"; + $folderPath = $this::ROOT_FOLDER . "/$registerFolderName"; $this->createFolder(folderPath: $folderPath); return $folderPath; @@ -120,11 +122,11 @@ public function createSchemaFolder(Register|int $register, Schema|int $schema): $registerFolderName = $this->getRegisterFolderName($register); // @todo maybe we want to use ShareLink here for register->folder as well? - $register->setFolder($this->getFolderLink($this::ROOT_FOLDER."/$registerFolderName")); + $register->setFolder($this->getFolderLink($this::ROOT_FOLDER . "/$registerFolderName")); $schemaFolderName = $this->getSchemaFolderName($schema); - $folderPath = $this::ROOT_FOLDER."/$registerFolderName/$schemaFolderName"; + $folderPath = $this::ROOT_FOLDER . "/$registerFolderName/$schemaFolderName"; $this->createFolder(folderPath: $folderPath); return $folderPath; @@ -153,10 +155,10 @@ private function getSchemaFolderName(Schema $schema): string * @throws Exception In case we can't create the folder because it is not permitted. */ public function createObjectFolder( - ObjectEntity $objectEntity, + ObjectEntity $objectEntity, Register|int|null $register = null, - Schema|int|null $schema = null, - string $folderPath = null + Schema|int|null $schema = null, + string $folderPath = null ): ?Node { if ($folderPath === null) { @@ -190,9 +192,9 @@ public function createObjectFolder( * @throws Exception In case we can't create the folder because it is not permitted. */ public function getObjectFolder( - ObjectEntity $objectEntity, + ObjectEntity $objectEntity, Register|int|null $register = null, - Schema|int|null $schema = null + Schema|int|null $schema = null ): ?Node { $folderPath = $this->getObjectFolderPath( @@ -224,12 +226,12 @@ public function getObjectFolder( * @throws Exception If something went wrong getting the path, a mismatch in object register/schema & function parameters register/schema for example. */ private function getObjectFolderPath( - ObjectEntity $objectEntity, + ObjectEntity $objectEntity, Register|int|null $register = null, - Schema|int|null $schema = null + Schema|int|null $schema = null ): string { - $objectRegister = (int) $objectEntity->getRegister(); + $objectRegister = (int)$objectEntity->getRegister(); if ($register === null) { $register = $objectRegister; } @@ -242,7 +244,7 @@ private function getObjectFolderPath( $register = $this->registerMapper->find($register); } - $objectSchema = (int) $objectEntity->getSchema(); + $objectSchema = (int)$objectEntity->getSchema(); if ($schema === null) { $schema = $objectSchema; } @@ -257,12 +259,12 @@ private function getObjectFolderPath( $registerFolderName = $this->getRegisterFolderName($register); // @todo maybe we want to use ShareLink here for register->folder as well? - $register->setFolder($this->getFolderLink($this::ROOT_FOLDER."/$registerFolderName")); + $register->setFolder($this->getFolderLink($this::ROOT_FOLDER . "/$registerFolderName")); $schemaFolderName = $this->getSchemaFolderName($schema); $objectFolderName = $this->getObjectFolderName($objectEntity); - return $this::ROOT_FOLDER."/$registerFolderName/$schemaFolderName/$objectFolderName"; + return $this::ROOT_FOLDER . "/$registerFolderName/$schemaFolderName/$objectFolderName"; } /** @@ -290,7 +292,7 @@ private function getObjectFolderName(ObjectEntity $objectEntity): string */ private function getFolderLink(string $folderPath): string { - $folderPath = str_replace('%2F','/', urlencode($folderPath)); + $folderPath = str_replace('%2F', '/', urlencode($folderPath)); return $this->getCurrentDomain() . "/index.php/apps/files/files?dir=$folderPath"; } @@ -345,6 +347,20 @@ private function getNode(string $path): ?Node } } + /** + * @param Node $file + * @param int $shareType + * @return IShare[] + */ + public function findShares(Node $file, int $shareType = 3): array + { + // Get the current user. + $currentUser = $this->userSession->getUser(); + $userId = $currentUser ? $currentUser->getUID() : 'Guest'; + + return $this->shareManager->getSharesBy(userId: $userId, shareType: $shareType, path: $file); + } + /** * Try to find a IShare object with given $path & $shareType. * diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 0dca209..698a20f 100644 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -517,7 +517,7 @@ public function getObjects( array $filters = [], array $sort = [], ?string $search = null, - bool $files = false, + bool $files = true, ) { // Set object type and filters if register and schema are provided @@ -1340,20 +1340,22 @@ private function hydrateFiles(ObjectEntity $object, array $files): ObjectEntity $formattedFiles = []; foreach($files as $file) { + $shares = $this->fileService->findShares($file); + $formattedFile = [ - 'id' => $file->getId(), - 'path' => $file->getPath(), - 'filename' => $file->getName(), - 'extension' => $file->getExtension(), - 'metadata' => $file->getMetadata(), - 'mimeType' => $file->getMimetype(), - 'uploaded' => (new DateTime())->setTimestamp($file->getUploadTime())->format('c'), - 'Etag' => $file->getEtag(), + 'id' => $file->getId(), + 'path' => $file->getPath(), + 'title' => $file->getName(), + 'accessUrl' => count($shares) > 0 ? $this->fileService->getShareLink($shares[0]) : null, + 'downloadUrl' => count($shares) > 0 ? $this->fileService->getShareLink($shares[0]).'/download' : null, + 'type' => $file->getMimetype(), + 'extension' => $file->getExtension(), + 'size' => $file->getSize(), + 'hash' => $file->getEtag(), + 'published' => (new DateTime())->setTimestamp($file->getCreationTime())->format('c'), + 'modified' => (new DateTime())->setTimestamp($file->getUploadTime())->format('c'), ]; - if($file->isShared() === true) { - } - $formattedFiles[] = $formattedFile; } From 8099abb98b822115b22060f0c893722dbabac213 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Tue, 21 Jan 2025 16:54:11 +0100 Subject: [PATCH 20/31] Documentation --- lib/Service/ObjectService.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 7942996..afd085c 100644 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -1312,6 +1312,9 @@ private function handleFileProperty(ObjectEntity $objectEntity, array $object, s /** * Get files for object * + * See https://nextcloud-server.netlify.app/classes/ocp-files-file for the Nextcloud documentation on the File class + * See https://nextcloud-server.netlify.app/classes/ocp-files-node for the Nextcloud documentation on the Node superclass + * * @param ObjectEntity $object The object to fetch files for. * @return Node[] The files found. * @throws \OCP\Files\NotFoundException @@ -1330,9 +1333,12 @@ public function getFiles(ObjectEntity $object): array /** * Hydrate files array with metadata. * + * See https://nextcloud-server.netlify.app/classes/ocp-files-file for the Nextcloud documentation on the File class + * See https://nextcloud-server.netlify.app/classes/ocp-files-node for the Nextcloud documentation on the Node superclass + * * @param ObjectEntity $object The object to hydrate the files array of. * @param Node[] $files The files to hydrate the files array with. - * + * * @return ObjectEntity The object with hydrated files array. */ public function hydrateFiles(ObjectEntity $object, array $files): ObjectEntity @@ -1340,6 +1346,8 @@ public function hydrateFiles(ObjectEntity $object, array $files): ObjectEntity $formattedFiles = []; foreach($files as $file) { + + // IShare documentation see https://nextcloud-server.netlify.app/classes/ocp-share-ishare $shares = $this->fileService->findShares($file); $formattedFile = [ From 41d7db61a7fdef66f921152d1626d15165b732c4 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Thu, 23 Jan 2025 15:12:55 +0100 Subject: [PATCH 21/31] Write file path instead of url in the folder property of an object Also, read the folder property from an object if the property is set, instead of generating the whole thing over and over again. --- lib/Service/FileService.php | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 03b0d7d..eae90fe 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -74,7 +74,7 @@ public function createRegisterFolder(Register|int $register): string $registerFolderName = $this->getRegisterFolderName($register); // @todo maybe we want to use ShareLink here for register->folder as well? - $register->setFolder($this->getFolderLink($this::ROOT_FOLDER . "/$registerFolderName")); + $register->setFolder($this::ROOT_FOLDER . "/$registerFolderName"); $folderPath = $this::ROOT_FOLDER . "/$registerFolderName"; $this->createFolder(folderPath: $folderPath); @@ -122,7 +122,7 @@ public function createSchemaFolder(Register|int $register, Schema|int $schema): $registerFolderName = $this->getRegisterFolderName($register); // @todo maybe we want to use ShareLink here for register->folder as well? - $register->setFolder($this->getFolderLink($this::ROOT_FOLDER . "/$registerFolderName")); + $register->setFolder($this::ROOT_FOLDER . "/$registerFolderName"); $schemaFolderName = $this->getSchemaFolderName($schema); @@ -168,7 +168,7 @@ public function createObjectFolder( // @todo Do we want to use ShareLink here? // @todo ^If so, we need to update these functions to be able to create shareLinks for folders as well (not only files) - $objectEntity->setFolder($this->getFolderLink($folderPath)); + $objectEntity->setFolder($folderPath); // // Create or find ShareLink // $share = $this->fileService->findShare(path: $filePath); @@ -197,11 +197,16 @@ public function getObjectFolder( Schema|int|null $schema = null ): ?Node { - $folderPath = $this->getObjectFolderPath( - objectEntity: $objectEntity, - register: $register, - schema: $schema - ); + if($objectEntity->getFolder() === null) { + $folderPath = $this->getObjectFolderPath( + objectEntity: $objectEntity, + register: $register, + schema: $schema + ); + } else { + $folderPath = $objectEntity->getFolder(); + } + $node = $this->getNode($folderPath); if ($node === null) { @@ -259,7 +264,7 @@ private function getObjectFolderPath( $registerFolderName = $this->getRegisterFolderName($register); // @todo maybe we want to use ShareLink here for register->folder as well? - $register->setFolder($this->getFolderLink($this::ROOT_FOLDER . "/$registerFolderName")); + $register->setFolder($this::ROOT_FOLDER . "/$registerFolderName"); $schemaFolderName = $this->getSchemaFolderName($schema); $objectFolderName = $this->getObjectFolderName($objectEntity); From a7149a1a5dcabd8fa8f5d1476e90fa0261c620d3 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Thu, 23 Jan 2025 16:26:53 +0100 Subject: [PATCH 22/31] Create root folder separately and share with openregister group create group if it not yet exists --- lib/Service/FileService.php | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index eae90fe..3bf5c16 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -18,6 +18,7 @@ use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IConfig; +use OCP\IGroupManager; use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserSession; @@ -36,6 +37,7 @@ class FileService { const ROOT_FOLDER = 'Open Registers'; + const APP_GROUP = 'openregister'; /** * Constructor for FileService @@ -54,6 +56,7 @@ public function __construct( private readonly IConfig $config, private readonly RegisterMapper $registerMapper, private readonly SchemaMapper $schemaMapper, + private readonly IGroupManager $groupManager, ) { } @@ -408,6 +411,24 @@ public function findShare(string $path, ?int $shareType = 3): ?IShare return null; } + private function shareWithGroup(int $nodeId, string $nodeType, string $target, int $permissions, string $groupId): IShare + { + $share = $this->shareManager->newShare(); + $share->setTarget(target: '/'. $target); + $share->setNodeId(fileId:$nodeId); + $share->setNodeType(type:$nodeType); + + $share->setShareType(shareType: 1); + $share->setPermissions(permissions: $permissions); + $share->setSharedBy(sharedBy:$this->userSession->getUser()->getUID()); + $share->setShareOwner(shareOwner:$this->userSession->getUser()->getUID()); + $share->setShareTime(shareTime: new DateTime()); + $share->setSharedWith(sharedWith: $groupId); + $share->setStatus(status: $share::STATUS_ACCEPTED); + + return $this->shareManager->createShare($share); + } + /** * Creates a IShare object using the $shareData array data. * @@ -503,6 +524,7 @@ public function createShareLink(string $path, ?int $shareType = 3, ?int $permiss */ public function createFolder(string $folderPath): bool { + $folderPath = trim(string: $folderPath, characters: '/'); // Get the current user. @@ -511,6 +533,24 @@ public function createFolder(string $folderPath): bool // Check if folder exists and if not create it. try { + try { + $userFolder->get(self::ROOT_FOLDER); + } catch(NotFoundException $exception) { + $rootFolder = $userFolder->newFolder(self::ROOT_FOLDER); + + if($this->groupManager->groupExists(self::APP_GROUP) === false) { + $this->groupManager->createGroup(self::APP_GROUP); + } + + $this->shareWithGroup( + nodeId: $rootFolder->getId(), + nodeType: $rootFolder->getType() === 'file' ? $rootFolder->getType() : 'folder', + target: self::ROOT_FOLDER, + permissions: 31, + groupId: self::APP_GROUP + ); + } + try { $userFolder->get(path: $folderPath); } catch (NotFoundException $e) { From ead73a6c87fd627b722ef46535c5214926f01088 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Fri, 24 Jan 2025 13:12:46 +0100 Subject: [PATCH 23/31] Add docblock, remove unused function --- lib/Service/FileService.php | 13 +++++++++++++ lib/Service/ObjectService.php | 11 ----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index 3bf5c16..edd20d3 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -411,6 +411,18 @@ public function findShare(string $path, ?int $shareType = 3): ?IShare return null; } + /** + * Share a file or folder with a user group in Nextcloud. + * + * @param int $nodeId The file or folder to share. + * @param string $nodeType 'file' or 'folder', the type of node. + * @param string $target The target folder to share the node in. + * @param int $permissions Permissions the group members will have in the folder. + * @param string $groupId The id of the group to share the folder with. + * + * @return IShare The resulting share + * @throws Exception + */ private function shareWithGroup(int $nodeId, string $nodeType, string $target, int $permissions, string $groupId): IShare { $share = $this->shareManager->newShare(); @@ -533,6 +545,7 @@ public function createFolder(string $folderPath): bool // Check if folder exists and if not create it. try { + // First, check if the root folder exists, and if not, create it and share it with the openregister group. try { $userFolder->get(self::ROOT_FOLDER); } catch(NotFoundException $exception) { diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index afd085c..f641206 100644 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -1146,17 +1146,6 @@ private function writeFile(string $fileContent, string $propertyName, ObjectEnti return $file; } - public function nodeCreatedEventFunction(NodeCreatedEvent $event): void - { - $node = $event->getNode(); - var_dump($node->getMetadata()); - if ($node instanceof \OC\Files\Node\File === true) { - $path = $node->getFileInfo()->getPath(); - $metadata = $node->getFileInfo()->getMetadata(); - var_dump($metadata); - } - } - /** * @todo * From 3e6ff0710b54ebf56f80bed3fcd1ffaf7e51e7a0 Mon Sep 17 00:00:00 2001 From: Thijn Date: Fri, 24 Jan 2025 14:22:09 +0100 Subject: [PATCH 24/31] lint-fix --- src/entities/object/object.ts | 2 +- src/store/modules/object.js | 2 +- src/views/object/ObjectDetails.vue | 14 +++++++------- src/views/object/ObjectsList.vue | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/entities/object/object.ts b/src/entities/object/object.ts index 679c71c..4c9df86 100644 --- a/src/entities/object/object.ts +++ b/src/entities/object/object.ts @@ -38,7 +38,7 @@ export class ObjectEntity implements TObject { /** * Validates the object against a schema - * @returns {SafeParseReturnType} Object containing validation result with success/error status + * @return {SafeParseReturnType} Object containing validation result with success/error status */ public validate(): SafeParseReturnType { const schema = z.object({ diff --git a/src/store/modules/object.js b/src/store/modules/object.js index f267470..b59fc87 100644 --- a/src/store/modules/object.js +++ b/src/store/modules/object.js @@ -210,7 +210,7 @@ export const useObjectStore = defineStore('object', { // FILES /** * Get files for an object - * + * * @param {number} id Object ID * @return {Promise} Promise that resolves with the object's files */ diff --git a/src/views/object/ObjectDetails.vue b/src/views/object/ObjectDetails.vue index 09f97c7..151a26f 100644 --- a/src/views/object/ObjectDetails.vue +++ b/src/views/object/ObjectDetails.vue @@ -40,7 +40,7 @@ import { objectStore, navigationStore } from '../../store/store.js' Delete - {{ success ? 'Close' : 'Cancel' }} - @@ -103,9 +103,9 @@ export default { this.duration || undefined, ) this.success = true + this.error = null this.closeModalTimeout = setTimeout(this.closeModal, 2000) } catch (error) { - this.success = false this.error = error.message || 'Failed to lock object' } finally { this.loading = false From 750896fabaf04c45af3d6848a22ef053d8ee364e Mon Sep 17 00:00:00 2001 From: Remko Date: Mon, 27 Jan 2025 13:48:38 +0100 Subject: [PATCH 26/31] Lint fix --- src/views/object/ObjectsList.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/views/object/ObjectsList.vue b/src/views/object/ObjectsList.vue index 30625ea..932a8fe 100644 --- a/src/views/object/ObjectsList.vue +++ b/src/views/object/ObjectsList.vue @@ -106,7 +106,6 @@ import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue' import Upload from 'vue-material-design-icons/Upload.vue' import LockOutline from 'vue-material-design-icons/LockOutline.vue' import LockOpenOutline from 'vue-material-design-icons/LockOpenOutline.vue' -import { showSuccess, showError } from '@nextcloud/dialogs' export default { name: 'ObjectsList', From 30dcdfdbdfc150aef180b53bd5d4a13fe3592e07 Mon Sep 17 00:00:00 2001 From: Remko Date: Mon, 27 Jan 2025 13:57:23 +0100 Subject: [PATCH 27/31] cleanup --- src/modals/object/LockObject.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modals/object/LockObject.vue b/src/modals/object/LockObject.vue index 0c1d181..9ad6550 100644 --- a/src/modals/object/LockObject.vue +++ b/src/modals/object/LockObject.vue @@ -25,7 +25,7 @@ import { objectStore, navigationStore } from '../../store/store.js' {{ success ? 'Close' : 'Cancel' }}