From 04d12bd661a6347dc4f56dfc382522f9408f27b5 Mon Sep 17 00:00:00 2001
From: Wilco Louwerse
Date: Mon, 7 Oct 2024 16:37:27 +0200
Subject: [PATCH 01/38] Upload json to create schema (& basics for Download
schema as json)
---
appinfo/routes.php | 5 +-
lib/Controller/SchemasController.php | 155 ++++++++++++++++++++++++---
lib/Service/DownloadService.php | 26 +++++
lib/Service/UploadService.php | 39 +++++++
4 files changed, 207 insertions(+), 18 deletions(-)
create mode 100644 lib/Service/DownloadService.php
create mode 100644 lib/Service/UploadService.php
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 93948ea..687dfbb 100755
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -8,7 +8,10 @@
'Objects' => ['url' => 'api/objects'],
],
'routes' => [
- ['name' => 'dashboard#page', 'url' => '/', 'verb' => 'GET'],
+ ['name' => 'dashboard#page', 'url' => '/', 'verb' => 'GET'],
['name' => 'registers#objects', 'url' => '/api/registers-objects/{register}/{schema}', 'verb' => 'GET'],
+ ['name' => 'schemas#upload', 'url' => '/api/schemas/upload', 'verb' => 'POST'],
+// ['name' => 'schemas#upload', 'url' => '/api/schemas/upload', 'verb' => 'PUT'],
+ ['name' => 'schemas#download', 'url' => '/api/schemas/{id}/download', 'verb' => 'GET'],
],
];
diff --git a/lib/Controller/SchemasController.php b/lib/Controller/SchemasController.php
index 82f4223..5455136 100644
--- a/lib/Controller/SchemasController.php
+++ b/lib/Controller/SchemasController.php
@@ -2,10 +2,12 @@
namespace OCA\OpenRegister\Controller;
+use OCA\OpenRegister\Service\DownloadService;
use OCA\OpenRegister\Service\ObjectService;
use OCA\OpenRegister\Service\SearchService;
use OCA\OpenRegister\Db\Schema;
use OCA\OpenRegister\Db\SchemaMapper;
+use OCA\OpenRegister\Service\UploadService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\JSONResponse;
@@ -25,7 +27,9 @@ public function __construct(
$appName,
IRequest $request,
private readonly IAppConfig $config,
- private readonly SchemaMapper $schemaMapper
+ private readonly SchemaMapper $schemaMapper,
+ private readonly DownloadService $downloadService,
+ private readonly UploadService $uploadService
)
{
parent::__construct($appName, $request);
@@ -33,7 +37,7 @@ public function __construct(
/**
* Returns the template of the main app's page
- *
+ *
* This method renders the main page of the application, adding any necessary data to the template.
*
* @NoAdminRequired
@@ -42,17 +46,17 @@ public function __construct(
* @return TemplateResponse The rendered template response
*/
public function page(): TemplateResponse
- {
+ {
return new TemplateResponse(
'openconnector',
'index',
[]
);
}
-
+
/**
* Retrieves a list of all schemas
- *
+ *
* This method returns a JSON response containing an array of all schemas in the system.
*
* @NoAdminRequired
@@ -69,12 +73,12 @@ public function index(ObjectService $objectService, SearchService $searchService
$searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch);
$filters = $searchService->unsetSpecialQueryParams(filters: $filters);
- return new JSONResponse(['results' => $this->schemaMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]);
+ return new JSONResponse(['results' => $this->schemaMapper->findAll(filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]);
}
/**
* Retrieves a single schema by its ID
- *
+ *
* This method returns a JSON response containing the details of a specific schema.
*
* @NoAdminRequired
@@ -94,7 +98,7 @@ public function show(string $id): JSONResponse
/**
* Creates a new schema
- *
+ *
* This method creates a new schema based on POST data.
*
* @NoAdminRequired
@@ -111,23 +115,23 @@ public function create(): JSONResponse
unset($data[$key]);
}
}
-
+
if (isset($data['id'])) {
unset($data['id']);
}
-
+
return new JSONResponse($this->schemaMapper->createFromArray(object: $data));
}
/**
* Updates an existing schema
- *
+ *
* This method updates an existing schema based on its ID.
*
* @NoAdminRequired
* @NoCSRFRequired
*
- * @param string $id The ID of the schema to update
+ * @param int $id The ID of the schema to update
* @return JSONResponse A JSON response containing the updated schema details
*/
public function update(int $id): JSONResponse
@@ -142,24 +146,141 @@ public function update(int $id): JSONResponse
if (isset($data['id'])) {
unset($data['id']);
}
- return new JSONResponse($this->schemaMapper->updateFromArray(id: (int) $id, object: $data));
+ return new JSONResponse($this->schemaMapper->updateFromArray(id: $id, object: $data));
}
/**
* Deletes a schema
- *
+ *
* This method deletes a schema based on its ID.
*
* @NoAdminRequired
* @NoCSRFRequired
*
- * @param string $id The ID of the schema to delete
+ * @param int $id The ID of the schema to delete
* @return JSONResponse An empty JSON response
*/
public function destroy(int $id): JSONResponse
{
- $this->schemaMapper->delete($this->schemaMapper->find((int) $id));
+ $this->schemaMapper->delete($this->schemaMapper->find(id: $id));
return new JSONResponse([]);
}
-}
\ No newline at end of file
+
+ /**
+ * Transforms a php array (decoded json input) to a php array that can be used to create a new Schema object.
+ *
+ * @param array $jsonArray A php array (decoded json input) to translate/map.
+ *
+ * @return array The transformed array.
+ */
+ private function jsonToSchema(array $jsonArray): array
+ {
+ $schemaObject = $this->uploadService->mapJsonSchema($jsonArray);
+
+ // @todo Should we do custom mappings here or in the 'abstract' function in UploadService?
+ $schemaObject = array_merge($schemaObject, [
+ 'summary' => $jsonArray['description'],
+ 'required' => $jsonArray['required'],
+ 'properties' => $jsonArray['properties']
+ ]);
+
+ return $schemaObject;
+ }
+
+ /**
+ * Creates a new Schema object using a json text/string as input. Uses 'json' from POST body.
+ * @todo Optionally a 'url' can be used instead to get a json file from somewhere else and use that instead.
+ * @todo Or a .json file can be uploaded using key 'file'.
+ * @todo move most of this code to a (new?) UploadService and make it even more abstract and reusable?
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @return JSONResponse
+ */
+ public function upload(): JSONResponse
+ {
+ $data = $this->request->getParams();
+
+ foreach ($data as $key => $value) {
+ if (str_starts_with($key, '_')) {
+ unset($data[$key]);
+ }
+ }
+
+ // Define the allowed keys
+ $allowedKeys = ['file', 'url', 'json'];
+
+ // Find which of the allowed keys are in the array
+ $matchingKeys = array_intersect_key($data, array_flip($allowedKeys));
+
+ // Check if there is exactly one matching key
+ if (count($matchingKeys) === 0) {
+ return new JSONResponse(data: ['error' => 'Missing one of these keys in your POST body: file, url or json.'], statusCode: 400);
+ }
+ if (count($matchingKeys) > 1) {
+ return new JSONResponse(data: ['error' => 'Please use one of these keys: file, url or json, not multiple.'], statusCode: 400);
+ }
+
+ if (empty($data['file']) === false) {
+ // @todo use .json file content from POST as $json
+ $data['json'] = [];
+ }
+
+ if (empty($data['url']) === false) {
+ // @todo get .json file using CallService and use that as $json
+ $data['json'] = [];
+ }
+
+ $jsonArray = $data['json'];
+
+ $schemaObject = $this->JsonToSchema(jsonArray: $jsonArray);
+
+ $schema = $this->schemaMapper->createFromArray(object: $schemaObject);
+
+ return new JSONResponse($schema);
+ }
+
+ /**
+ * Creates and return a json file for a Schema.
+ * @todo move most of this code to DownloadService and make it even more Abstract using Entity->jsonSerialize instead of Schema->jsonSerialize, etc.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @param int $id The ID of the schema to return json file for
+ * @return JSONResponse A json Response containing the json
+ */
+ public function download(int $id): JSONResponse
+ {
+ try {
+ $schema = $this->schemaMapper->find($id);
+ } catch (DoesNotExistException $exception) {
+ return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404);
+ }
+
+ $contentType = $this->request->getHeader('Content-Type');
+
+ if (empty($contentType) === true) {
+ return new JSONResponse(data: ['error' => 'Request is missing header Content-Type'], statusCode: 400);
+ }
+
+ switch ($contentType) {
+ case 'application/json':
+ $type = 'json';
+ $responseData = [
+ 'jsonArray' => $schema->jsonSerialize(),
+ 'jsonString' => json_encode($schema->jsonSerialize())
+ ];
+ break;
+ default:
+ return new JSONResponse(data: ['error' => "The Content-Type $contentType is not supported."], statusCode: 400);
+ }
+
+ // @todo Create a downloadable json file and return it.
+ $file = $this->downloadService->download(type: $type);
+
+ return new JSONResponse($responseData);
+ }
+}
diff --git a/lib/Service/DownloadService.php b/lib/Service/DownloadService.php
new file mode 100644
index 0000000..981b62d
--- /dev/null
+++ b/lib/Service/DownloadService.php
@@ -0,0 +1,26 @@
+ $input['title'] ?? '',
+ 'reference' => $input['$id'] ?? '',
+ 'version' => $input['version'] ?? '',
+ 'description' => $input['description'] ?? ''
+ ];
+ }
+
+}
From a900f9d603085a937ae62fb417ed410286d184b4 Mon Sep 17 00:00:00 2001
From: Wilco Louwerse
Date: Mon, 7 Oct 2024 16:39:02 +0200
Subject: [PATCH 02/38] Inline this variable in SchemasController
---
lib/Controller/SchemasController.php | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/lib/Controller/SchemasController.php b/lib/Controller/SchemasController.php
index 5455136..4a59788 100644
--- a/lib/Controller/SchemasController.php
+++ b/lib/Controller/SchemasController.php
@@ -179,13 +179,11 @@ private function jsonToSchema(array $jsonArray): array
$schemaObject = $this->uploadService->mapJsonSchema($jsonArray);
// @todo Should we do custom mappings here or in the 'abstract' function in UploadService?
- $schemaObject = array_merge($schemaObject, [
+ return array_merge($schemaObject, [
'summary' => $jsonArray['description'],
'required' => $jsonArray['required'],
'properties' => $jsonArray['properties']
]);
-
- return $schemaObject;
}
/**
From b09232d580c27d028aa638b9330aa6bf23c1e14d Mon Sep 17 00:00:00 2001
From: Wilco Louwerse
Date: Mon, 7 Oct 2024 16:56:12 +0200
Subject: [PATCH 03/38] add working code for url instead of json in post
schemas/upload
---
lib/Controller/SchemasController.php | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/lib/Controller/SchemasController.php b/lib/Controller/SchemasController.php
index 4a59788..5aff521 100644
--- a/lib/Controller/SchemasController.php
+++ b/lib/Controller/SchemasController.php
@@ -2,6 +2,7 @@
namespace OCA\OpenRegister\Controller;
+use GuzzleHttp\Client;
use OCA\OpenRegister\Service\DownloadService;
use OCA\OpenRegister\Service\ObjectService;
use OCA\OpenRegister\Service\SearchService;
@@ -32,6 +33,7 @@ public function __construct(
private readonly UploadService $uploadService
)
{
+ $this->client = new Client([]);
parent::__construct($appName, $request);
}
@@ -227,8 +229,20 @@ public function upload(): JSONResponse
}
if (empty($data['url']) === false) {
- // @todo get .json file using CallService and use that as $json
- $data['json'] = [];
+ // @todo move to function (cleanup)
+ try {
+ $response = $this->client->request('GET', $data['url']);
+ } catch (GuzzleHttp\Exception\BadResponseException $e) {
+ $response = $e->getResponse();
+ return new JSONResponse(data: ['error' => 'Failed to do a GET api-call on url: '.$data['url']], statusCode: 400);
+ }
+
+ $responseBody = $response->getBody()->getContents();
+ // @todo use Conten-Type header in response instead?
+ if (is_string($responseBody) === true) {
+ $responseBody = json_decode(json: $responseBody, associative: true);
+ }
+ $data['json'] = $responseBody;
}
$jsonArray = $data['json'];
From 2673120a67163fb5f28efc4e6569b7b635d5277e Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Mon, 7 Oct 2024 21:24:16 +0200
Subject: [PATCH 04/38] All the changes
---
appinfo/routes.php | 1 +
lib/Controller/ObjectsController.php | 41 ++++++-
lib/Db/ObjectAuditLog.php | 78 ++++++++++++++
lib/Db/ObjectAuditLogMapper.php | 85 +++++++++++++++
lib/Db/ObjectEntity.php | 3 +
lib/Db/ObjectEntityMapper.php | 13 ++-
lib/Db/Register.php | 6 ++
lib/Db/RegisterMapper.php | 23 ++--
lib/Db/Schema.php | 11 +-
lib/Db/SchemaMapper.php | 22 ++--
lib/Db/Source.php | 6 ++
lib/Db/SourceMapper.php | 22 ++--
lib/Migration/Version1Date20240924200009.php | 31 +++++-
lib/Service/ValidationService.php | 22 ++++
src/modals/Modals.vue | 3 +
src/modals/schema/EditSchema.vue | 41 +------
src/modals/schema/UploadSchema.vue | 108 +++++++++++++++++++
src/store/modules/schema.js | 39 +++++++
src/views/object/ObjectDetails.vue | 2 +-
src/views/schema/SchemaDetails.vue | 18 ++++
20 files changed, 503 insertions(+), 72 deletions(-)
create mode 100644 lib/Db/ObjectAuditLog.php
create mode 100644 lib/Db/ObjectAuditLogMapper.php
create mode 100755 lib/Service/ValidationService.php
create mode 100644 src/modals/schema/UploadSchema.vue
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 93948ea..6a2440b 100755
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -10,5 +10,6 @@
'routes' => [
['name' => 'dashboard#page', 'url' => '/', 'verb' => 'GET'],
['name' => 'registers#objects', 'url' => '/api/registers-objects/{register}/{schema}', 'verb' => 'GET'],
+ ['name' => 'objects#logs', 'url' => '/api/objects-logs/{id}', 'verb' => 'GET'],
],
];
diff --git a/lib/Controller/ObjectsController.php b/lib/Controller/ObjectsController.php
index 9cc6802..032a90e 100644
--- a/lib/Controller/ObjectsController.php
+++ b/lib/Controller/ObjectsController.php
@@ -6,6 +6,7 @@
use OCA\OpenRegister\Service\SearchService;
use OCA\OpenRegister\Db\ObjectEntity;
use OCA\OpenRegister\Db\ObjectEntityMapper;
+use OCA\OpenRegister\Db\ObjectAuditLogMapper;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\JSONResponse;
@@ -25,7 +26,8 @@ public function __construct(
$appName,
IRequest $request,
private readonly IAppConfig $config,
- private readonly ObjectEntityMapper $objectEntityMapper
+ private readonly ObjectEntityMapper $objectEntityMapper,
+ private readonly ObjectAuditLogMapper $objectAuditLogMapper
)
{
parent::__construct($appName, $request);
@@ -162,4 +164,41 @@ public function destroy(int $id): JSONResponse
return new JSONResponse([]);
}
+
+ /**
+ * Retrieves call logs for a object
+ *
+ * This method returns all the call logs associated with a object based on its ID.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @param int $id The ID of the object to retrieve logs for
+ * @return JSONResponse A JSON response containing the call logs
+ */
+ public function contracts(int $id): JSONResponse
+ {
+ // @todo
+ }
+
+ /**
+ * Retrieves call logs for a object
+ *
+ * This method returns all the call logs associated with a object based on its ID.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @param int $id The ID of the object to retrieve logs for
+ * @return JSONResponse A JSON response containing the call logs
+ */
+ public function logs(int $id): JSONResponse
+ {
+ try {
+ $jobLogs = $this->objectAuditLogMapper->findAll(null, null, ['object_id' => $id]);
+ return new JSONResponse($jobLogs);
+ } catch (DoesNotExistException $e) {
+ return new JSONResponse(['error' => 'Logs not found'], 404);
+ }
+ }
}
\ No newline at end of file
diff --git a/lib/Db/ObjectAuditLog.php b/lib/Db/ObjectAuditLog.php
new file mode 100644
index 0000000..211bc3f
--- /dev/null
+++ b/lib/Db/ObjectAuditLog.php
@@ -0,0 +1,78 @@
+addType(fieldName:'uuid', type: 'string');
+ $this->addType(fieldName:'objectId', type: 'string');
+ $this->addType(fieldName:'changes', type: 'json');
+ $this->addType(fieldName:'expires', type: 'datetime');
+ $this->addType(fieldName:'created', type: 'datetime');
+ $this->addType(fieldName:'userId', type: 'string');
+ }
+
+ public function getJsonFields(): array
+ {
+ return array_keys(
+ array_filter($this->getFieldTypes(), function ($field) {
+ return $field === 'json';
+ })
+ );
+ }
+
+ public function hydrate(array $object): self
+ {
+ $jsonFields = $this->getJsonFields();
+
+ foreach($object as $key => $value) {
+ if (in_array($key, $jsonFields) === true && $value === []) {
+ $value = null;
+ }
+
+ $method = 'set'.ucfirst($key);
+
+ try {
+ $this->$method($value);
+ } catch (\Exception $exception) {
+ }
+ }
+
+ return $this;
+ }
+
+ public function jsonSerialize(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'uuid' => $this->uuid,
+ 'objectId' => $this->objectId,
+ 'changes' => $this->changes,
+ 'expires' => isset($this->expires) ? $this->expires->format('c') : null,
+ 'created' => isset($this->created) ? $this->created->format('c') : null,
+ 'userId' => $this->userId
+ ];
+ }
+}
\ No newline at end of file
diff --git a/lib/Db/ObjectAuditLogMapper.php b/lib/Db/ObjectAuditLogMapper.php
new file mode 100644
index 0000000..1399de9
--- /dev/null
+++ b/lib/Db/ObjectAuditLogMapper.php
@@ -0,0 +1,85 @@
+db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from('openregister_object_audit_logs')
+ ->where(
+ $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
+ );
+
+ return $this->findEntity(query: $qb);
+ }
+
+ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array
+ {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from('openregister_object_audit_logs')
+ ->setMaxResults($limit)
+ ->setFirstResult($offset);
+
+ foreach($filters as $filter => $value) {
+ if ($value === 'IS NOT NULL') {
+ $qb->andWhere($qb->expr()->isNotNull($filter));
+ } elseif ($value === 'IS NULL') {
+ $qb->andWhere($qb->expr()->isNull($filter));
+ } else {
+ $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value)));
+ }
+ }
+
+ if (!empty($searchConditions)) {
+ $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')');
+ foreach ($searchParams as $param => $value) {
+ $qb->setParameter($param, $value);
+ }
+ }
+
+ return $this->findEntities(query: $qb);
+ }
+
+ public function createFromArray(array $object): ObjectAuditLog
+ {
+ $obj = new ObjectAuditLog();
+ $obj->hydrate($object);
+ // Set uuid
+ if($obj->getUuid() === null){
+ $obj->setUuid(Uuid::v4());
+ }
+
+ return $this->insert(entity: $obj);
+ }
+
+ public function updateFromArray(int $id, array $object): ObjectAuditLog
+ {
+ $obj = $this->find($id);
+ $obj->hydrate($object);
+
+ // Set or update the version
+ $version = explode('.', $obj->getVersion());
+ $version[2] = (int)$version[2] + 1;
+ $obj->setVersion(implode('.', $version));
+
+ return $this->update($obj);
+ }
+}
diff --git a/lib/Db/ObjectEntity.php b/lib/Db/ObjectEntity.php
index b8a56e0..6cb3359 100644
--- a/lib/Db/ObjectEntity.php
+++ b/lib/Db/ObjectEntity.php
@@ -9,6 +9,7 @@
class ObjectEntity extends Entity implements JsonSerializable
{
protected ?string $uuid = null;
+ protected ?string $version = null;
protected ?string $register = null;
protected ?string $schema = null;
protected ?array $object = [];
@@ -17,6 +18,7 @@ class ObjectEntity extends Entity implements JsonSerializable
public function __construct() {
$this->addType(fieldName:'uuid', type: 'string');
+ $this->addType(fieldName:'version', type: 'string');
$this->addType(fieldName:'register', type: 'string');
$this->addType(fieldName:'schema', type: 'string');
$this->addType(fieldName:'object', type: 'json');
@@ -64,6 +66,7 @@ public function jsonSerialize(): array
'id' => $this->id,
'uuid' => $this->uuid,
'register' => $this->register,
+ 'version' => $this->version,
'schema' => $this->schema,
'object' => $this->object,
'updated' => isset($this->updated) ? $this->updated->format('c') : null,
diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php
index 59bc128..5af4b1f 100644
--- a/lib/Db/ObjectEntityMapper.php
+++ b/lib/Db/ObjectEntityMapper.php
@@ -131,20 +131,23 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters
public function createFromArray(array $object): ObjectEntity
{
$obj = new ObjectEntity();
- $obj->hydrate(object: $object);
+ $obj->hydrate($object);
+ // Set uuid
if($obj->getUuid() === null){
$obj->setUuid(Uuid::v4());
}
- return $this->insert(entity: $obj);
+ return $this->insert($obj);
}
public function updateFromArray(int $id, array $object): ObjectEntity
{
$obj = $this->find($id);
$obj->hydrate($object);
- if($obj->getUuid() === null){
- $obj->setUuid(Uuid::v4());
- }
+
+ // Set or update the version
+ $version = explode('.', $obj->getVersion());
+ $version[2] = (int)$version[2] + 1;
+ $obj->setVersion(implode('.', $version));
return $this->update($obj);
}
diff --git a/lib/Db/Register.php b/lib/Db/Register.php
index 4d289fc..6ec97d4 100644
--- a/lib/Db/Register.php
+++ b/lib/Db/Register.php
@@ -8,8 +8,10 @@
class Register extends Entity implements JsonSerializable
{
+ protected ?string $uuid = null;
protected ?string $title = null;
protected ?string $description = null;
+ protected ?string $version = null;
protected ?array $schemas = [];
protected ?string $source = null;
protected ?string $tablePrefix = null;
@@ -17,8 +19,10 @@ class Register extends Entity implements JsonSerializable
protected ?DateTime $created = null;
public function __construct() {
+ $this->addType('uuid', 'string');
$this->addType(fieldName: 'title', type: 'string');
$this->addType(fieldName: 'description', type: 'string');
+ $this->addType(fieldName: 'version', type: 'string');
$this->addType(fieldName: 'schemas', type: 'json');
$this->addType(fieldName: 'source', type: 'string');
$this->addType(fieldName: 'tablePrefix', type: 'string');
@@ -64,8 +68,10 @@ public function jsonSerialize(): array
{
return [
'id' => $this->id,
+ 'uuid' => $this->uuid,
'title' => $this->title,
'description' => $this->description,
+ 'version' => $this->version,
'schemas' => $this->schemas,
'source' => $this->source,
'tablePrefix' => $this->tablePrefix,
diff --git a/lib/Db/RegisterMapper.php b/lib/Db/RegisterMapper.php
index 2e16cb2..8e18fac 100644
--- a/lib/Db/RegisterMapper.php
+++ b/lib/Db/RegisterMapper.php
@@ -7,6 +7,7 @@
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
+use Symfony\Component\Uid\Uuid;
class RegisterMapper extends QBMapper
{
@@ -59,16 +60,26 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters
public function createFromArray(array $object): Register
{
- $register = new Register();
- $register->hydrate(object: $object);
- return $this->insert(entity: $register);
+ $obj = new Register();
+ $obj->hydrate($object);
+ // Set uuid
+ if($obj->getUuid() === null){
+ $obj->setUuid(Uuid::v4());
+ }
+
+ return $this->insert(entity: $obj);
}
public function updateFromArray(int $id, array $object): Register
{
- $register = $this->find($id);
- $register->hydrate($object);
+ $obj = $this->find($id);
+ $obj->hydrate($object);
+
+ // Set or update the version
+ $version = explode('.', $obj->getVersion());
+ $version[2] = (int)$version[2] + 1;
+ $obj->setVersion(implode('.', $version));
- return $this->update($register);
+ return $this->update($obj);
}
}
diff --git a/lib/Db/Schema.php b/lib/Db/Schema.php
index 91afec6..80e3efb 100644
--- a/lib/Db/Schema.php
+++ b/lib/Db/Schema.php
@@ -8,21 +8,21 @@
class Schema extends Entity implements JsonSerializable
{
+ protected ?string $uuid = null;
protected ?string $title = null;
- protected ?string $version = null;
protected ?string $description = null;
+ protected ?string $version = null;
protected ?string $summary = null;
protected ?array $required = [];
protected ?array $properties = [];
- protected ?array $archive = [];
- protected ?string $source = null;
protected ?DateTime $updated = null;
protected ?DateTime $created = null;
public function __construct() {
+ $this->addType('uuid', 'string');
$this->addType(fieldName: 'title', type: 'string');
- $this->addType(fieldName: 'version', type: 'string');
$this->addType(fieldName: 'description', type: 'string');
+ $this->addType(fieldName: 'version', type: 'string');
$this->addType(fieldName: 'summary', type: 'string');
$this->addType(fieldName: 'required', type: 'json');
$this->addType(fieldName: 'properties', type: 'json');
@@ -93,9 +93,10 @@ public function jsonSerialize(): array
$array = [
'id' => $this->id,
+ 'uuid' => $this->uuid,
'title' => $this->title,
- 'version' => $this->version,
'description' => $this->description,
+ 'version' => $this->version,
'summary' => $this->summary,
'required' => $this->required,
'properties' => $properties,
diff --git a/lib/Db/SchemaMapper.php b/lib/Db/SchemaMapper.php
index 5750eea..3f7761f 100644
--- a/lib/Db/SchemaMapper.php
+++ b/lib/Db/SchemaMapper.php
@@ -7,6 +7,7 @@
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
+use Symfony\Component\Uid\Uuid;
class SchemaMapper extends QBMapper
{
@@ -59,16 +60,25 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters
public function createFromArray(array $object): Schema
{
- $schema = new Schema();
- $schema->hydrate(object: $object);
- return $this->insert(entity: $schema);
+ $obj = new Schema();
+ $obj->hydrate($object);
+ // Set uuid
+ if($obj->getUuid() === null){
+ $obj->setUuid(Uuid::v4());
+ }
+ return $this->insert(entity: $obj);
}
public function updateFromArray(int $id, array $object): Schema
{
- $schema = $this->find($id);
- $schema->hydrate($object);
+ $obj = $this->find($id);
+ $obj->hydrate($object);
+
+ // Set or update the version
+ $version = explode('.', $obj->getVersion());
+ $version[2] = (int)$version[2] + 1;
+ $obj->setVersion(implode('.', $version));
- return $this->update($schema);
+ return $this->update($obj);
}
}
diff --git a/lib/Db/Source.php b/lib/Db/Source.php
index 1a8b928..697e7f2 100644
--- a/lib/Db/Source.php
+++ b/lib/Db/Source.php
@@ -8,16 +8,20 @@
class Source extends Entity implements JsonSerializable
{
+ protected ?string $uuid = null;
protected ?string $title = null;
protected ?string $description = null;
+ protected ?string $version = null;
protected ?string $databaseUrl = null;
protected ?string $type = null;
protected ?DateTime $updated = null;
protected ?DateTime $created = null;
public function __construct() {
+ $this->addType('uuid', 'string');
$this->addType(fieldName: 'title', type: 'string');
$this->addType(fieldName: 'description', type: 'string');
+ $this->addType(fieldName: 'version', type: 'string');
$this->addType(fieldName: 'databaseUrl', type: 'string');
$this->addType(fieldName: 'type', type: 'string');
$this->addType(fieldName: 'updated', type: 'datetime');
@@ -60,8 +64,10 @@ public function jsonSerialize(): array
{
return [
'id' => $this->id,
+ 'uuid' => $this->uuid,
'title' => $this->title,
'description' => $this->description,
+ 'version' => $this->version,
'databaseUrl' => $this->databaseUrl,
'type' => $this->type,
'updated' => isset($this->updated) ? $this->updated->format('c') : null,
diff --git a/lib/Db/SourceMapper.php b/lib/Db/SourceMapper.php
index 65034c7..cebef6e 100644
--- a/lib/Db/SourceMapper.php
+++ b/lib/Db/SourceMapper.php
@@ -7,6 +7,7 @@
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
+use Symfony\Component\Uid\Uuid;
class SourceMapper extends QBMapper
{
@@ -59,16 +60,25 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters
public function createFromArray(array $object): Source
{
- $source = new Source();
- $source->hydrate(object: $object);
- return $this->insert(entity: $source);
+ $obj = new Source();
+ $obj->hydrate($object);
+ // Set uuid
+ if($obj->getUuid() === null){
+ $obj->setUuid(Uuid::v4());
+ }
+ return $this->insert(entity: $obj);
}
public function updateFromArray(int $id, array $object): Source
{
- $source = $this->find($id);
- $source->hydrate($object);
+ $obj = $this->find($id);
+ $obj->hydrate($object);
+
+ // Set or update the version
+ $version = explode('.', $obj->getVersion());
+ $version[2] = (int)$version[2] + 1;
+ $obj->setVersion(implode('.', $version));
- return $this->update($source);
+ return $this->update($obj);
}
}
diff --git a/lib/Migration/Version1Date20240924200009.php b/lib/Migration/Version1Date20240924200009.php
index de0db7a..cb226f8 100755
--- a/lib/Migration/Version1Date20240924200009.php
+++ b/lib/Migration/Version1Date20240924200009.php
@@ -41,8 +41,10 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
if (!$schema->hasTable('openregister_sources')) {
$table = $schema->createTable('openregister_sources');
$table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true]);
+ $table->addColumn('uuid', Types::STRING, ['notnull' => true, 'length' => 255]);
$table->addColumn('title', Types::STRING, ['notnull' => true, 'length' => 255]);
$table->addColumn('description', Types::TEXT, ['notnull' => false]);
+ $table->addColumn('version', Types::STRING, ['notnull' => true, 'length' => 255, 'default' => '0.0.1']);
$table->addColumn('database_url', Types::STRING, ['notnull' => true, 'length' => 255]);
$table->addColumn('type', Types::STRING, ['notnull' => true, 'length' => 64]);
$table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']);
@@ -51,13 +53,15 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
$table->setPrimaryKey(['id']);
$table->addIndex(['title'], 'register_sources_title_index');
$table->addIndex(['type'], 'register_sources_type_index');
+ $table->addIndex(['uuid'], 'register_sources_uuid_index');
}
if (!$schema->hasTable('openregister_schemas')) {
$table = $schema->createTable('openregister_schemas');
$table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true]);
+ $table->addColumn('uuid', Types::STRING, ['notnull' => true, 'length' => 255]);
+ $table->addColumn('version', Types::STRING, ['notnull' => true, 'length' => 255, 'default' => '0.0.1']);
$table->addColumn('title', Types::STRING, ['notnull' => true, 'length' => 255]);
- $table->addColumn('version', Types::STRING, ['notnull' => true, 'length' => 64]);
$table->addColumn('description', Types::TEXT, ['notnull' => false]);
$table->addColumn('summary', Types::TEXT, ['notnull' => false]);
$table->addColumn('required', Types::JSON, ['notnull' => false]);
@@ -67,12 +71,14 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
$table->setPrimaryKey(['id']);
$table->addIndex(['title'], 'register_schemas_title_index');
- $table->addIndex(['source'], 'register_schemas_source_index');
+ $table->addIndex(['uuid'], 'register_schemas_uuid_index');
}
if (!$schema->hasTable('openregister_registers')) {
$table = $schema->createTable('openregister_registers');
$table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true]);
+ $table->addColumn('uuid', Types::STRING, ['notnull' => true, 'length' => 255]);
+ $table->addColumn('version', Types::STRING, ['notnull' => true, 'length' => 255, 'default' => '0.0.1']);
$table->addColumn('title', Types::STRING, ['notnull' => true, 'length' => 255]);
$table->addColumn('description', Types::TEXT, ['notnull' => false]);
$table->addColumn('schemas', Types::JSON, ['notnull' => false]);
@@ -84,12 +90,14 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
$table->setPrimaryKey(['id']);
$table->addIndex(['title'], 'registers_title_index');
$table->addIndex(['source'], 'registers_source_index');
+ $table->addIndex(['uuid'], 'registers_uuid_index');
}
if (!$schema->hasTable('openregister_objects')) {
$table = $schema->createTable('openregister_objects');
$table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true]);
$table->addColumn('uuid', Types::STRING, ['notnull' => true, 'length' => 255]);
+ $table->addColumn('version', Types::STRING, ['notnull' => true, 'length' => 255, 'default' => '0.0.1']);
$table->addColumn('register', Types::STRING, ['notnull' => true, 'length' => 255]);
$table->addColumn('schema', Types::STRING, ['notnull' => true, 'length' => 255]);
$table->addColumn('object', Types::JSON, ['notnull' => false]);
@@ -101,6 +109,25 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
$table->addIndex(['schema'], 'object_entity_schema');
}
+ if (!$schema->hasTable('openregister_object_audit_logs')) {
+ $table = $schema->createTable('openregister_object_audit_logs');
+ $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true]);
+ $table->addColumn('uuid', Types::STRING, ['notnull' => true, 'length' => 255]);
+ $table->addColumn('schema_id', Types::STRING, ['notnull' => true, 'length' => 255]);
+ $table->addColumn('object_id', Types::STRING, ['notnull' => true, 'length' => 255]);
+ $table->addColumn('user_id', Types::STRING, ['notnull' => false, 'length' => 255]);
+ $table->addColumn('session_id', Types::STRING, ['notnull' => false, 'length' => 255]);
+ $table->addColumn('changes', Types::JSON, ['notnull' => false]);
+ $table->addColumn('expires', Types::DATETIME, ['notnull' => false]);
+ $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']);
+
+ $table->setPrimaryKey(['id']);
+ $table->addIndex(['uuid'], 'object_audit_log_uuid');
+ $table->addIndex(['schema_id'], 'object_audit_log_schema_id');
+ $table->addIndex(['object_id'], 'object_audit_log_object_id');
+ $table->addIndex(['user_id'], 'object_audit_log_user_id');
+ }
+
return $schema;
}
diff --git a/lib/Service/ValidationService.php b/lib/Service/ValidationService.php
new file mode 100755
index 0000000..2cbf75b
--- /dev/null
+++ b/lib/Service/ValidationService.php
@@ -0,0 +1,22 @@
+
+ *
+ * @license EUPL
+ *
+ * @category Service
+ */
+class ValidationService
+{
+
+
+}
diff --git a/src/modals/Modals.vue b/src/modals/Modals.vue
index 5841d55..f2e77b0 100755
--- a/src/modals/Modals.vue
+++ b/src/modals/Modals.vue
@@ -5,6 +5,7 @@
+
@@ -19,6 +20,7 @@ import EditRegister from './register/EditRegister.vue'
import DeleteRegister from './register/DeleteRegister.vue'
import EditSchema from './schema/EditSchema.vue'
import DeleteSchema from './schema/DeleteSchema.vue'
+import UploadSchema from './schema/UploadSchema.vue'
import EditSchemaProperty from './schema/EditSchemaProperty.vue'
import DeleteSchemaProperty from './schema/DeleteSchemaProperty.vue'
import EditSource from './source/EditSource.vue'
@@ -33,6 +35,7 @@ export default {
DeleteRegister,
EditSchema,
DeleteSchema,
+ UploadSchema,
EditSchemaProperty,
DeleteSchemaProperty,
EditSource,
diff --git a/src/modals/schema/EditSchema.vue b/src/modals/schema/EditSchema.vue
index 185887d..8f68c7e 100644
--- a/src/modals/schema/EditSchema.vue
+++ b/src/modals/schema/EditSchema.vue
@@ -1,5 +1,5 @@
@@ -27,11 +27,6 @@ import { schemaStore, navigationStore, sourceStore } from '../../store/store.js'
-
@@ -93,12 +88,9 @@ export default {
version: '',
description: '',
summary: '',
- source: '',
created: '',
updated: '',
},
- sourcesLoading: false,
- sources: {},
success: false,
loading: false,
error: false,
@@ -111,7 +103,6 @@ export default {
updated() {
if (navigationStore.modal === 'editSchema' && !this.hasUpdated) {
this.initializeSchemaItem()
- this.initializeSources()
this.hasUpdated = true
}
},
@@ -124,37 +115,9 @@ export default {
version: schemaStore.schemaItem.version || '',
description: schemaStore.schemaItem.description || '',
summary: schemaStore.schemaItem.summary || '',
- source: schemaStore.schemaItem.source || '',
}
}
},
- initializeSources() {
- this.sourcesLoading = true
-
- sourceStore.refreshSourceList()
- .then(() => {
- const activeSource = schemaStore.schemaItem?.id
- ? sourceStore.sourceList.find((source) => source.id.toString() === schemaStore.schemaItem.source)
- : null
-
- this.sources = {
- multiple: false,
- closeOnSelect: true,
- options: sourceStore.sourceList.map((source) => ({
- id: source.id,
- label: source.title,
- })),
- value: activeSource
- ? {
- id: activeSource.id,
- label: activeSource.title,
- }
- : null,
- }
-
- this.sourcesLoading = false
- })
- },
closeModal() {
navigationStore.setModal(false)
this.success = null
@@ -166,7 +129,6 @@ export default {
version: '',
description: '',
summary: '',
- source: '',
created: '',
updated: '',
}
@@ -176,7 +138,6 @@ export default {
schemaStore.saveSchema({
...this.schemaItem,
- source: this.sources?.value?.id || '',
}).then(({ response }) => {
this.success = response.ok
this.error = false
diff --git a/src/modals/schema/UploadSchema.vue b/src/modals/schema/UploadSchema.vue
new file mode 100644
index 0000000..e10d200
--- /dev/null
+++ b/src/modals/schema/UploadSchema.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+ Schema successfully uploaded
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ success ? 'Close' : 'Cancel' }}
+
+
+
+
+
+
+ Upload
+
+
+
+
+
+
diff --git a/src/store/modules/schema.js b/src/store/modules/schema.js
index bef57f9..5512d17 100644
--- a/src/store/modules/schema.js
+++ b/src/store/modules/schema.js
@@ -128,6 +128,45 @@ export const useSchemaStore = defineStore('schema', {
return { response, data }
+ },
+ // Create or save a schema from store
+ async uploadSchema(schema) {
+ if (!this.schemaItem) {
+ throw new Error('No schema item to save')
+ }
+ if (!schema) {
+ throw new Error('No schema item to upload')
+ }
+
+ console.log('Uploading schema...')
+ const response = await fetch(
+ '/index.php/apps/openregister/api/schemas',
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(schemaItem),
+ },
+ )
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ const responseData = await response.json()
+
+ if (!responseData || typeof responseData !== 'object') {
+ throw new Error('Invalid response data')
+ }
+
+ const data = new Schema(responseData)
+
+ this.setSchemaItem(data)
+ this.refreshSchemaList()
+
+ return { response, data }
+
},
// schema properties
setSchemaPropertyKey(schemaPropertyKey) {
diff --git a/src/views/object/ObjectDetails.vue b/src/views/object/ObjectDetails.vue
index bf87d2f..ebbff10 100644
--- a/src/views/object/ObjectDetails.vue
+++ b/src/views/object/ObjectDetails.vue
@@ -56,7 +56,7 @@ import { objectStore, navigationStore } from '../../store/store.js'
{{ JSON.stringify(objectStore.objectItem.object, null, 2) }}
-
+
@todo
diff --git a/src/views/schema/SchemaDetails.vue b/src/views/schema/SchemaDetails.vue
index fa262f3..14831a2 100644
--- a/src/views/schema/SchemaDetails.vue
+++ b/src/views/schema/SchemaDetails.vue
@@ -27,6 +27,18 @@ import { schemaStore, navigationStore } from '../../store/store.js'
Add Property
+
+
+
+
+ Upload
+
+
+
+
+
+ Download
+
@@ -94,6 +106,8 @@ import { NcActions, NcActionButton, NcListItem } from '@nextcloud/vue'
import { BTabs, BTab } from 'bootstrap-vue'
import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
import Pencil from 'vue-material-design-icons/Pencil.vue'
+import Download from 'vue-material-design-icons/Download.vue'
+import Upload from 'vue-material-design-icons/Upload.vue'
import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
import PlusCircleOutline from 'vue-material-design-icons/PlusCircleOutline.vue'
import CircleOutline from 'vue-material-design-icons/CircleOutline.vue'
@@ -107,6 +121,10 @@ export default {
DotsHorizontal,
Pencil,
TrashCanOutline,
+ PlusCircleOutline,
+ CircleOutline,
+ Download,
+ Upload,
BTabs,
BTab,
},
From 301a734a138ae16d11b1b0c352b91116695c8171 Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Mon, 7 Oct 2024 21:30:17 +0200
Subject: [PATCH 05/38] Store update
---
src/store/modules/schema.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/store/modules/schema.js b/src/store/modules/schema.js
index 5512d17..8614096 100644
--- a/src/store/modules/schema.js
+++ b/src/store/modules/schema.js
@@ -140,7 +140,7 @@ export const useSchemaStore = defineStore('schema', {
console.log('Uploading schema...')
const response = await fetch(
- '/index.php/apps/openregister/api/schemas',
+ '/index.php/apps/openregister/api/schemas/upload',
{
method: 'POST',
headers: {
From e59188e87e36539e643fdf15b0c232195776218f Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Mon, 7 Oct 2024 22:24:10 +0200
Subject: [PATCH 06/38] Final work on uploading schema's
---
appinfo/routes.php | 2 +-
lib/Controller/SchemasController.php | 26 ++++++++++++++++++++++----
lib/Service/UploadService.php | 2 +-
src/modals/schema/UploadSchema.vue | 10 +++++++---
src/store/modules/schema.js | 16 ++++++++++------
src/views/schema/SchemasList.vue | 9 +++++++++
6 files changed, 50 insertions(+), 15 deletions(-)
diff --git a/appinfo/routes.php b/appinfo/routes.php
index b4599c6..3d9671f 100755
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -12,7 +12,7 @@
['name' => 'registers#objects', 'url' => '/api/registers-objects/{register}/{schema}', 'verb' => 'GET'],
['name' => 'objects#logs', 'url' => '/api/objects-logs/{id}', 'verb' => 'GET'],
['name' => 'schemas#upload', 'url' => '/api/schemas/upload', 'verb' => 'POST'],
-// ['name' => 'schemas#upload', 'url' => '/api/schemas/upload', 'verb' => 'PUT'],
+ ['name' => 'schemas#upload', 'url' => '/api/schemas/upload/{id}', 'verb' => 'PUT'],
['name' => 'schemas#download', 'url' => '/api/schemas/{id}/download', 'verb' => 'GET'],
],
];
diff --git a/lib/Controller/SchemasController.php b/lib/Controller/SchemasController.php
index 5aff521..d7f3bf8 100644
--- a/lib/Controller/SchemasController.php
+++ b/lib/Controller/SchemasController.php
@@ -199,8 +199,15 @@ private function jsonToSchema(array $jsonArray): array
*
* @return JSONResponse
*/
- public function upload(): JSONResponse
+ public function upload(?int $id = null): JSONResponse
{
+ if($id !== null){
+ $schema = $this->schemaMapper->find($id);
+ }
+ else{
+ $schema = new Schema();
+ }
+
$data = $this->request->getParams();
foreach ($data as $key => $value) {
@@ -245,11 +252,22 @@ public function upload(): JSONResponse
$data['json'] = $responseBody;
}
- $jsonArray = $data['json'];
+ $jsonArray = json_decode($data['json'], associative: true);
- $schemaObject = $this->JsonToSchema(jsonArray: $jsonArray);
+ //$schemaObject = $this->JsonToSchema(jsonArray: $jsonArray);
- $schema = $this->schemaMapper->createFromArray(object: $schemaObject);
+ // Set default title if not provided or empty
+ if (!isset($jsonArray['title']) || empty($jsonArray['title'])) {
+ $jsonArray['title'] = 'new object';
+ }
+
+ $schema->hydrate($jsonArray);
+ if($schema->getId() === null){
+ $schema = $this->schemaMapper->create($schema);
+ }
+ else{
+ $schema = $this->schemaMapper->update($schema);
+ }
return new JSONResponse($schema);
}
diff --git a/lib/Service/UploadService.php b/lib/Service/UploadService.php
index 38cb63e..3e272ff 100644
--- a/lib/Service/UploadService.php
+++ b/lib/Service/UploadService.php
@@ -23,7 +23,7 @@ public function upload()
*
* @return array
*/
- public function mapJsonSchema(array $input): array
+ public function mapJsonSchema(array $json): array
{
// @todo maybe do a switch for $input['$schema'] here? Or just do custom mappings in the Controllers?
diff --git a/src/modals/schema/UploadSchema.vue b/src/modals/schema/UploadSchema.vue
index e10d200..6ce20ba 100644
--- a/src/modals/schema/UploadSchema.vue
+++ b/src/modals/schema/UploadSchema.vue
@@ -17,7 +17,7 @@ import { schemaStore, navigationStore } from '../../store/store.js'
+ :value.sync="schema.json" />
@@ -73,7 +73,9 @@ export default {
},
data() {
return {
- schema: '{}',
+ schema: {
+ json: '{}'
+ },
success: false,
loading: false,
error: false,
@@ -87,7 +89,9 @@ export default {
this.loading = false
this.error = false
this.hasUpdated = false
- this.schema = '{}'
+ this.schema = {
+ json: '{}'
+ }
},
async uploadSchema() {
this.loading = true
diff --git a/src/store/modules/schema.js b/src/store/modules/schema.js
index 8614096..104298d 100644
--- a/src/store/modules/schema.js
+++ b/src/store/modules/schema.js
@@ -131,22 +131,26 @@ export const useSchemaStore = defineStore('schema', {
},
// Create or save a schema from store
async uploadSchema(schema) {
- if (!this.schemaItem) {
- throw new Error('No schema item to save')
- }
if (!schema) {
throw new Error('No schema item to upload')
}
console.log('Uploading schema...')
+
+ const isNewSchema = !this.schemaItem
+ const endpoint = isNewSchema
+ ? '/index.php/apps/openregister/api/schemas/upload'
+ : `/index.php/apps/openregister/api/schemas/upload/${this.schemaItem.id}`
+ const method = isNewSchema ? 'POST' : 'PUT'
+
const response = await fetch(
- '/index.php/apps/openregister/api/schemas/upload',
+ endpoint,
{
- method: 'POST',
+ method,
headers: {
'Content-Type': 'application/json',
},
- body: JSON.stringify(schemaItem),
+ body: JSON.stringify(schema),
},
)
diff --git a/src/views/schema/SchemasList.vue b/src/views/schema/SchemasList.vue
index 84e1660..62b23a3 100644
--- a/src/views/schema/SchemasList.vue
+++ b/src/views/schema/SchemasList.vue
@@ -22,6 +22,12 @@ import { schemaStore, navigationStore, searchStore } from '../../store/store.js'
Refresh
+
+
+
+
+ Upload
+
@@ -90,6 +96,7 @@ import Plus from 'vue-material-design-icons/Plus.vue'
import Pencil from 'vue-material-design-icons/Pencil.vue'
import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
import PlusCircleOutline from 'vue-material-design-icons/PlusCircleOutline.vue'
+import Upload from 'vue-material-design-icons/Upload.vue'
export default {
name: 'SchemasList',
@@ -106,6 +113,8 @@ export default {
Plus,
Pencil,
TrashCanOutline,
+ PlusCircleOutline,
+ Upload,
},
mounted() {
schemaStore.refreshSchemaList()
From 1ac9efe75702c89b109972eab8ea8e475197675f Mon Sep 17 00:00:00 2001
From: Ruben van der Linde
Date: Tue, 8 Oct 2024 22:17:39 +0200
Subject: [PATCH 07/38] Load complete oas definitions
---
appinfo/routes.php | 3 +
composer.json | 4 +-
composer.lock | 153 ++++++++++++++++++++++++-
lib/Controller/RegistersController.php | 153 ++++++++++++++++++++++++-
lib/Controller/SchemasController.php | 15 ++-
lib/Db/RegisterMapper.php | 96 +++++++++++++++-
lib/Service/ObjectService.php | 1 +
src/modals/Modals.vue | 3 +
src/modals/register/UploadRegister.vue | 117 +++++++++++++++++++
src/modals/schema/UploadSchema.vue | 9 +-
src/store/modules/register.js | 43 +++++++
src/views/register/RegisterDetails.vue | 9 +-
src/views/register/RegistersList.vue | 8 ++
13 files changed, 598 insertions(+), 16 deletions(-)
create mode 100644 src/modals/register/UploadRegister.vue
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 3d9671f..8f94fb4 100755
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -14,5 +14,8 @@
['name' => 'schemas#upload', 'url' => '/api/schemas/upload', 'verb' => 'POST'],
['name' => 'schemas#upload', 'url' => '/api/schemas/upload/{id}', 'verb' => 'PUT'],
['name' => 'schemas#download', 'url' => '/api/schemas/{id}/download', 'verb' => 'GET'],
+ ['name' => 'registers#upload', 'url' => '/api/registers/upload', 'verb' => 'POST'],
+ ['name' => 'registers#upload', 'url' => '/api/registers/upload/{id}', 'verb' => 'PUT'],
+ ['name' => 'registers#download', 'url' => '/api/registers/{id}/download', 'verb' => 'GET'],
],
];
diff --git a/composer.json b/composer.json
index e655804..0351f27 100755
--- a/composer.json
+++ b/composer.json
@@ -33,9 +33,9 @@
"adbario/php-dot-notation": "^3.3.0",
"bamarni/composer-bin-plugin": "^1.8",
"elasticsearch/elasticsearch": "^v8.14.0",
- "adbario/php-dot-notation": "^3.3.0",
"guzzlehttp/guzzle": "^7.0",
- "symfony/uid": "^6.4"
+ "symfony/uid": "^6.4",
+ "symfony/yaml": "^6.4"
},
"require-dev": {
"nextcloud/ocp": "dev-stable29",
diff --git a/composer.lock b/composer.lock
index ffc4174..4761078 100755
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "ca042ce32fdb4f39b85f7de862b66c5b",
+ "content-hash": "d4ee34c9190d5b1b8eb1a1841807244e",
"packages": [
{
"name": "adbario/php-dot-notation",
@@ -1195,6 +1195,85 @@
],
"time": "2024-04-18T09:32:20+00:00"
},
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.31.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
{
"name": "symfony/polyfill-php80",
"version": "v1.30.0",
@@ -1579,6 +1658,78 @@
}
],
"time": "2024-05-31T14:49:08+00:00"
+ },
+ {
+ "name": "symfony/yaml",
+ "version": "v6.4.12",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "762ee56b2649659380e0ef4d592d807bc17b7971"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/762ee56b2649659380e0ef4d592d807bc17b7971",
+ "reference": "762ee56b2649659380e0ef4d592d807bc17b7971",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/console": "<5.4"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0"
+ },
+ "bin": [
+ "Resources/bin/yaml-lint"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Yaml\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Loads and dumps YAML files",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/yaml/tree/v6.4.12"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-17T12:47:12+00:00"
}
],
"packages-dev": [
diff --git a/lib/Controller/RegistersController.php b/lib/Controller/RegistersController.php
index 424fe67..260be45 100644
--- a/lib/Controller/RegistersController.php
+++ b/lib/Controller/RegistersController.php
@@ -2,16 +2,21 @@
namespace OCA\OpenRegister\Controller;
+use GuzzleHttp\Client;
use OCA\OpenRegister\Service\ObjectService;
use OCA\OpenRegister\Service\SearchService;
use OCA\OpenRegister\Db\Register;
use OCA\OpenRegister\Db\RegisterMapper;
+use OCA\OpenRegister\Db\Schema;
+use OCA\OpenRegister\Db\SchemaMapper;
use OCA\OpenRegister\Db\ObjectEntityMapper;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IAppConfig;
use OCP\IRequest;
+use Symfony\Component\Yaml\Yaml;
+use Symfony\Component\Uid\Uuid;
class RegistersController extends Controller
{
@@ -21,16 +26,23 @@ class RegistersController extends Controller
* @param string $appName The name of the app
* @param IRequest $request The request object
* @param IAppConfig $config The app configuration object
+ * @param Client $client The client
+ * @param SchemaMapper $schemaMapper The schema mapper
+ * @param ObjectEntityMapper $objectEntityMapper The object entity mapper
+ * @param RegisterMapper $registerMapper The register mapper
*/
public function __construct(
$appName,
IRequest $request,
private readonly IAppConfig $config,
private readonly RegisterMapper $registerMapper,
- private readonly ObjectEntityMapper $objectEntityMapper
+ private readonly ObjectEntityMapper $objectEntityMapper,
+ private readonly SchemaMapper $schemaMapper,
+ private Client $client
)
{
parent::__construct($appName, $request);
+ $this->client = new Client([]);
}
/**
@@ -182,4 +194,143 @@ public function objects(int $register, int $schema): JSONResponse
{
return new JSONResponse($this->objectEntityMapper->findByRegisterAndSchema(register: $register, schema: $schema));
}
+
+ /**
+ * Creates a new Register object using a json text/string as input. Uses 'json' from POST body.
+ * @todo Optionally a 'url' can be used instead to get a json file from somewhere else and use that instead.
+ * @todo Or a .json file can be uploaded using key 'file'.
+ * @todo move most of this code to a (new?) UploadService and make it even more abstract and reusable?
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @return JSONResponse
+ */
+ public function upload(?int $id = null): JSONResponse
+ {
+ if($id !== null){
+ $register = $this->registerMapper->find($id);
+ }
+ else{
+ $register = new Register();
+ }
+
+ $data = $this->request->getParams();
+
+ foreach ($data as $key => $value) {
+ if (str_starts_with($key, '_')) {
+ unset($data[$key]);
+ }
+ }
+
+ // Define the allowed keys
+ $allowedKeys = ['file', 'url', 'json'];
+
+ // Find which of the allowed keys are in the array
+ $matchingKeys = array_intersect_key($data, array_flip($allowedKeys));
+
+ // Check if there is exactly one matching key
+ if (count($matchingKeys) === 0) {
+ return new JSONResponse(data: ['error' => 'Missing one of these keys in your POST body: file, url or json.'], statusCode: 400);
+ }
+
+ if (empty($data['file']) === false) {
+ // @todo use .json file content from POST as $json
+ //$data['json'] = [];
+ }
+
+ if (empty($data['url']) === false) {
+ // @todo move to function (cleanup)
+ try {
+ $response = $this->client->request('GET', $data['url']);
+ } catch (GuzzleHttp\Exception\BadResponseException $e) {
+ $response = $e->getResponse();
+ return new JSONResponse(data: ['error' => 'Failed to do a GET api-call on url: '.$data['url']], statusCode: 400);
+ }
+
+ $responseBody = $response->getBody()->getContents();
+ // Use Content-Type header to determine the format
+ $contentType = $response->getHeaderLine('Content-Type');
+
+ if (strpos($contentType, 'application/json') !== false) {
+ $array = json_decode($responseBody, true);
+ } elseif (strpos($contentType, 'application/yaml') !== false || strpos($contentType, 'text/yaml') !== false) {
+ $array = Yaml::parse($responseBody);
+ } else {
+ // If Content-Type is not specified or not recognized, try to parse as JSON first, then YAML
+ $array = json_decode($responseBody, true);
+ if ($array === null) {
+ $array = Yaml::parse($responseBody);
+ }
+ }
+
+ if ($array === null || $array === false) {
+ return new JSONResponse(data: ['error' => 'Failed to parse response body as JSON or YAML'], statusCode: 400);
+ }
+
+ }
+ else{
+ $array = json_decode($data['json'], associative: true);
+ }
+
+
+ // Validate that the jsonArray is a valid OAS3 object containing schemas
+ if (!isset($array['openapi']) || !isset($array['components']['schemas'])) {
+ return new JSONResponse(data: ['error' => 'Invalid OAS3 object. Must contain openapi version and components.schemas.'], statusCode: 400);
+ }
+
+ // Set default title if not provided or empty
+ if (!isset($array['info']['title']) || empty($array['info']['title'])) {
+ $jsonArray['info']['title'] = 'New Register';
+ }
+
+ $register->hydrate($array);
+ if($register->getId() === null){
+ $register = $this->registerMapper->insert($register);
+ }
+ else{
+ $register = $this->registerMapper->update($register);
+ }
+
+ // Process and save schemas
+ foreach ($array['components']['schemas'] as $schemaName => $schemaData) {
+ // Check if a schema with this title already exists
+ $schema = $this->registerMapper->hasSchemaWithTitle($register->getId(), $schemaName);
+ if ($schema === false) {
+ // Check if a schema with this title already exists for this register
+ try{
+ $schemas = $this->schemaMapper->findAll(null, null, ['title' => $schemaName]);
+ if(count($schemas) > 0){
+ $schema = $schemas[0];
+ }
+ else{
+ // None found so, Create a new schema
+ $schema = new Schema();
+ $schema->setTitle($schemaName);
+ $schema->setUuid(Uuid::v4());
+ $this->schemaMapper->insert($schema);
+ }
+ }
+ catch(DoesNotExistException $e){
+ // None found so, Create a new schema
+ $schema = new Schema();
+ $schema->setTitle($schemaName);
+ $schema->setUuid(Uuid::v4());
+ $this->schemaMapper->insert($schema);
+ }
+ }
+
+ $schema->hydrate($schemaData);
+ $this->schemaMapper->update($schema);
+ // Add the schema to the register
+ $schemas = $register->getSchemas();
+ $schemas[] = $schema->getId();
+ $register->setSchemas($schemas);
+ // Lets save the updated register
+ $register = $this->registerMapper->update($register);
+ }
+
+
+ return new JSONResponse($register);
+ }
}
\ No newline at end of file
diff --git a/lib/Controller/SchemasController.php b/lib/Controller/SchemasController.php
index d7f3bf8..009c88a 100644
--- a/lib/Controller/SchemasController.php
+++ b/lib/Controller/SchemasController.php
@@ -23,6 +23,10 @@ class SchemasController extends Controller
* @param string $appName The name of the app
* @param IRequest $request The request object
* @param IAppConfig $config The app configuration object
+ * @param SchemaMapper $schemaMapper The schema mapper
+ * @param DownloadService $downloadService The download service
+ * @param UploadService $uploadService The upload service
+ * @param Client $client The client
*/
public function __construct(
$appName,
@@ -30,11 +34,12 @@ public function __construct(
private readonly IAppConfig $config,
private readonly SchemaMapper $schemaMapper,
private readonly DownloadService $downloadService,
- private readonly UploadService $uploadService
+ private readonly UploadService $uploadService,
+ private Client $client
)
{
- $this->client = new Client([]);
parent::__construct($appName, $request);
+ $this->client = new Client([]);
}
/**
@@ -226,13 +231,11 @@ public function upload(?int $id = null): JSONResponse
if (count($matchingKeys) === 0) {
return new JSONResponse(data: ['error' => 'Missing one of these keys in your POST body: file, url or json.'], statusCode: 400);
}
- if (count($matchingKeys) > 1) {
- return new JSONResponse(data: ['error' => 'Please use one of these keys: file, url or json, not multiple.'], statusCode: 400);
- }
+
if (empty($data['file']) === false) {
// @todo use .json file content from POST as $json
- $data['json'] = [];
+ //$data['json'] = [];
}
if (empty($data['url']) === false) {
diff --git a/lib/Db/RegisterMapper.php b/lib/Db/RegisterMapper.php
index a6c82bb..71df364 100644
--- a/lib/Db/RegisterMapper.php
+++ b/lib/Db/RegisterMapper.php
@@ -6,38 +6,69 @@
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCA\OpenRegister\Db\SchemaMapper;
+use OCA\OpenRegister\Db\Schema;
use OCP\IDBConnection;
use Symfony\Component\Uid\Uuid;
class RegisterMapper extends QBMapper
{
- public function __construct(IDBConnection $db)
+ private $schemaMapper;
+
+ /**
+ * Constructor for RegisterMapper
+ *
+ * @param IDBConnection $db The database connection
+ * @param SchemaMapper $schemaMapper The schema mapper
+ */
+ public function __construct(IDBConnection $db, SchemaMapper $schemaMapper)
{
parent::__construct($db, 'openregister_registers');
+ $this->schemaMapper = $schemaMapper;
}
+ /**
+ * Find a register by its ID
+ *
+ * @param int $id The ID of the register to find
+ * @return Register The found register
+ */
public function find(int $id): Register
{
$qb = $this->db->getQueryBuilder();
+ // Build the query
$qb->select('*')
->from('openregister_registers')
->where(
$qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
);
+ // Execute the query and return the result
return $this->findEntity(query: $qb);
}
+ /**
+ * Find all registers with optional filtering and searching
+ *
+ * @param int|null $limit Maximum number of results to return
+ * @param int|null $offset Number of results to skip
+ * @param array|null $filters Associative array of filters
+ * @param array|null $searchConditions Array of search conditions
+ * @param array|null $searchParams Array of search parameters
+ * @return array Array of found registers
+ */
public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array
{
$qb = $this->db->getQueryBuilder();
+ // Build the base query
$qb->select('*')
->from('openregister_registers')
->setMaxResults($limit)
->setFirstResult($offset);
+ // Apply filters
foreach ($filters as $filter => $value) {
if ($value === 'IS NOT NULL') {
$qb->andWhere($qb->expr()->isNotNull($filter));
@@ -48,6 +79,7 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters
}
}
+ // Apply search conditions
if (!empty($searchConditions)) {
$qb->andWhere('(' . implode(' OR ', $searchConditions) . ')');
foreach ($searchParams as $param => $value) {
@@ -55,31 +87,89 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters
}
}
+ // Execute the query and return the results
return $this->findEntities(query: $qb);
}
+ /**
+ * Create a new register from an array of data
+ *
+ * @param array $object The data to create the register from
+ * @return Register The created register
+ */
public function createFromArray(array $object): Register
{
$obj = new Register();
$obj->hydrate($object);
- // Set uuid
+ // Set uuid if not provided
if($obj->getUuid() === null){
$obj->setUuid(Uuid::v4());
}
+ // Insert the new register and return it
return $this->insert(entity: $obj);
}
+ /**
+ * Update an existing register from an array of data
+ *
+ * @param int $id The ID of the register to update
+ * @param array $object The new data for the register
+ * @return Register The updated register
+ */
public function updateFromArray(int $id, array $object): Register
{
$obj = $this->find($id);
$obj->hydrate($object);
- // Set or update the version
+ // Update the version
$version = explode('.', $obj->getVersion());
$version[2] = (int)$version[2] + 1;
$obj->setVersion(implode('.', $version));
+ // Update the register and return it
return $this->update($obj);
}
+
+ /**
+ * Get all schemas associated with a register
+ *
+ * @param int $registerId The ID of the register
+ * @return array Array of schemas
+ */
+ public function getSchemasByRegisterId(int $registerId): array
+ {
+ $register = $this->find($registerId);
+ $schemaIds = $register->getSchemas();
+
+ $schemas = [];
+
+ // Fetch each schema by its ID
+ foreach ($schemaIds as $schemaId) {
+ $schemas[] = $this->schemaMapper->find((int) $schemaId);
+ }
+
+ return $schemas;
+ }
+
+ /**
+ * Check if a register has a schema with a specific title
+ *
+ * @param int $registerId The ID of the register
+ * @param string $schemaTitle The title of the schema to look for
+ * @return Schema|bool The schema if found, false otherwise
+ */
+ public function hasSchemaWithTitle(int $registerId, string $schemaTitle): Schema|bool
+ {
+ $schemas = $this->getSchemasByRegisterId($registerId);
+
+ // Check each schema for a matching title
+ foreach ($schemas as $schema) {
+ if ($schema->getTitle() === $schemaTitle) {
+ return $schema;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php
index 1e2cf1e..695c39f 100755
--- a/lib/Service/ObjectService.php
+++ b/lib/Service/ObjectService.php
@@ -11,6 +11,7 @@
use OCA\OpenRegister\Db\ObjectEntity;
use OCA\OpenRegister\Db\ObjectEntityMapper;
use Symfony\Component\Uid\Uuid;
+use GuzzleHttp\Client;
class ObjectService
{
diff --git a/src/modals/Modals.vue b/src/modals/Modals.vue
index f2e77b0..b38e1b3 100755
--- a/src/modals/Modals.vue
+++ b/src/modals/Modals.vue
@@ -2,6 +2,7 @@
+
@@ -18,6 +19,7 @@
+
+
+
+
+ Register successfully uploaded
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ success ? 'Close' : 'Cancel' }}
+
+
+
+
+
+
+ Upload
+
+
+
+
+
+
diff --git a/src/modals/schema/UploadSchema.vue b/src/modals/schema/UploadSchema.vue
index 6ce20ba..7d5ae37 100644
--- a/src/modals/schema/UploadSchema.vue
+++ b/src/modals/schema/UploadSchema.vue
@@ -15,6 +15,9 @@ import { schemaStore, navigationStore } from '../../store/store.js'