Skip to content

Commit

Permalink
Merge pull request #145 from ConductionNL/development
Browse files Browse the repository at this point in the history
Development to main
  • Loading branch information
remko48 authored Jan 27, 2025
2 parents d31f52b + ddd45ee commit 3c1ccff
Show file tree
Hide file tree
Showing 28 changed files with 2,031 additions and 53 deletions.
Empty file modified appinfo/info.xml
100755 → 100644
Empty file.
4 changes: 4 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
['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' => '[^/]+']],
['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'],
],
];
98 changes: 98 additions & 0 deletions docs/object/object.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 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
- Ability to revert to any previous version
- Detailed change tracking between versions

### 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
- Revert to previous versions

## 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:
201 changes: 197 additions & 4 deletions lib/Controller/ObjectsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -86,7 +88,8 @@ public function index(ObjectService $objectService, SearchService $searchService
$filters = $searchService->unsetSpecialQueryParams(filters: $filters);

// @todo: figure out how to use extend here
$results = $this->objectEntityMapper->findAll(filters: $filters);
$results = $objectService->getObjects(objectType: 'objectEntity', filters: $filters);
// $results = $this->objectEntityMapper->findAll(filters: $filters);

// We dont want to return the entity, but the object (and kant reley on the normal serilzier)
foreach ($results as $key => $result) {
Expand Down Expand Up @@ -160,6 +163,13 @@ public function create(ObjectService $objectService): JSONResponse
// Save the object
try {
$objectEntity = $objectService->saveObject(register: $data['register'], schema: $data['schema'], object: $object);

// Unlock the object after saving
try {
$this->objectEntityMapper->unlockObject($objectEntity->getId());
} catch (\Exception $e) {
// Ignore unlock errors since the save was successful
}
} catch (ValidationException $exception) {
$formatter = new ErrorFormatter();
return new JSONResponse(['message' => $exception->getMessage(), 'validationErrors' => $formatter->format($exception->getErrors())], 400);
Expand Down Expand Up @@ -206,6 +216,13 @@ public function update(int $id, ObjectService $objectService): JSONResponse
// save it
try {
$objectEntity = $objectService->saveObject(register: $data['register'], schema: $data['schema'], object: $data['object']);

// Unlock the object after saving
try {
$this->objectEntityMapper->unlockObject($objectEntity->getId());
} catch (\Exception $e) {
// Ignore unlock errors since the save was successful
}
} catch (ValidationException $exception) {
$formatter = new ErrorFormatter();
return new JSONResponse(['message' => $exception->getMessage(), 'validationErrors' => $formatter->format($exception->getErrors())], 400);
Expand Down Expand Up @@ -328,8 +345,6 @@ public function logs(int $id): JSONResponse
}
}



/**
* Retrieves all available mappings
*
Expand Down Expand Up @@ -380,4 +395,182 @@ 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 using AuditTrailMapper instead
$revertedObject = $this->auditTrailMapper->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);
}
}

/**
* 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);
}
}
}
Loading

0 comments on commit 3c1ccff

Please sign in to comment.