diff --git a/appinfo/routes.php b/appinfo/routes.php index dde8404..9567c1a 100755 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -8,9 +8,14 @@ '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' => 'objects#auditTrails', 'url' => '/api/audit-trails/{id}', 'verb' => 'GET'], - + ['name' => 'objects#logs', 'url' => '/api/objects-logs/{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' => '[^/]+']], ], ]; diff --git a/composer.json b/composer.json index e655804..7e07700 100755 --- a/composer.json +++ b/composer.json @@ -33,9 +33,10 @@ "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" + "opis/json-schema": "^2.3", + "symfony/uid": "^6.4", + "symfony/yaml": "^6.4" }, "require-dev": { "nextcloud/ocp": "dev-stable29", diff --git a/composer.lock b/composer.lock index ffc4174..64ff9c4 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", @@ -361,16 +361,16 @@ }, { "name": "guzzlehttp/promises", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", - "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", "shasum": "" }, "require": { @@ -424,7 +424,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.3" + "source": "https://github.com/guzzle/promises/tree/2.0.4" }, "funding": [ { @@ -440,7 +440,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T10:29:17+00:00" + "time": "2024-10-17T10:06:22+00:00" }, { "name": "guzzlehttp/psr7", @@ -560,33 +560,36 @@ }, { "name": "open-telemetry/api", - "version": "1.0.3", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "87de95d926f46262885d0d390060c095af13e2e5" + "reference": "542064815d38a6df55af7957cd6f1d7d967c99c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/87de95d926f46262885d0d390060c095af13e2e5", - "reference": "87de95d926f46262885d0d390060c095af13e2e5", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/542064815d38a6df55af7957cd6f1d7d967c99c6", + "reference": "542064815d38a6df55af7957cd6f1d7d967c99c6", "shasum": "" }, "require": { "open-telemetry/context": "^1.0", - "php": "^7.4 || ^8.0", + "php": "^8.1", "psr/log": "^1.1|^2.0|^3.0", - "symfony/polyfill-php80": "^1.26", - "symfony/polyfill-php81": "^1.26", "symfony/polyfill-php82": "^1.26" }, "conflict": { - "open-telemetry/sdk": "<=1.0.4" + "open-telemetry/sdk": "<=1.0.8" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.0.x-dev" + "dev-main": "1.1.x-dev" + }, + "spi": { + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] } }, "autoload": { @@ -623,26 +626,24 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-02-06T01:32:25+00:00" + "time": "2024-10-15T22:42:37+00:00" }, { "name": "open-telemetry/context", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/context.git", - "reference": "e9d254a7c89885e63fd2fde54e31e81aaaf52b7c" + "reference": "0cba875ea1953435f78aec7f1d75afa87bdbf7f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/e9d254a7c89885e63fd2fde54e31e81aaaf52b7c", - "reference": "e9d254a7c89885e63fd2fde54e31e81aaaf52b7c", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/0cba875ea1953435f78aec7f1d75afa87bdbf7f3", + "reference": "0cba875ea1953435f78aec7f1d75afa87bdbf7f3", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0", - "symfony/polyfill-php80": "^1.26", - "symfony/polyfill-php81": "^1.26", + "php": "^8.1", "symfony/polyfill-php82": "^1.26" }, "suggest": { @@ -684,20 +685,210 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2024-01-13T05:50:44+00:00" + "time": "2024-08-21T00:29:20+00:00" + }, + { + "name": "opis/json-schema", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/opis/json-schema.git", + "reference": "c48df6d7089a45f01e1c82432348f2d5976f9bfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/json-schema/zipball/c48df6d7089a45f01e1c82432348f2d5976f9bfb", + "reference": "c48df6d7089a45f01e1c82432348f2d5976f9bfb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "opis/string": "^2.0", + "opis/uri": "^1.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ext-bcmath": "*", + "ext-intl": "*", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\JsonSchema\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + }, + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + } + ], + "description": "Json Schema Validator for PHP", + "homepage": "https://opis.io/json-schema", + "keywords": [ + "json", + "json-schema", + "schema", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/opis/json-schema/issues", + "source": "https://github.com/opis/json-schema/tree/2.3.0" + }, + "time": "2022-01-08T20:38:03+00:00" + }, + { + "name": "opis/string", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/opis/string.git", + "reference": "9ebf1a1f873f502f6859d11210b25a4bf5d141e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/string/zipball/9ebf1a1f873f502f6859d11210b25a4bf5d141e7", + "reference": "9ebf1a1f873f502f6859d11210b25a4bf5d141e7", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-json": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\String\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Multibyte strings as objects", + "homepage": "https://opis.io/string", + "keywords": [ + "multi-byte", + "opis", + "string", + "string manipulation", + "utf-8" + ], + "support": { + "issues": "https://github.com/opis/string/issues", + "source": "https://github.com/opis/string/tree/2.0.1" + }, + "time": "2022-01-14T15:42:23+00:00" + }, + { + "name": "opis/uri", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/opis/uri.git", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/uri/zipball/0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "shasum": "" + }, + "require": { + "opis/string": "^2.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Build, parse and validate URIs and URI-templates", + "homepage": "https://opis.io", + "keywords": [ + "URI Template", + "parse url", + "punycode", + "uri", + "uri components", + "url", + "validate uri" + ], + "support": { + "issues": "https://github.com/opis/uri/issues", + "source": "https://github.com/opis/uri/tree/1.1.0" + }, + "time": "2021-05-22T15:57:08+00:00" }, { "name": "php-http/discovery", - "version": "1.19.4", + "version": "1.20.0", "source": { "type": "git", "url": "https://github.com/php-http/discovery.git", - "reference": "0700efda8d7526335132360167315fdab3aeb599" + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599", - "reference": "0700efda8d7526335132360167315fdab3aeb599", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", "shasum": "" }, "require": { @@ -761,22 +952,22 @@ ], "support": { "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.19.4" + "source": "https://github.com/php-http/discovery/tree/1.20.0" }, - "time": "2024-03-29T13:00:05+00:00" + "time": "2024-10-02T11:20:13+00:00" }, { "name": "php-http/httplug", - "version": "2.4.0", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/php-http/httplug.git", - "reference": "625ad742c360c8ac580fcc647a1541d29e257f67" + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67", - "reference": "625ad742c360c8ac580fcc647a1541d29e257f67", + "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4", "shasum": "" }, "require": { @@ -818,9 +1009,9 @@ ], "support": { "issues": "https://github.com/php-http/httplug/issues", - "source": "https://github.com/php-http/httplug/tree/2.4.0" + "source": "https://github.com/php-http/httplug/tree/2.4.1" }, - "time": "2023-04-14T15:10:03+00:00" + "time": "2024-09-23T11:39:58+00:00" }, { "name": "php-http/promise", @@ -1195,6 +1386,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", @@ -1353,20 +1623,20 @@ }, { "name": "symfony/polyfill-php82", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php82.git", - "reference": "77ff49780f56906788a88974867ed68bc49fae5b" + "reference": "5d2ed36f7734637dacc025f179698031951b1692" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/77ff49780f56906788a88974867ed68bc49fae5b", - "reference": "77ff49780f56906788a88974867ed68bc49fae5b", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", + "reference": "5d2ed36f7734637dacc025f179698031951b1692", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -1409,7 +1679,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php82/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php82/tree/v1.31.0" }, "funding": [ { @@ -1425,24 +1695,24 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "2ba1f33797470debcda07fe9dce20a0003df18e9" + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/2ba1f33797470debcda07fe9dce20a0003df18e9", - "reference": "2ba1f33797470debcda07fe9dce20a0003df18e9", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-uuid": "*" @@ -1488,7 +1758,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" }, "funding": [ { @@ -1504,20 +1774,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/uid", - "version": "v6.4.8", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "35904eca37a84bb764c560cbfcac9f0ac2bcdbdf" + "reference": "18eb207f0436a993fffbdd811b5b8fa35fa5e007" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/35904eca37a84bb764c560cbfcac9f0ac2bcdbdf", - "reference": "35904eca37a84bb764c560cbfcac9f0ac2bcdbdf", + "url": "https://api.github.com/repos/symfony/uid/zipball/18eb207f0436a993fffbdd811b5b8fa35fa5e007", + "reference": "18eb207f0436a993fffbdd811b5b8fa35fa5e007", "shasum": "" }, "require": { @@ -1562,7 +1832,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.4.8" + "source": "https://github.com/symfony/uid/tree/v6.4.13" }, "funding": [ { @@ -1579,6 +1849,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": [ @@ -1588,12 +1930,12 @@ "source": { "type": "git", "url": "https://github.com/nextcloud-deps/ocp.git", - "reference": "53059f1bbcdd624fa1783591da5575faa4284d15" + "reference": "5054ce1e0018c7f0946df391a54861f8172d7be2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/53059f1bbcdd624fa1783591da5575faa4284d15", - "reference": "53059f1bbcdd624fa1783591da5575faa4284d15", + "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/5054ce1e0018c7f0946df391a54861f8172d7be2", + "reference": "5054ce1e0018c7f0946df391a54861f8172d7be2", "shasum": "" }, "require": { @@ -1624,7 +1966,7 @@ "issues": "https://github.com/nextcloud-deps/ocp/issues", "source": "https://github.com/nextcloud-deps/ocp/tree/stable29" }, - "time": "2024-08-09T00:38:21+00:00" + "time": "2024-09-17T00:34:06+00:00" }, { "name": "psr/clock", @@ -1783,23 +2125,23 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "f8de2a81061775002d96aea80b12f2ab3c5eeb8d" + "reference": "622ede44e079ad5c341a40013ef0e16fab2902ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/f8de2a81061775002d96aea80b12f2ab3c5eeb8d", - "reference": "f8de2a81061775002d96aea80b12f2ab3c5eeb8d", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/622ede44e079ad5c341a40013ef0e16fab2902ab", + "reference": "622ede44e079ad5c341a40013ef0e16fab2902ab", "shasum": "" }, "conflict": { "3f/pygmentize": "<1.2", - "admidio/admidio": "<4.3.10", + "admidio/admidio": "<4.3.12", "adodb/adodb-php": "<=5.20.20|>=5.21,<=5.21.3", "aheinze/cockpit": "<2.2", - "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.04.6", + "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", "aimeos/ai-admin-jsonadm": "<2020.10.13|>=2021.04.1,<2021.10.6|>=2022.04.1,<2022.10.3|>=2023.04.1,<2023.10.4|==2024.04.1", "aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.22|>=2022.04.1,<2022.10.13|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.04.7", - "aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9", + "aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9|==2024.04.1", "aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7", "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", "airesvsg/acf-to-rest-api": "<=3.1", @@ -1808,6 +2150,7 @@ "alextselegidis/easyappointments": "<1.5", "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", "amazing/media2click": ">=1,<1.3.3", + "ameos/ameos_tarteaucitron": "<1.2.23", "amphp/artax": "<1.0.6|>=2,<2.0.6", "amphp/http": "<=1.7.2|>=2,<=2.1", "amphp/http-client": ">=4,<4.4", @@ -1840,7 +2183,7 @@ "barrelstrength/sprout-forms": "<3.9", "barryvdh/laravel-translation-manager": "<0.6.2", "barzahlen/barzahlen-php": "<2.0.1", - "baserproject/basercms": "<5.0.9", + "baserproject/basercms": "<=5.1.1", "bassjobsen/bootstrap-3-typeahead": ">4.0.2", "bbpress/bbpress": "<2.6.5", "bcosca/fatfree": "<3.7.2", @@ -1885,21 +2228,23 @@ "codeigniter4/shield": "<1.0.0.0-beta8", "codiad/codiad": "<=2.8.4", "composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7", - "concrete5/concrete5": "<9.3.3", + "concrete5/concrete5": "<9.3.4", "concrete5/core": "<8.5.8|>=9,<9.1", "contao-components/mediaelement": ">=2.14.2,<2.21.1", "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4", - "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.9.40|>=4.10,<4.11.7|>=4.13,<4.13.21|>=5.1,<5.1.4", + "contao/contao": "<=5.4.1", "contao/core": "<3.5.39", - "contao/core-bundle": "<4.13.40|>=5,<5.3.4", + "contao/core-bundle": "<4.13.49|>=5,<5.3.15|>=5.4,<5.4.3", "contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8", "contao/managed-edition": "<=1.5", "corveda/phpsandbox": "<1.3.5", "cosenary/instagram": "<=2.3", - "craftcms/cms": "<4.6.2|>=5.0.0.0-beta1,<=5.2.2", + "craftcms/cms": "<4.6.2|>=5,<=5.2.2", "croogo/croogo": "<4", "cuyz/valinor": "<0.12", + "czim/file-handling": "<1.5|>=2,<2.3", "czproject/git-php": "<4.0.3", + "damienharper/auditor-bundle": "<5.2.6", "dapphp/securimage": "<3.6.6", "darylldoyle/safe-svg": "<1.9.10", "datadog/dd-trace": ">=0.30,<0.30.2", @@ -1910,6 +2255,7 @@ "derhansen/fe_change_pwd": "<2.0.5|>=3,<3.0.3", "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4", "desperado/xml-bundle": "<=0.1.7", + "dev-lancer/minecraft-motd-parser": "<=1.0.5", "devgroup/dotplant": "<2020.09.14-dev", "directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2", "doctrine/annotations": "<1.2.7", @@ -1924,8 +2270,9 @@ "dolibarr/dolibarr": "<19.0.2", "dompdf/dompdf": "<2.0.4", "doublethreedigital/guest-entries": "<3.1.2", - "drupal/core": ">=6,<6.38|>=7,<7.96|>=8,<10.1.8|>=10.2,<10.2.2", - "drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4", + "drupal/core": ">=6,<6.38|>=7,<7.96|>=8,<10.2.9|>=10.3,<10.3.6|>=11,<11.0.5", + "drupal/core-recommended": ">=8,<10.2.9|>=10.3,<10.3.6|>=11,<11.0.5", + "drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.80|>=8,<10.2.9|>=10.3,<10.3.6|>=11,<11.0.5", "duncanmcclean/guest-entries": "<3.1.2", "dweeves/magmi": "<=0.7.24", "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2", @@ -1968,6 +2315,8 @@ "feehi/cms": "<=2.1.1", "feehi/feehicms": "<=2.1.1", "fenom/fenom": "<=2.12.1", + "filament/infolists": ">=3,<3.2.115", + "filament/tables": ">=3,<3.2.115", "filegator/filegator": "<7.8", "filp/whoops": "<2.1.13", "fineuploader/php-traditional-server": "<=1.2.2", @@ -2002,12 +2351,12 @@ "froxlor/froxlor": "<=2.2.0.0-RC3", "frozennode/administrator": "<=5.0.12", "fuel/core": "<1.8.1", - "funadmin/funadmin": "<=3.2|>=3.3.2,<=3.3.3", + "funadmin/funadmin": "<=5.0.2", "gaoming13/wechat-php-sdk": "<=1.10.2", "genix/cms": "<=1.1.11", "getformwork/formwork": "<1.13.1|==2.0.0.0-beta1", "getgrav/grav": "<1.7.46", - "getkirby/cms": "<4.1.1", + "getkirby/cms": "<=3.6.6.5|>=3.7,<=3.7.5.4|>=3.8,<=3.8.4.3|>=3.9,<=3.9.8.1|>=3.10,<=3.10.1|>=4,<=4.3", "getkirby/kirby": "<=2.5.12", "getkirby/panel": "<2.5.14", "getkirby/starterkit": "<=3.7.0.2", @@ -2054,6 +2403,7 @@ "in2code/femanager": "<5.5.3|>=6,<6.3.4|>=7,<7.2.3", "in2code/ipandlanguageredirect": "<5.1.2", "in2code/lux": "<17.6.1|>=18,<24.0.2", + "in2code/powermail": "<7.5.1|>=8,<8.5.1|>=9,<10.9.1|>=11,<12.4.1", "innologi/typo3-appointments": "<2.0.6", "intelliants/subrion": "<4.2.2", "inter-mediator/inter-mediator": "==5.5", @@ -2083,18 +2433,20 @@ "kelvinmo/simplexrd": "<3.1.1", "kevinpapst/kimai2": "<1.16.7", "khodakhah/nodcms": "<=3", - "kimai/kimai": "<2.16", + "kimai/kimai": "<=2.20.1", "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", "klaviyo/magento2-extension": ">=1,<3", "knplabs/knp-snappy": "<=1.4.2", "kohana/core": "<3.3.3", - "krayin/laravel-crm": "<1.2.2", + "krayin/laravel-crm": "<=1.3", "kreait/firebase-php": ">=3.2,<3.8.1", "kumbiaphp/kumbiapp": "<=1.1.1", "la-haute-societe/tcpdf": "<6.2.22", "laminas/laminas-diactoros": "<2.18.1|==2.19|==2.20|==2.21|==2.22|==2.23|>=2.24,<2.24.2|>=2.25,<2.25.2", "laminas/laminas-form": "<2.17.1|>=3,<3.0.2|>=3.1,<3.1.1", "laminas/laminas-http": "<2.14.2", + "lara-zeus/artemis": ">=1,<=1.0.6", + "lara-zeus/dynamic-dashboard": ">=3,<=3.0.1", "laravel/fortify": "<1.11.1", "laravel/framework": "<6.20.44|>=7,<7.30.6|>=8,<8.75", "laravel/laravel": ">=5.4,<5.4.22", @@ -2110,13 +2462,14 @@ "librenms/librenms": "<2017.08.18", "liftkit/database": "<2.13.2", "lightsaml/lightsaml": "<1.3.5", - "limesurvey/limesurvey": "<3.27.19", + "limesurvey/limesurvey": "<6.5.12", "livehelperchat/livehelperchat": "<=3.91", - "livewire/livewire": ">2.2.4,<2.2.6|>=3.3.5,<3.4.9", + "livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.5.2", "lms/routes": "<2.1.1", "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2", "luyadev/yii-helpers": "<1.2.1", - "magento/community-edition": "<2.4.5|==2.4.5|>=2.4.5.0-patch1,<2.4.5.0-patch8|==2.4.6|>=2.4.6.0-patch1,<2.4.6.0-patch6|==2.4.7", + "maestroerror/php-heic-to-jpg": "<1.0.5", + "magento/community-edition": "<2.4.5|==2.4.5|>=2.4.5.0-patch1,<2.4.5.0-patch10|==2.4.6|>=2.4.6.0-patch1,<2.4.6.0-patch8|>=2.4.7.0-beta1,<2.4.7.0-patch3", "magento/core": "<=1.9.4.5", "magento/magento1ce": "<1.9.4.3-dev", "magento/magento1ee": ">=1,<1.14.4.3-dev", @@ -2124,11 +2477,13 @@ "magneto/core": "<1.9.4.4-dev", "maikuolan/phpmussel": ">=1,<1.6", "mainwp/mainwp": "<=4.4.3.3", - "mantisbt/mantisbt": "<2.26.2", + "mantisbt/mantisbt": "<=2.26.3", "marcwillmann/turn": "<0.3.3", "matyhtf/framework": "<3.0.6", - "mautic/core": "<4.4.12|>=5.0.0.0-alpha,<5.0.4", + "mautic/core": "<4.4.13|>=5,<5.1.1", + "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", "mdanter/ecc": "<2", + "mediawiki/cargo": "<3.6.1", "mediawiki/core": "<1.36.2", "mediawiki/matomo": "<2.4.3", "mediawiki/semantic-media-wiki": "<4.0.2", @@ -2162,6 +2517,7 @@ "munkireport/softwareupdate": "<1.6", "mustache/mustache": ">=2,<2.14.1", "namshi/jose": "<2.2", + "nategood/httpful": "<1", "neoan3-apps/template": "<1.1.1", "neorazorx/facturascripts": "<2022.04", "neos/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", @@ -2184,7 +2540,7 @@ "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", "october/backend": "<1.1.2", "october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1", - "october/october": "<=3.4.4", + "october/october": "<=3.6.4", "october/rain": "<1.0.472|>=1.1,<1.1.2", "october/system": "<1.0.476|>=1.1,<1.1.12|>=2,<2.2.34|>=3,<3.5.15", "omeka/omeka-s": "<4.0.3", @@ -2236,7 +2592,7 @@ "phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5", "phpoffice/common": "<0.2.9", "phpoffice/phpexcel": "<1.8", - "phpoffice/phpspreadsheet": "<1.16", + "phpoffice/phpspreadsheet": "<1.29.2|>=2,<2.1.1|>=2.2,<2.3", "phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36", "phpservermon/phpservermon": "<3.6", "phpsysinfo/phpsysinfo": "<3.4.3", @@ -2245,9 +2601,10 @@ "phpxmlrpc/extras": "<0.6.1", "phpxmlrpc/phpxmlrpc": "<4.9.2", "pi/pi": "<=2.5", - "pimcore/admin-ui-classic-bundle": "<=1.5.1", + "pimcore/admin-ui-classic-bundle": "<1.5.4", "pimcore/customer-management-framework-bundle": "<4.0.6", "pimcore/data-hub": "<1.2.4", + "pimcore/data-importer": "<1.8.9|>=1.9,<1.9.3", "pimcore/demo": "<10.3", "pimcore/ecommerce-framework-bundle": "<1.0.10", "pimcore/perspective-editor": "<1.5.1", @@ -2272,13 +2629,13 @@ "processwire/processwire": "<=3.0.229", "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", "propel/propel1": ">=1,<=1.7.1", - "pterodactyl/panel": "<1.11.6", + "pterodactyl/panel": "<1.11.8", "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", "ptrofimov/beanstalk_console": "<1.7.14", "pubnub/pubnub": "<6.1", "pusher/pusher-php-server": "<2.2.1", "pwweb/laravel-core": "<=0.3.6.0-beta", - "pxlrbt/filament-excel": "<2.3.3", + "pxlrbt/filament-excel": "<1.1.14|>=2.0.0.0-alpha,<2.3.3", "pyrocms/pyrocms": "<=3.9.1", "qcubed/qcubed": "<=3.1.1", "quickapps/cms": "<=2.0.0.0-beta2", @@ -2289,7 +2646,7 @@ "rap2hpoutre/laravel-log-viewer": "<0.13", "react/http": ">=0.7,<1.9", "really-simple-plugins/complianz-gdpr": "<6.4.2", - "redaxo/source": "<=5.15.1", + "redaxo/source": "<=5.17.1", "remdex/livehelperchat": "<4.29", "reportico-web/reportico": "<=8.1", "rhukster/dom-sanitizer": "<1.0.7", @@ -2345,7 +2702,7 @@ "slim/slim": "<2.6", "slub/slub-events": "<3.0.3", "smarty/smarty": "<4.5.3|>=5,<5.1.1", - "snipe/snipe-it": "<6.4.2", + "snipe/snipe-it": "<7.0.10", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", "spatie/browsershot": "<3.57.4", @@ -2355,6 +2712,7 @@ "spoonity/tcpdf": "<6.2.22", "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", "ssddanbrown/bookstack": "<24.05.1", + "starcitizentools/citizen-skin": ">=2.6.3,<2.31", "statamic/cms": "<4.46|>=5.3,<5.6.2", "stormpath/sdk": "<9.9.99", "studio-42/elfinder": "<=2.1.64", @@ -2362,7 +2720,7 @@ "subhh/libconnect": "<7.0.8|>=8,<8.1", "sukohi/surpass": "<1", "sulu/form-bundle": ">=2,<2.5.3", - "sulu/sulu": "<1.6.44|>=2,<2.4.17|>=2.5,<2.5.13", + "sulu/sulu": "<1.6.44|>=2,<2.5.21|>=2.6,<2.6.5", "sumocoders/framework-user-bundle": "<1.4", "superbig/craft-audit": "<3.0.2", "swag/paypal": "<5.4.4", @@ -2429,18 +2787,18 @@ "tinymighty/wiki-seo": "<1.2.2", "titon/framework": "<9.9.99", "tobiasbg/tablepress": "<=2.0.0.0-RC1", - "topthink/framework": "<6.0.17|>=6.1,<6.1.5|>=8,<8.0.4", + "topthink/framework": "<6.0.17|>=6.1,<=8.0.4", "topthink/think": "<=6.1.1", "topthink/thinkphp": "<=3.2.3", "torrentpier/torrentpier": "<=2.4.3", "tpwd/ke_search": "<4.0.3|>=4.1,<4.6.6|>=5,<5.0.2", - "tribalsystems/zenario": "<9.5.60602", + "tribalsystems/zenario": "<=9.7.61188", "truckersmp/phpwhois": "<=4.3.1", "ttskch/pagination-service-provider": "<1", "twbs/bootstrap": "<=3.4.1|>=4,<=4.6.2", - "twig/twig": "<1.44.7|>=2,<2.15.3|>=3,<3.4.3", + "twig/twig": "<1.44.8|>=2,<2.16.1|>=3,<3.11.1|>=3.12,<3.14", "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", - "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1", + "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<10.4.46|>=11,<11.5.40|>=12,<12.4.21|>=13,<13.3.1", "typo3/cms-core": "<=8.7.56|>=9,<=9.5.47|>=10,<=10.4.44|>=11,<=11.5.36|>=12,<=12.4.14|>=13,<=13.1", "typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1", "typo3/cms-fluid": "<4.3.4|>=4.4,<4.4.1", @@ -2490,6 +2848,7 @@ "winter/wn-dusk-plugin": "<2.1", "winter/wn-system-module": "<1.2.4", "wintercms/winter": "<=1.2.3", + "wireui/wireui": "<1.19.3|>=2,<2.1.3", "woocommerce/woocommerce": "<6.6|>=8.8,<8.8.5|>=8.9,<8.9.3", "wp-cli/wp-cli": ">=0.12,<2.5", "wp-graphql/wp-graphql": "<=1.14.5", @@ -2592,7 +2951,7 @@ "type": "tidelift" } ], - "time": "2024-08-23T19:04:38+00:00" + "time": "2024-10-28T19:04:33+00:00" } ], "aliases": [], diff --git a/lib/Controller/ObjectsController.php b/lib/Controller/ObjectsController.php index 253288a..12da7d9 100644 --- a/lib/Controller/ObjectsController.php +++ b/lib/Controller/ObjectsController.php @@ -2,22 +2,24 @@ namespace OCA\OpenRegister\Controller; +use OCA\OpenRegister\Exception\ValidationException; use OCA\OpenRegister\Service\ObjectService; use OCA\OpenRegister\Service\SearchService; -use OCA\OpenRegister\Db\ObjectEntity; +use OCA\OpenRegister\Db\ObjectAuditLogMapper; use OCA\OpenRegister\Db\ObjectEntityMapper; -use OCA\OpenRegister\Db\AuditTrail; use OCA\OpenRegister\Db\AuditTrailMapper; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; +use OCP\DB\Exception; use OCP\IAppConfig; use OCP\IRequest; +use Opis\JsonSchema\Errors\ErrorFormatter; use Symfony\Component\Uid\Uuid; class ObjectsController extends Controller { - + /** * Constructor for the ObjectsController @@ -31,7 +33,8 @@ public function __construct( IRequest $request, private readonly IAppConfig $config, private readonly ObjectEntityMapper $objectEntityMapper, - private readonly AuditTrailMapper $auditTrailMapper + private readonly AuditTrailMapper $auditTrailMapper, + private readonly ObjectAuditLogMapper $objectAuditLogMapper ) { parent::__construct($appName, $request); @@ -96,6 +99,7 @@ public function index(ObjectService $objectService, SearchService $searchService * @NoCSRFRequired * * @param string $id The ID of the object to retrieve + * * @return JSONResponse A JSON response containing the object details */ public function show(string $id): JSONResponse @@ -117,7 +121,7 @@ public function show(string $id): JSONResponse * * @return JSONResponse A JSON response containing the created object */ - public function create(): JSONResponse + public function create(ObjectService $objectService): JSONResponse { $data = $this->request->getParams(); $object = $data['object']; @@ -130,26 +134,29 @@ public function create(): JSONResponse if (isset($data['id'])) { unset($data['id']); - } - - // save it - $objectEntity = $this->objectEntityMapper->createFromArray(object: $data); - - $this->auditTrailMapper->createAuditTrail(new: $objectEntity); + + // Save the object + try { + $objectEntity = $objectService->saveObject(register: $data['register'], schema: $data['schema'], object: $object); + } catch (ValidationException $exception) { + $formatter = new ErrorFormatter(); + return new JSONResponse(['message' => $exception->getMessage(), 'validationErrors' => $formatter->format($exception->getErrors())], 400); + } return new JSONResponse($objectEntity->getObjectArray()); } /** - * Updates an existing object + * Updates an existing object * * This method updates an existing object based on its ID. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the object to update + * @param int $id The ID of the object to update + * * @return JSONResponse A JSON response containing the updated object details */ public function update(int $id): JSONResponse @@ -175,41 +182,87 @@ public function update(int $id): JSONResponse return new JSONResponse($objectEntity->getOBjectArray()); } + /** + * Deletes an object + * + * This method deletes an object based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $id The ID of the object to delete + * + * @return JSONResponse An empty JSON response + * @throws Exception + */ + public function destroy(int $id): JSONResponse + { + // Create a log entry + $oldObject = $this->objectEntityMapper->find($id); + $this->auditTrailMapper->createAuditTrail(old: $oldObject); + + $this->objectEntityMapper->delete($this->objectEntityMapper->find($id)); + + return new JSONResponse([]); + } + + /** + * Retrieves a list of logs for an object + * + * This method returns a JSON response containing the logs for a specific object. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $id The ID of the object to get AuditTrails for + * + * @return JSONResponse An empty JSON response + */ + public function auditTrails(int $id): JSONResponse + { + return new JSONResponse($this->auditTrailMapper->findAll(filters: ['object' => $id])); + } + /** - * Deletes an object + * Retrieves call logs for a object * - * This method deletes an object based on its ID. + * This method returns all the call logs associated with a object based on its ID. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the object to delete - * @return JSONResponse An empty JSON response + * @param int $id The ID of the object to retrieve logs for + * + * @return JSONResponse A JSON response containing the call logs */ - public function destroy(int $id): JSONResponse + public function contracts(int $id): JSONResponse { - // Create a log entry + // Create a log entry $oldObject = $this->objectEntityMapper->find($id); $this->auditTrailMapper->createAuditTrail(old: $oldObject); - $this->objectEntityMapper->delete($this->objectEntityMapper->find((int) $id)); - - return new JSONResponse([]); + return new JSONResponse(['error' => 'Not yet implemented'], 501); } /** - * Retrieves a list of logs for an object + * Retrieves call logs for an object * * This method returns a JSON response containing the logs for a specific object. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the object to delete - * @return JSONResponse An empty JSON response + * @param int $id The ID of the object to retrieve logs for + * + * @return JSONResponse A JSON response containing the call logs */ - public function auditTrails(int $id): JSONResponse + public function logs(int $id): JSONResponse { - return new JSONResponse($this->auditTrailMapper->findAll(filters: ['object' => $id])); + 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); + } } } diff --git a/lib/Controller/RegistersController.php b/lib/Controller/RegistersController.php index 424fe67..9314a4b 100644 --- a/lib/Controller/RegistersController.php +++ b/lib/Controller/RegistersController.php @@ -2,16 +2,20 @@ namespace OCA\OpenRegister\Controller; +use GuzzleHttp\Exception\GuzzleException; use OCA\OpenRegister\Service\ObjectService; use OCA\OpenRegister\Service\SearchService; use OCA\OpenRegister\Db\Register; use OCA\OpenRegister\Db\RegisterMapper; use OCA\OpenRegister\Db\ObjectEntityMapper; +use OCA\OpenRegister\Service\UploadService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; -use OCP\IAppConfig; +use OCP\DB\Exception; use OCP\IRequest; +use Symfony\Component\Uid\Uuid; +use Symfony\Component\Yaml\Yaml; class RegistersController extends Controller { @@ -20,14 +24,15 @@ 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 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 UploadService $uploadService ) { parent::__construct($appName, $request); @@ -35,7 +40,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 @@ -44,17 +49,17 @@ public function __construct( * @return TemplateResponse The rendered template response */ public function page(): TemplateResponse - { + { return new TemplateResponse( 'openconnector', 'index', [] ); } - + /** * Retrieves a list of all registers - * + * * This method returns a JSON response containing an array of all registers in the system. * * @NoAdminRequired @@ -76,7 +81,7 @@ public function index(ObjectService $objectService, SearchService $searchService /** * Retrieves a single register by its ID - * + * * This method returns a JSON response containing the details of a specific register. * * @NoAdminRequired @@ -96,7 +101,7 @@ public function show(string $id): JSONResponse /** * Creates a new register - * + * * This method creates a new register based on POST data. * * @NoAdminRequired @@ -113,23 +118,23 @@ public function create(): JSONResponse unset($data[$key]); } } - + if (isset($data['id'])) { unset($data['id']); } - + return new JSONResponse($this->registerMapper->createFromArray(object: $data)); } /** * Updates an existing register - * + * * This method updates an existing register based on its ID. * * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the register to update + * @param int $id The ID of the register to update * @return JSONResponse A JSON response containing the updated register details */ public function update(int $id): JSONResponse @@ -147,17 +152,18 @@ public function update(int $id): JSONResponse return new JSONResponse($this->registerMapper->updateFromArray(id: (int) $id, object: $data)); } - /** - * Deletes a register - * - * This method deletes a register based on its ID. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @param string $id The ID of the register to delete - * @return JSONResponse An empty JSON response - */ + /** + * Deletes a register + * + * This method deletes a register based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $id The ID of the register to delete + * @return JSONResponse An empty JSON response + * @throws Exception + */ public function destroy(int $id): JSONResponse { $this->registerMapper->delete($this->registerMapper->find((int) $id)); @@ -165,21 +171,89 @@ public function destroy(int $id): JSONResponse return new JSONResponse([]); } - /** - * Get objects - * - * Get all the objects for a register and schema - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @param string $register The ID of the register - * @param string $schema The ID of the schema - * - * @return JSONResponse An empty JSON response - */ + /** + * Get objects + * + * Get all the objects for a register and schema + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $register The ID of the register + * @param int $schema The ID of the schema + * + * @return JSONResponse An empty JSON response + */ public function objects(int $register, int $schema): JSONResponse { return new JSONResponse($this->objectEntityMapper->findByRegisterAndSchema(register: $register, schema: $schema)); } -} \ No newline at end of file + + /** + * Updates an existing Register object using a json text/string as input. Uses 'file', 'url' or else 'json' from POST body. + * + * @param int|null $id + * + * @return JSONResponse + * @throws Exception + * @throws GuzzleException + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function uploadUpdate(?int $id = null): JSONResponse + { + return $this->upload($id); + } + + /** + * Creates a new Register object or updates an existing one using a json text/string as input. Uses 'file', 'url' or else 'json' from POST body. + * + * @param int|null $id + * + * @return JSONResponse + * @throws GuzzleException + * @throws Exception + * + * @NoAdminRequired + * @NoCSRFRequired + * + */ + public function upload(?int $id = null): JSONResponse + { + if ($id !== null){ + $register = $this->registerMapper->find($id); + } + else { + $register = new Register(); + $register->setUuid(Uuid::v4()); + } + + $phpArray = $this->uploadService->getUploadedJson($this->request->getParams()); + if ($phpArray instanceof JSONResponse) { + return $phpArray; + } + + // Validate that the jsonArray is a valid OAS3 object containing schemas + if (isset($phpArray['openapi']) === false || isset($phpArray['components']['schemas']) === false) { + 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 (empty($phpArray['info']['title']) === true) { + $phpArray['info']['title'] = 'New Register'; + } + + $register->hydrate($phpArray); + if ($register->getId() === null) { + $register = $this->registerMapper->insert($register); + } else { + $register = $this->registerMapper->update($register); + } + + // Process and save schemas + $register = $this->uploadService->handleRegisterSchemas(register: $register, phpArray: $phpArray); + + return new JSONResponse($register); + } +} diff --git a/lib/Controller/SchemasController.php b/lib/Controller/SchemasController.php index 82f4223..269e737 100644 --- a/lib/Controller/SchemasController.php +++ b/lib/Controller/SchemasController.php @@ -2,15 +2,20 @@ namespace OCA\OpenRegister\Controller; +use GuzzleHttp\Exception\GuzzleException; +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; +use OCP\DB\Exception; use OCP\IAppConfig; use OCP\IRequest; +use Symfony\Component\Uid\Uuid; class SchemasController extends Controller { @@ -20,12 +25,17 @@ 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 */ 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 +43,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 +52,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 +79,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 +104,7 @@ public function show(string $id): JSONResponse /** * Creates a new schema - * + * * This method creates a new schema based on POST data. * * @NoAdminRequired @@ -111,23 +121,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 @@ -139,27 +149,130 @@ public function update(int $id): JSONResponse unset($data[$key]); } } + 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 - * @return JSONResponse An empty JSON response - */ + /** + * Deletes a schema + * + * This method deletes a schema based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $id The ID of the schema to delete + * @return JSONResponse An empty JSON response + * @throws Exception + */ 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 + + /** + * Updates an existing Schema object using a json text/string as input. Uses 'file', 'url' or else 'json' from POST body. + * + * @param int|null $id + * + * @return JSONResponse + * @throws Exception + * @throws GuzzleException + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function uploadUpdate(?int $id = null): JSONResponse + { + return $this->upload($id); + } + + /** + * Creates a new Schema object or updates an existing one using a json text/string as input. Uses 'file', 'url' or else 'json' from POST body. + * + * @param int|null $id + * + * @return JSONResponse + * @throws Exception + * @throws GuzzleException + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function upload(?int $id = null): JSONResponse + { + if ($id !== null){ + $schema = $this->schemaMapper->find($id); + } else { + $schema = new Schema(); + $schema->setUuid(Uuid::v4()); + } + + $phpArray = $this->uploadService->getUploadedJson($this->request->getParams()); + if ($phpArray instanceof JSONResponse) { + return $phpArray; + } + + // Set default title if not provided or empty + if (empty($phpArray['title']) === true) { + $phpArray['title'] = 'New Schema'; + } + + $schema->hydrate($phpArray); + if ($schema->getId() === null) { + $schema = $this->schemaMapper->insert($schema); + } else { + $schema = $this->schemaMapper->update($schema); + } + + 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/Db/AuditTrailMapper.php b/lib/Db/AuditTrailMapper.php index 8df5069..c62b2f6 100644 --- a/lib/Db/AuditTrailMapper.php +++ b/lib/Db/AuditTrailMapper.php @@ -10,16 +10,33 @@ use Symfony\Component\Uid\Uuid; use OCA\OpenRegister\Db\ObjectEntityMapper; +/** + * The AuditTrailMapper class + * + * @package OCA\OpenRegister\Db + */ class AuditTrailMapper extends QBMapper { private $objectEntityMapper; + /** + * Constructor for the AuditTrailMapper + * + * @param IDBConnection $db The database connection + * @param ObjectEntityMapper $objectEntityMapper The object entity mapper + */ public function __construct(IDBConnection $db, ObjectEntityMapper $objectEntityMapper) { parent::__construct($db, 'openregister_audit_trails'); $this->objectEntityMapper = $objectEntityMapper; } + /** + * Finds an audit trail by id + * + * @param int $id The id of the audit trail + * @return Log The audit trail + */ public function find(int $id): Log { $qb = $this->db->getQueryBuilder(); @@ -33,6 +50,16 @@ public function find(int $id): Log return $this->findEntity(query: $qb); } + /** + * Finds all audit trails + * + * @param int|null $limit The limit of the results + * @param int|null $offset The offset of the results + * @param array|null $filters The filters to apply + * @param array|null $searchConditions The search conditions to apply + * @param array|null $searchParams The search parameters to apply + * @return array The audit trails + */ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array { $qb = $this->db->getQueryBuilder(); @@ -62,6 +89,12 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters return $this->findEntities(query: $qb); } + /** + * Finds all audit trails for a given object + * + * @param string $idOrUuid The id or uuid of the object + * @return array The audit trails + */ public function findAllUuid(string $idOrUuid, ?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array { try { @@ -74,6 +107,12 @@ public function findAllUuid(string $idOrUuid, ?int $limit = null, ?int $offset = } } + /** + * Creates an audit trail from an array + * + * @param array $object The object to create the audit trail from + * @return Log The created audit trail + */ public function createFromArray(array $object): Log { $log = new Log(); diff --git a/lib/Db/ObjectAuditLog.php b/lib/Db/ObjectAuditLog.php new file mode 100644 index 0000000..852ed8e --- /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 + ]; + } +} diff --git a/lib/Db/ObjectAuditLogMapper.php b/lib/Db/ObjectAuditLogMapper.php new file mode 100644 index 0000000..2b27355 --- /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 969aa8d..68c9245 100644 --- a/lib/Db/ObjectEntity.php +++ b/lib/Db/ObjectEntity.php @@ -9,9 +9,9 @@ class ObjectEntity extends Entity implements JsonSerializable { protected ?string $uuid = null; + protected ?string $version = null; protected ?string $register = null; protected ?string $schema = null; - protected ?string $version = null; protected ?array $object = []; protected ?string $textRepresentation = null; protected ?DateTime $updated = null; @@ -19,9 +19,9 @@ 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: 'version', type: 'string'); $this->addType(fieldName:'object', type: 'json'); $this->addType(fieldName:'textRepresentation', type: 'text'); $this->addType(fieldName:'updated', type: 'datetime'); diff --git a/lib/Db/ObjectEntityMapper.php b/lib/Db/ObjectEntityMapper.php index 5090d1a..a883fba 100644 --- a/lib/Db/ObjectEntityMapper.php +++ b/lib/Db/ObjectEntityMapper.php @@ -14,12 +14,23 @@ use OCP\IDBConnection; use Symfony\Component\Uid\Uuid; +/** + * The ObjectEntityMapper class + * + * @package OCA\OpenRegister\Db + */ class ObjectEntityMapper extends QBMapper { private IDatabaseJsonService $databaseJsonService; public const MAIN_FILTERS = ['register', 'schema', 'uuid', 'created', 'updated']; + /** + * Constructor for the ObjectEntityMapper + * + * @param IDBConnection $db The database connection + * @param MySQLJsonService $mySQLJsonService The MySQL JSON service + */ public function __construct(IDBConnection $db, MySQLJsonService $mySQLJsonService) { parent::__construct($db, 'openregister_objects'); @@ -103,6 +114,13 @@ public function findByRegisterAndSchema(string $register, string $schema): Objec return $this->findEntities(query: $qb); } + /** + * Counts all objects + * + * @param array|null $filters The filters to apply + * @param string|null $search The search string to apply + * @return int The number of objects + */ public function countAll(?array $filters = [], ?string $search = null): int { $qb = $this->db->getQueryBuilder(); @@ -172,27 +190,49 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters return $this->findEntities(query: $qb); } + /** + * Creates an object from an array + * + * @param array $object The object to create + * @return ObjectEntity The created object + */ public function createFromArray(array $object): ObjectEntity { $obj = new ObjectEntity(); $obj->hydrate(object: $object); - if ($obj->getUuid() === null){ + if ($obj->getUuid() === null) { $obj->setUuid(Uuid::v4()); } - return $this->insert(entity: $obj); + return $this->insert($obj); } + /** + * Updates an object from an array + * + * @param int $id The id of the object to update + * @param array $object The object to update + * @return ObjectEntity The updated object + */ 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); } + /** + * Gets the facets for the objects + * + * @param array $filters The filters to apply + * @param string|null $search The search string to apply + * @return array The facets + */ public function getFacets(array $filters = [], ?string $search = null) { if(key_exists(key: 'register', array: $filters) === true) { diff --git a/lib/Db/Register.php b/lib/Db/Register.php index 73607fb..0c03d93 100644 --- a/lib/Db/Register.php +++ b/lib/Db/Register.php @@ -23,11 +23,12 @@ public function __construct() { $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: 'schemas', type: 'json'); $this->addType(fieldName: 'source', type: 'string'); $this->addType(fieldName: 'tablePrefix', type: 'string'); - $this->addType(fieldName:'updated', type: 'datetime'); - $this->addType(fieldName:'created', type: 'datetime'); + $this->addType(fieldName: 'updated', type: 'datetime'); + $this->addType(fieldName: 'created', type: 'datetime'); } public function getJsonFields(): array diff --git a/lib/Db/RegisterMapper.php b/lib/Db/RegisterMapper.php index a04f918..9dcdf83 100644 --- a/lib/Db/RegisterMapper.php +++ b/lib/Db/RegisterMapper.php @@ -6,38 +6,74 @@ 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; +/** + * The RegisterMapper class + * + * @package OCA\OpenRegister\Db + */ 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 +84,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,9 +92,16 @@ 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 { $register = new Register(); @@ -71,11 +115,66 @@ public function createFromArray(array $object): Register return $this->insert(entity: $register); } + /** + * 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 { - $register = $this->find($id); - $register->hydrate($object); + $obj = $this->find($id); + $obj->hydrate($object); - return $this->update($register); + // 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/Db/Schema.php b/lib/Db/Schema.php index 04423ff..314ed55 100644 --- a/lib/Db/Schema.php +++ b/lib/Db/Schema.php @@ -5,31 +5,38 @@ use DateTime; use JsonSerializable; use OCP\AppFramework\Db\Entity; +use OCP\DB\Types; +use OCP\IURLGenerator; +use stdClass; class Schema extends Entity implements JsonSerializable { - protected ?string $uuid = null; + 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; + protected bool $hardValidation = false; + protected ?DateTime $updated = null; + protected ?DateTime $created = null; public function __construct() { $this->addType(fieldName: 'uuid', type: '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'); - $this->addType(fieldName:'updated', type: 'datetime'); - $this->addType(fieldName:'created', type: 'datetime'); + $this->addType(fieldName: 'archive', type: 'json'); + $this->addType(fieldName: 'source', type: 'string'); + $this->addType(fieldName: 'hardValidation', type: Types::BOOLEAN); + $this->addType(fieldName: 'updated', type: 'datetime'); + $this->addType(fieldName: 'created', type: 'datetime'); } public function getJsonFields(): array @@ -65,43 +72,51 @@ public function hydrate(array $object): self return $this; } - + /** + * Serializes the schema to an array + * + * @return array + */ public function jsonSerialize(): array { $properties = []; - foreach ($this->properties as $key => $property) { - $properties[$key] = $property; - if (isset($property['type']) === false) { - $properties[$key] = $property; - continue; - } - switch ($property['format']) { - case 'string': - // For now array as string - case 'array': - $properties[$key]['default'] = (string) $property; - break; - case 'int': - case 'integer': - case 'number': - $properties[$key]['default'] = (int) $property; - break; - case 'bool': - $properties[$key]['default'] = (bool) $property; - break; - - } - } + if (isset($this->properties) === true) { + foreach ($this->properties as $key => $property) { + $properties[$key] = $property; + if (isset($property['type']) === false) { + $properties[$key] = $property; + continue; + } + switch ($property['format']) { + case 'string': + // For now array as string + case 'array': + $properties[$key]['default'] = (string) $property; + break; + case 'int': + case 'integer': + case 'number': + $properties[$key]['default'] = (int) $property; + break; + case 'bool': + $properties[$key]['default'] = (bool) $property; + break; + } + } + } $array = [ 'id' => $this->id, - 'uuid' => $this->uuid, + 'uuid' => $this->uuid, 'title' => $this->title, - 'version' => $this->version, 'description' => $this->description, + 'version' => $this->version, 'summary' => $this->summary, 'required' => $this->required, 'properties' => $properties, + 'archive' => $this->archive, + 'source' => $this->source, + 'hardValidation' => $this->hardValidation, 'updated' => isset($this->updated) ? $this->updated->format('c') : null, 'created' => isset($this->created) ? $this->created->format('c') : null, ]; @@ -116,4 +131,38 @@ public function jsonSerialize(): array return $array; } + + /** + * Creates a decoded JSON Schema object from the information in the schema + * + * @return object Decoded JSON Schema object. + */ + public function getSchemaObject(IURLGenerator $urlGenerator): object + { + $data = $this->jsonSerialize(); + $properties = $data['properties']; + unset($data['properties'], $data['id'], $data['uuid'], $data['summary'], $data['archive'], $data['source'], + $data['updated'], $data['created']); + + $data['required'] = []; + + $data['type'] = 'object'; + + foreach ($properties as $property) { + $title = $property['title']; + if ($property['required'] === true) { + $data['required'][] = $title; + } + unset($property['title'], $property['required']); + + // Remove empty fields with array_filter(). + $data['properties'][$title] = array_filter($property); + } + + $data['$schema'] = 'https://json-schema.org/draft/2020-12/schema'; + $data['$id'] = $urlGenerator->getAbsoluteURL($urlGenerator->linkToRoute('openregister.Schemas.show', ['id' => $this->getUuid()])); + + + return json_decode(json_encode($data)); + } } diff --git a/lib/Db/SchemaMapper.php b/lib/Db/SchemaMapper.php index 26a517c..2ca6725 100644 --- a/lib/Db/SchemaMapper.php +++ b/lib/Db/SchemaMapper.php @@ -9,13 +9,29 @@ use OCP\IDBConnection; use Symfony\Component\Uid\Uuid; +/** + * The SchemaMapper class + * + * @package OCA\OpenRegister\Db + */ class SchemaMapper extends QBMapper { + /** + * Constructor for the SchemaMapper + * + * @param IDBConnection $db The database connection + */ public function __construct(IDBConnection $db) { parent::__construct($db, 'openregister_schemas'); } + /** + * Finds a schema by id + * + * @param int $id The id of the schema + * @return Schema The schema + */ public function find(int $id): Schema { $qb = $this->db->getQueryBuilder(); @@ -28,6 +44,13 @@ public function find(int $id): Schema return $this->findEntity(query: $qb); } + + /** + * Finds multiple schemas by id + * + * @param array $ids The ids of the schemas + * @return array The schemas + */ public function findMultiple(array $ids): array { $result = []; @@ -38,6 +61,17 @@ public function findMultiple(array $ids): array return $result; } + + /** + * Finds all schemas + * + * @param int|null $limit The limit of the results + * @param int|null $offset The offset of the results + * @param array|null $filters The filters to apply + * @param array|null $searchConditions The search conditions to apply + * @param array|null $searchParams The search parameters to apply + * @return array The schemas + */ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array { $qb = $this->db->getQueryBuilder(); @@ -67,6 +101,12 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters return $this->findEntities(query: $qb); } + /** + * Creates a schema from an array + * + * @param array $object The object to create + * @return Schema The created schema + */ public function createFromArray(array $object): Schema { $schema = new Schema(); @@ -80,11 +120,23 @@ public function createFromArray(array $object): Schema return $this->insert(entity: $schema); } + /** + * Updates a schema from an array + * + * @param int $id The id of the schema to update + * @param array $object The object to update + * @return Schema The updated schema + */ 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/SourceMapper.php b/lib/Db/SourceMapper.php index 94a3eb0..3dbfac3 100644 --- a/lib/Db/SourceMapper.php +++ b/lib/Db/SourceMapper.php @@ -9,13 +9,30 @@ use OCP\IDBConnection; use Symfony\Component\Uid\Uuid; +/** + * The SourceMapper class + * + * @package OCA\OpenRegister\Db + */ class SourceMapper extends QBMapper { + /** + * Constructor for the SourceMapper + * + * @param IDBConnection $db The database connection + */ public function __construct(IDBConnection $db) { parent::__construct($db, 'openregister_sources'); } + + /** + * Finds a source by id + * + * @param int $id The id of the source + * @return Source The source + */ public function find(int $id): Source { $qb = $this->db->getQueryBuilder(); @@ -29,6 +46,16 @@ public function find(int $id): Source return $this->findEntity(query: $qb); } + /** + * Finds all sources + * + * @param int|null $limit The limit of the results + * @param int|null $offset The offset of the results + * @param array|null $filters The filters to apply + * @param array|null $searchConditions The search conditions to apply + * @param array|null $searchParams The search parameters to apply + * @return array The sources + */ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array { $qb = $this->db->getQueryBuilder(); @@ -58,6 +85,12 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters return $this->findEntities(query: $qb); } + /** + * Creates a source from an array + * + * @param array $object The object to create + * @return Source The created source + */ public function createFromArray(array $object): Source { $source = new Source(); @@ -70,11 +103,23 @@ public function createFromArray(array $object): Source return $this->insert(entity: $source); } + /** + * Updates a source from an array + * + * @param int $id The id of the source to update + * @param array $object The object to update + * @return Source The updated source + */ 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/Exception/ValidationException.php b/lib/Exception/ValidationException.php new file mode 100644 index 0000000..c3c8196 --- /dev/null +++ b/lib/Exception/ValidationException.php @@ -0,0 +1,25 @@ +errors; + } + +} diff --git a/lib/Formats/BsnFormat.php b/lib/Formats/BsnFormat.php new file mode 100644 index 0000000..509e030 --- /dev/null +++ b/lib/Formats/BsnFormat.php @@ -0,0 +1,36 @@ + 1) ? $reversedIterator : -1); + $reversedIterator--; + } + + return $control % 11 === 0; + } +} diff --git a/lib/Migration/Version1Date20240924200009.php b/lib/Migration/Version1Date20240924200009.php index 0347615..add362c 100755 --- a/lib/Migration/Version1Date20240924200009.php +++ b/lib/Migration/Version1Date20240924200009.php @@ -38,11 +38,13 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); - if (!$schema->hasTable('openregister_sources')) { + if ($schema->hasTable('openregister_sources') === false) { $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')) { + if ($schema->hasTable('openregister_schemas') === false) { $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,11 +71,14 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->setPrimaryKey(['id']); $table->addIndex(['title'], 'register_schemas_title_index'); + $table->addIndex(['uuid'], 'register_schemas_uuid_index'); } - if (!$schema->hasTable('openregister_registers')) { + if ($schema->hasTable('openregister_registers') === false) { $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]); @@ -83,15 +90,17 @@ 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'); + if ($schema->hasTable('openregister_objects') === false) { + $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]); + $table->addColumn('object', Types::JSON, ['notnull' => false]); $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); diff --git a/lib/Migration/Version1Date20241030131427.php b/lib/Migration/Version1Date20241030131427.php new file mode 100644 index 0000000..aa8479d --- /dev/null +++ b/lib/Migration/Version1Date20241030131427.php @@ -0,0 +1,79 @@ +getTable('openregister_schemas'); + if ($table->hasColumn('hard_validation') === false) { + $table->addColumn(name: 'hard_validation', typeName: Types::BOOLEAN, options: ['notnull' => true])->setDefault(default: false); + } + if ($table->hasColumn('archive') === false) { + $table->addColumn(name: 'archive', typeName: Types::JSON, options: ['notnull' => false])->setDefault(default: '{}'); + } + + if ($schema->hasTable('openregister_object_audit_logs') === false) { + $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; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} 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 @@ + 'objects', - 'collection' => 'json', + 'database' => 'objects', // The default database name + 'collection' => 'json', // The default collection name ]; /** - * Gets a guzzle client based upon given config. + * Gets a configured Guzzle HTTP client * - * @param array $config The config to be used for the client. - * @return Client + * @param array $config Configuration array containing connection details + * @return Client Configured Guzzle client instance */ public function getClient(array $config): Client { + // Remove MongoDB specific config before creating Guzzle client $guzzleConf = $config; unset($guzzleConf['mongodbCluster']); @@ -32,21 +44,24 @@ public function getClient(array $config): Client /** * Save an object to MongoDB * - * @param array $data The data to be saved. - * @param array $config The configuration that should be used by the call. - * - * @return array The resulting object. - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $data The data object to be saved + * @param array $config MongoDB connection configuration + * @return array The saved object with generated ID + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function saveObject(array $data, array $config): array { + // Initialize HTTP client $client = $this->getClient(config: $config); + // Prepare object with base configuration and data $object = self::BASE_OBJECT; $object['dataSource'] = $config['mongodbCluster']; $object['document'] = $data; + // Generate and set UUID for new document $object['document']['id'] = $object['document']['_id'] = Uuid::v4(); + // Insert document via API $result = $client->post( uri: 'action/insertOne', options: ['json' => $object], @@ -57,23 +72,23 @@ public function saveObject(array $data, array $config): array ); $id = $resultData['insertedId']; + // Return complete object by finding it with new ID return $this->findObject(filters: ['_id' => $id], config: $config); } /** - * Finds objects based upon a set of filters. - * - * @param array $filters The filters to compare the object to. - * @param array $config The configuration that should be used by the call. + * Find multiple objects matching given filters * - * @return array The objects found for given filters. - * - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $filters Query filters to match documents + * @param array $config MongoDB connection configuration + * @return array Array of matching documents + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function findObjects(array $filters, array $config): array { $client = $this->getClient(config: $config); + // Prepare query object $object = self::BASE_OBJECT; $object['dataSource'] = $config['mongodbCluster']; $object['filter'] = $filters; @@ -83,6 +98,7 @@ public function findObjects(array $filters, array $config): array // $object['filter'][] = ['$sort' => $sort]; // } + // Execute find query via API $returnData = $client->post( uri: 'action/find', options: ['json' => $object] @@ -95,23 +111,23 @@ public function findObjects(array $filters, array $config): array } /** - * Finds an object based upon a set of filters (usually the id) - * - * @param array $filters The filters to compare the objects to. - * @param array $config The config to be used by the call. + * Find a single object matching given filters * - * @return array The resulting object. - * - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $filters Query filters to match document + * @param array $config MongoDB connection configuration + * @return array The matched document + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function findObject(array $filters, array $config): array { $client = $this->getClient(config: $config); + // Prepare query object $object = self::BASE_OBJECT; $object['filter'] = $filters; $object['dataSource'] = $config['mongodbCluster']; + // Execute findOne query via API $returnData = $client->post( uri: 'action/findOne', options: ['json' => $object] @@ -125,59 +141,57 @@ public function findObject(array $filters, array $config): array return $result['document']; } - - /** - * Updates an object in MongoDB - * - * @param array $filters The filter to search the object with (id) - * @param array $update The fields that should be updated. - * @param array $config The configuration to be used by the call. - * - * @return array The updated object. + * Update an existing object in MongoDB * - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $filters Query filters to match document for update + * @param array $update Update operations to apply + * @param array $config MongoDB connection configuration + * @return array The updated document + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function updateObject(array $filters, array $update, array $config): array { $client = $this->getClient(config: $config); + // Convert update data to dot notation for nested updates $dotUpdate = new Dot($update); + // Prepare update query $object = self::BASE_OBJECT; $object['filter'] = $filters; $object['update']['$set'] = $update; $object['upsert'] = true; $object['dataSource'] = $config['mongodbCluster']; + // Execute update via API + $returnData = $client->post( + uri: 'action/updateOne', + options: ['json' => $object] + ); - - $returnData = $client->post( - uri: 'action/updateOne', - options: ['json' => $object] - ); - + // Return updated document return $this->findObject($filters, $config); } /** - * Delete an object according to a filter (id specifically) + * Delete an object from MongoDB * - * @param array $filters The filters to use. - * @param array $config The config to be used by the call. - * - * @return array An empty array. - * - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $filters Query filters to match document for deletion + * @param array $config MongoDB connection configuration + * @return array Empty array on successful deletion + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function deleteObject(array $filters, array $config): array { $client = $this->getClient(config: $config); + // Prepare delete query $object = self::BASE_OBJECT; $object['filter'] = $filters; $object['dataSource'] = $config['mongodbCluster']; + // Execute deletion via API $returnData = $client->post( uri: 'action/deleteOne', options: ['json' => $object] @@ -187,23 +201,25 @@ public function deleteObject(array $filters, array $config): array } /** - * Aggregates objects for search facets. + * Perform aggregation operations on MongoDB collection * - * @param array $filters The filters apply to the search request. - * @param array $pipeline The pipeline to use. - * @param array $config The configuration to use in the call. - * @return array - * @throws \GuzzleHttp\Exception\GuzzleException + * @param array $filters Initial query filters + * @param array $pipeline Aggregation pipeline stages + * @param array $config MongoDB connection configuration + * @return array Aggregation results + * @throws \GuzzleHttp\Exception\GuzzleException When API request fails */ public function aggregateObjects(array $filters, array $pipeline, array $config):array { $client = $this->getClient(config: $config); + // Prepare aggregation query $object = self::BASE_OBJECT; $object['filter'] = $filters; $object['pipeline'] = $pipeline; $object['dataSource'] = $config['mongodbCluster']; + // Execute aggregation via API $returnData = $client->post( uri: 'action/aggregate', options: ['json' => $object] @@ -213,7 +229,5 @@ public function aggregateObjects(array $filters, array $pipeline, array $config) json: $returnData->getBody()->getContents(), associative: true ); - } - } diff --git a/lib/Service/MySQLJsonService.php b/lib/Service/MySQLJsonService.php index 43e9150..f0ff964 100644 --- a/lib/Service/MySQLJsonService.php +++ b/lib/Service/MySQLJsonService.php @@ -5,18 +5,30 @@ use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; +/** + * Service class for handling MySQL JSON operations + * + * This class provides methods for querying and filtering JSON data stored in MySQL, + * including complex filtering, searching, ordering and aggregation functionality. + */ class MySQLJsonService implements IDatabaseJsonService { /** - * @inheritDoc + * Add ordering to a query based on JSON fields + * + * @param IQueryBuilder $builder The query builder instance + * @param array $order Array of field => direction pairs for ordering + * @return IQueryBuilder The modified query builder */ public function orderJson(IQueryBuilder $builder, array $order = []): IQueryBuilder { - + // Loop through each ordering field and direction foreach($order as $item=>$direction) { + // Create parameters for the JSON path and sort direction $builder->createNamedParameter(value: "$.$item", placeHolder: ":path$item"); $builder->createNamedParameter(value: $direction, placeHolder: ":direction$item"); + // Add ORDER BY clause using JSON_UNQUOTE and JSON_EXTRACT $builder->orderBy($builder->createFunction("json_unquote(json_extract(object, :path$item))"),$direction); } @@ -24,12 +36,18 @@ public function orderJson(IQueryBuilder $builder, array $order = []): IQueryBuil } /** - * @inheritDoc + * Add full-text search functionality for JSON fields + * + * @param IQueryBuilder $builder The query builder instance + * @param string|null $search The search term to look for + * @return IQueryBuilder The modified query builder */ public function searchJson(IQueryBuilder $builder, ?string $search = null): IQueryBuilder { if ($search !== null) { + // Create parameter for the search term with wildcards $builder->createNamedParameter(value: "%$search%", placeHolder: ':search'); + // Add WHERE clause to search case-insensitive across all JSON fields $builder->andWhere("JSON_SEARCH(LOWER(object), 'one', LOWER(:search)) IS NOT NULL"); } @@ -39,28 +57,33 @@ public function searchJson(IQueryBuilder $builder, ?string $search = null): IQue /** * Add complex filters to the filter set. * - * @param IQueryBuilder $builder The query builder - * @param string $filter The filtered field. - * @param array $values The values to filter on. + * Handles special filter cases like 'after' and 'before' for date ranges, + * as well as IN clauses for arrays of values. * - * @return IQueryBuilder The updated query builder. + * @param IQueryBuilder $builder The query builder instance + * @param string $filter The filtered field + * @param array $values The values to filter on + * @return IQueryBuilder The modified query builder */ private function jsonFilterArray(IQueryBuilder $builder, string $filter, array $values): IQueryBuilder { foreach ($values as $key=>$value) { switch ($key) { case 'after': + // Add >= filter for dates after specified value $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR, placeHolder: ":value{$filter}after"); $builder ->andWhere("json_unquote(json_extract(object, :path$filter)) >= (:value{$filter}after)"); break; case 'before': - $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR, placeHolder: ":value${filter}before"); + // Add <= filter for dates before specified value + $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR, placeHolder: ":value{$filter}before"); $builder ->andWhere("json_unquote(json_extract(object, :path$filter)) <= (:value{$filter}before)"); break; default: - $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR_ARRAY, placeHolder: ":value$filter"); + // Add IN clause for array of values + $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR_ARRAY, placeHolder: ":value{$filter}"); $builder ->andWhere("json_unquote(json_extract(object, :path$filter)) IN (:value$filter)"); break; @@ -74,18 +97,22 @@ private function jsonFilterArray(IQueryBuilder $builder, string $filter, array $ /** * Build a string to search multiple values in an array. * - * @param array $values The values to search for. - * @param string $filter The field to filter on. - * @param IQueryBuilder $builder The query builder. + * Creates an OR condition for each value to check if it exists + * within a JSON array field. * - * @return string The resulting query string. + * @param array $values The values to search for + * @param string $filter The field to filter on + * @param IQueryBuilder $builder The query builder instance + * @return string The resulting OR conditions as a string */ private function getMultipleContains (array $values, string $filter, IQueryBuilder $builder): string { $orString = ''; foreach($values as $key=>$value) { + // Create parameter for each value $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR, placeHolder: ":value$filter$key"); + // Add OR condition checking if value exists in JSON array $orString .= " OR json_contains(object, json_quote(:value$filter$key), :path$filter)"; } @@ -93,27 +120,39 @@ private function getMultipleContains (array $values, string $filter, IQueryBuild } /** - * @inheritDoc + * Add JSON filtering to a query + * + * Handles various filter types including: + * - Complex filters (after/before) + * - Array filters + * - Simple equality filters + * + * @param IQueryBuilder $builder The query builder instance + * @param array $filters Array of filters to apply + * @return IQueryBuilder The modified query builder */ public function filterJson(IQueryBuilder $builder, array $filters): IQueryBuilder { + // Remove special system fields from filters unset($filters['register'], $filters['schema'], $filters['updated'], $filters['created'], $filters['_queries']); foreach($filters as $filter=>$value) { - + // Create parameter for JSON path $builder->createNamedParameter(value: "$.$filter", placeHolder: ":path$filter"); if(is_array($value) === true && array_is_list($value) === false) { + // Handle complex filters (after/before) $builder = $this->jsonFilterArray(builder: $builder, filter: $filter, values: $value); continue; } else if (is_array($value) === true) { - + // Handle array of values with IN clause and contains check $builder->createNamedParameter(value: $value, type: IQueryBuilder::PARAM_STR_ARRAY, placeHolder: ":value$filter"); $builder ->andWhere("(json_unquote(json_extract(object, :path$filter)) IN (:value$filter))". $this->getMultipleContains($value, $filter, $builder)); continue; } + // Handle simple equality filter $builder->createNamedParameter(value: $value, placeHolder: ":value$filter"); $builder ->andWhere("json_extract(object, :path$filter) = :value$filter OR json_contains(object, json_quote(:value$filter), :path$filter)"); @@ -123,16 +162,28 @@ public function filterJson(IQueryBuilder $builder, array $filters): IQueryBuilde } /** - * @inheritDoc + * Get aggregations (facets) for specified fields + * + * Returns counts of unique values for each specified field, + * filtered by the provided filters and search term. + * + * @param IQueryBuilder $builder The query builder instance + * @param array $fields Fields to get aggregations for + * @param int $register Register ID to filter by + * @param int $schema Schema ID to filter by + * @param array $filters Additional filters to apply + * @param string|null $search Optional search term + * @return array Array of facets with counts for each field */ public function getAggregations(IQueryBuilder $builder, array $fields, int $register, int $schema, array $filters = [], ?string $search = null): array { $facets = []; foreach($fields as $field) { + // Create parameter for JSON path $builder->createNamedParameter(value: "$.$field", placeHolder: ":$field"); - + // Build base query for aggregation $builder ->selectAlias($builder->createFunction("json_unquote(json_extract(object, :$field))"), '_id') ->selectAlias($builder->createFunction("count(*)"), 'count') @@ -143,12 +194,15 @@ public function getAggregations(IQueryBuilder $builder, array $fields, int $regi ) ->groupBy('_id'); + // Apply filters and search $builder = $this->filterJson($builder, $filters); $builder = $this->searchJson($builder, $search); + // Execute query and store results $result = $builder->executeQuery(); $facets[$field] = $result->fetchAll(); + // Reset builder for next field $builder->resetQueryParts(); $builder->setParameters([]); diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index e711a37..90d5113 100755 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -2,6 +2,7 @@ namespace OCA\OpenRegister\Service; +use OC\URLGenerator; use OCA\OpenRegister\Db\Source; use OCA\OpenRegister\Db\SourceMapper; use OCA\OpenRegister\Db\Schema; @@ -12,462 +13,567 @@ use OCA\OpenRegister\Db\ObjectEntityMapper; use OCA\OpenRegister\Db\AuditTrail; use OCA\OpenRegister\Db\AuditTrailMapper; +use OCA\OpenRegister\Exception\ValidationException; +use OCA\OpenRegister\Formats\BsnFormat; +use OCP\IURLGenerator; +use Opis\JsonSchema\ValidationResult; +use Opis\JsonSchema\Validator; +use stdClass; use Symfony\Component\Uid\Uuid; - +use GuzzleHttp\Client; + +/** + * Service class for handling object operations + * + * This service provides methods for CRUD operations on objects, including: + * - Creating, reading, updating and deleting objects + * - Finding objects by ID/UUID + * - Getting audit trails + * - Extending objects with related data + * + * @package OCA\OpenRegister\Service + */ class ObjectService { - - private int $register; - private int $schema; - - private AuditTrailMapper $auditTrailMapper; - - /** - * The constructor sets al needed variables. - * - * @param ObjectEntityMapper $objectEntityMapper The ObjectEntity Mapper - */ - public function __construct( - ObjectEntityMapper $objectEntityMapper, - RegisterMapper $registerMapper, - SchemaMapper $schemaMapper, - AuditTrailMapper $auditTrailMapper - ) - { - $this->objectEntityMapper = $objectEntityMapper; - $this->registerMapper = $registerMapper; - $this->schemaMapper = $schemaMapper; - $this->auditTrailMapper = $auditTrailMapper; - } - - public function find(int|string $id) { - return $this->getObject( - register: $this->registerMapper->find($this->getRegister()), - schema: $this->schemaMapper->find($this->getSchema()), - uuid: $id - ); - } - - public function createFromArray(array $object) { - return $this->saveObject( - register: $this->getRegister(), - schema: $this->getSchema(), - object: $object - ); - } - - public function updateFromArray(string $id, array $object, bool $updatedObject) { - $object['id'] = $id; - - return $this->saveObject( - register: $this->getRegister(), - schema: $this->getSchema(), - object: $object - ); - } - - public function delete(array|\JsonSerializable $object): bool - { - if($object instanceof \JsonSerializable === true) { - $object = $object->jsonSerialize(); - } - - return $this->deleteObject( - register: $this->registerMapper->find($this->getRegister()), - schema: $this->schemaMapper->find($this->getSchema()), - uuid: $object['id'] - ); - } - - public function findAll(?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null): array - { - $objects = $this->getObjects( - register: $this->getRegister(), - schema: $this->getSchema(), - limit: $limit, - offset: $offset, - filters: $filters, - sort: $sort, - search: $search - ); -// $data = array_map([$this, 'getDataFromObject'], $objects); - - return $objects; - } - - public function count(array $filters = [], ?string $search = null): int - { - if($this->getSchema() !== null && $this->getRegister() !== null) { - $filters['register'] = $this->getRegister(); - $filters['schema'] = $this->getSchema(); - } - $count = $this->objectEntityMapper - ->countAll(filters: $filters, search: $search); - - return $count; - } - - public function findMultiple(array $ids): array - { - $result = []; - foreach($ids as $id) { - $result[] = $this->find($id); - } - - return $result; - } - - public function getAggregations(array $filters, ?string $search = null): array - { - $mapper = $this->getMapper(objectType: 'objectEntity'); - - $filters['register'] = $this->getRegister(); - $filters['schema'] = $this->getSchema(); - - if ($mapper instanceof ObjectEntityMapper === true) { - $facets = $this->objectEntityMapper->getFacets($filters, $search); - return $facets; - } - - return []; - } - - private function getDataFromObject(mixed $object) { - - return $object->getObject(); - } - - /** - * Gets all objects of a specific type. - * - * @param string|null $objectType The type of objects to retrieve. - * @param int|null $register - * @param int|null $schema - * @param int|null $limit The maximum number of objects to retrieve. - * @param int|null $offset The offset from which to start retrieving objects. - * @param array $filters - * @return array The retrieved objects. - * @throws \Exception - */ - public function getObjects(?string $objectType = null, ?int $register = null, ?int $schema = null, ?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null): array - { - if($objectType === null && $register !== null && $schema !== null) { - $objectType = 'objectEntity'; - $filters['register'] = $register; - $filters['schema'] = $schema; - } - - // Get the appropriate mapper for the object type - $mapper = $this->getMapper($objectType); - - // Use the mapper to find and return all objects of the specified type - return $mapper->findAll(limit: $limit, offset: $offset, filters: $filters, sort: $sort, search: $search); - } - - /** - * Save an object - * - * @param Register|string $register The register to save the object to. - * @param Schema|string $schema The schema to save the object to. - * @param array $object The data to be saved. - * - * @return ObjectEntity The resulting object. - */ - public function saveObject(int $register, int $schema, array $object): ObjectEntity - { - // Convert register and schema to their respective objects if they are strings - if (is_string($register)) { - $register = $this->registerMapper->find($register); - } - if (is_string($schema)) { - $schema = $this->schemaMapper->find($schema); - } - - if(isset($object['id']) === true) { - // Does the object already exist? - $objectEntity = $this->objectEntityMapper->findByUuid($this->registerMapper->find($register), $this->schemaMapper->find($schema), $object['id']); - } - - if($objectEntity === null){ - $objectEntity = new ObjectEntity(); - $objectEntity->setRegister($register); - $objectEntity->setSchema($schema); - ///return $this->objectEntityMapper->update($objectEntity); - } - // Does the object have an if? - if (isset($object['id']) && !empty($object['id'])) { - // Update existing object - $objectEntity->setUuid($object['id']); - } else { - // Create new object - $objectEntity->setUuid(Uuid::v4()); - $object['id'] = $objectEntity->getUuid(); - } - - $oldObject = clone $objectEntity; - $objectEntity->setObject($object); - - // If the object has no uuid, create a new one - if (empty($objectEntity->getUuid())) { - $objectEntity->setUuid(Uuid::v4()); - } - - if($objectEntity->getId()){ - $objectEntity = $this->objectEntityMapper->update($objectEntity); - $this->auditTrailMapper->createAuditTrail(new: $objectEntity, old: $oldObject); - } - else { - $objectEntity = $this->objectEntityMapper->insert($objectEntity); - $this->auditTrailMapper->createAuditTrail(new: $objectEntity); - } - - return $objectEntity; - } - - - /** - * Get an object - * - * @param Register $register The register to save the object to. - * @param string $uuid The uuid of the object to get - * - * @return ObjectEntity The resulting object. - */ - public function getObject(Register $register, Schema $schema, string $uuid): ObjectEntity - { - - // Lets see if we need to save to an internal source - if ($register->getSource() === 'internal' || $register->getSource() === '') { - return $this->objectEntityMapper->findByUuid($register, $schema, $uuid); - } - - //@todo mongodb support - - // Handle external source here if needed - throw new \Exception('Unsupported source type'); - } - - /** - * Delete an object - * - * @param Register $register The register to delete the object from. - * @param string $uuid The uuid of the object to delete - - * @return ObjectEntity The resulting object. - */ - public function deleteObject(Register $register, Schema $schema, string $uuid): bool - { - // Lets see if we need to save to an internal source - if ($register->getSource() === 'internal' || $register->getSource() === '') { - $object = $this->objectEntityMapper->findByUuid(register: $register, schema: $schema, uuid: $uuid); - $this->objectEntityMapper->delete($object); - - return true; - } - - //@todo mongodb support - - // Handle external source here if needed - throw new \Exception('Unsupported source type'); - } - - /** - * Gets the appropriate mapper based on the object type. - * - * @param string $objectType The type of object to retrieve the mapper for. - * @return mixed The appropriate mapper. - * @throws \InvalidArgumentException If an unknown object type is provided. - * @throws \Exception If OpenRegister service is not available or if register/schema is not configured. - */ - public function getMapper(?string $objectType = null, ?int $register = null, ?int $schema = null) - { - if($register !== null && $schema !== null) { - $this->setSchema($schema); - $this->setRegister($register); - - return $this; - } - - // If the source is internal, return the appropriate mapper based on the object type - switch ($objectType) { - case 'register': - return $this->registerMapper; - case 'schema': - return $this->schemaMapper; - case 'objectEntity': - return $this->objectEntityMapper; - default: - throw new \InvalidArgumentException("Unknown object type: $objectType"); - } - } - - - - /** - * Gets multiple objects based on the object type and ids. - * - * @param string $objectType The type of objects to retrieve. - * @param array $ids The ids of the objects to retrieve. - * @return array The retrieved objects. - * @throws \InvalidArgumentException If an unknown object type is provided. - */ - public function getMultipleObjects(string $objectType, array $ids) - { - // Process the ids - $processedIds = array_map(function($id) { - if (is_object($id) && method_exists($id, 'getId')) { - return $id->getId(); - } elseif (is_array($id) && isset($id['id'])) { - return $id['id']; - } else { - return $id; - } - }, $ids); - - // Clean up the ids if they are URIs - $cleanedIds = array_map(function($id) { - // If the id is a URI, get only the last part of the path - if (filter_var($id, FILTER_VALIDATE_URL)) { - $parts = explode('/', rtrim($id, '/')); - return end($parts); - } - return $id; - }, $processedIds); - - // Get the appropriate mapper for the object type - $mapper = $this->getMapper($objectType); - - // Use the mapper to find and return multiple objects based on the provided cleaned ids - return $mapper->findMultiple($cleanedIds); - } - - /** - * Extends an entity with related objects based on the extend array. - * - * @param mixed $entity The entity to extend - * @param array $extend An array of properties to extend - * @return array The extended entity as an array - * @throws \Exception If a property is not present on the entity - */ - public function extendEntity(array $entity, array $extend): array - { - // Convert the entity to an array if it's not already one - if(is_array($entity)) { - $result = $entity; - } else { - $result = $entity->jsonSerialize(); - } - - // Iterate through each property to be extended - foreach ($extend as $property) { - // Create a singular property name - $singularProperty = rtrim($property, 's'); - - // Check if property or singular property are keys in the array - if (array_key_exists(key: $property, array: $result) === true) { - $value = $result[$property]; - if (empty($value)) { - continue; - } - } elseif (array_key_exists(key: $singularProperty, array: $result)) { - $value = $result[$singularProperty]; - } else { - throw new \Exception("Property '$property' or '$singularProperty' is not present in the entity."); - } - - // Get a mapper for the property - $propertyObject = $property; - try { - $mapper = $this->getMapper(objectType: $property); - $propertyObject = $singularProperty; - } catch (\Exception $e) { - try { - $mapper = $this->getMapper(objectType: $singularProperty); - $propertyObject = $singularProperty; - } catch (\Exception $e) { - // If still no mapper, throw a no mapper available error - throw new \Exception(message: "No mapper available for property '$property'."); - } - } - - // Update the values - if (is_array($value) === true) { - // If the value is an array, get multiple related objects - $result[$property] = $this->getMultipleObjects(objectType: $propertyObject, ids: $value); - } else { - // If the value is not an array, get a single related object - $objectId = is_object(value: $value) ? $value->getId() : $value; - $result[$property] = $mapper->find($objectId); - } - } - - // Return the extended entity as an array - return $result; - } - - /** - * Get the registers extended with schemas for this instance of OpenRegisters - * - * @return array The registers of this OpenRegisters instance extended with schemas - * @throws \Exception - */ - public function getRegisters(): array - { - $registers = $this->registerMapper->findAll(); - - - // Convert entity objects to arrays using jsonSerialize - $registers = array_map(function($object) { - return $object->jsonSerialize(); - }, $registers); - - $extend = ['schemas']; - // Extend the objects if the extend array is not empty - if(empty($extend) === false) { - $registers = array_map(function($object) use ($extend) { - return $this->extendEntity(entity: $object, extend: $extend); - }, $registers); - } - - return $registers; - } - - public function getRegister(): int - { - return $this->register; - } - - public function setRegister(int $register): void - { - $this->register = $register; - } - - public function getSchema(): int - { - return $this->schema; - } - - public function setSchema(int $schema): void - { - $this->schema = $schema; - } - - /** - * Get the audit trail for a specific object - * - * @param int $register The register ID - * @param int $schema The schema ID - * @param string $id The object ID - * @return array The audit trail for the object - */ - public function getAuditTrail(int $register, int $schema, string $id): array - { - $filters = [ - //'register' => $register, - //'schema' => $schema, - 'object' => $id - ]; - - return $this->auditTrailMapper->findAllUuid(idOrUuid: $id); - } + /** @var int The current register ID */ + private int $register; + + /** @var int The current schema ID */ + private int $schema; + + /** @var AuditTrailMapper For tracking object changes */ + private AuditTrailMapper $auditTrailMapper; + + /** + * Constructor for ObjectService + * + * Initializes the service with required mappers for database operations + * + * @param ObjectEntityMapper $objectEntityMapper Mapper for object entities + * @param RegisterMapper $registerMapper Mapper for registers + * @param SchemaMapper $schemaMapper Mapper for schemas + * @param AuditTrailMapper $auditTrailMapper Mapper for audit trails + */ + public function __construct( + ObjectEntityMapper $objectEntityMapper, + RegisterMapper $registerMapper, + SchemaMapper $schemaMapper, + AuditTrailMapper $auditTrailMapper + ) + { + $this->objectEntityMapper = $objectEntityMapper; + $this->registerMapper = $registerMapper; + $this->schemaMapper = $schemaMapper; + $this->auditTrailMapper = $auditTrailMapper; + } + + /** + * Find an object by ID or UUID + * + * @param int|string $id The ID or UUID to search for + * @return ObjectEntity The found object + */ + public function find(int|string $id) { + return $this->getObject( + register: $this->registerMapper->find($this->getRegister()), + schema: $this->schemaMapper->find($this->getSchema()), + uuid: $id + ); + } + + /** + * Create a new object from array data + * + * @param array $object The object data + * @return ObjectEntity The created object + */ + public function createFromArray(array $object) { + return $this->saveObject( + register: $this->getRegister(), + schema: $this->getSchema(), + object: $object + ); + } + + /** + * Update an existing object from array data + * + * @param string $id The object ID to update + * @param array $object The new object data + * @param bool $updatedObject Whether this is an update operation + * @return ObjectEntity The updated object + */ + public function updateFromArray(string $id, array $object, bool $updatedObject) { + // Add ID to object data for update + $object['id'] = $id; + + return $this->saveObject( + register: $this->getRegister(), + schema: $this->getSchema(), + object: $object + ); + } + + /** + * Delete an object + * + * @param array|\JsonSerializable $object The object to delete + * @return bool True if deletion was successful + */ + public function delete(array|\JsonSerializable $object): bool + { + // Convert JsonSerializable objects to array + if($object instanceof \JsonSerializable === true) { + $object = $object->jsonSerialize(); + } + + return $this->deleteObject( + register: $this->registerMapper->find($this->getRegister()), + schema: $this->schemaMapper->find($this->getSchema()), + uuid: $object['id'] + ); + } + + /** + * Find all objects matching given criteria + * + * @param int|null $limit Maximum number of results + * @param int|null $offset Starting offset for pagination + * @param array $filters Filter criteria + * @param array $sort Sorting criteria + * @param string|null $search Search term + * @return array List of matching objects + */ + public function findAll(?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null): array + { + $objects = $this->getObjects( + register: $this->getRegister(), + schema: $this->getSchema(), + limit: $limit, + offset: $offset, + filters: $filters, + sort: $sort, + search: $search + ); + + return $objects; + } + + /** + * Count total objects matching filters + * + * @param array $filters Filter criteria + * @param string|null $search Search term + * @return int Total count + */ + public function count(array $filters = [], ?string $search = null): int + { + // Add register and schema filters if set + if($this->getSchema() !== null && $this->getRegister() !== null) { + $filters['register'] = $this->getRegister(); + $filters['schema'] = $this->getSchema(); + } + + return $this->objectEntityMapper + ->countAll(filters: $filters, search: $search); + } + + /** + * Find multiple objects by their IDs + * + * @param array $ids Array of object IDs to find + * @return array Array of found objects + */ + public function findMultiple(array $ids): array + { + $result = []; + foreach($ids as $id) { + $result[] = $this->find($id); + } + + return $result; + } + + /** + * Get aggregations for objects matching filters + * + * @param array $filters Filter criteria + * @param string|null $search Search term + * @return array Aggregation results + */ + public function getAggregations(array $filters, ?string $search = null): array + { + $mapper = $this->getMapper(objectType: 'objectEntity'); + + $filters['register'] = $this->getRegister(); + $filters['schema'] = $this->getSchema(); + + // Only ObjectEntityMapper supports facets + if ($mapper instanceof ObjectEntityMapper === true) { + $facets = $this->objectEntityMapper->getFacets($filters, $search); + return $facets; + } + + return []; + } + + /** + * Extract object data from an entity + * + * @param mixed $object The object to extract data from + * @return mixed The extracted object data + */ + private function getDataFromObject(mixed $object) { + return $object->getObject(); + } + + /** + * Gets all objects of a specific type. + * + * @param string|null $objectType The type of objects to retrieve. + * @param int|null $register + * @param int|null $schema + * @param int|null $limit The maximum number of objects to retrieve. + * @param int|null $offset The offset from which to start retrieving objects. + * @param array $filters + * @return array The retrieved objects. + * @throws \Exception + */ + public function getObjects(?string $objectType = null, ?int $register = null, ?int $schema = null, ?int $limit = null, ?int $offset = null, array $filters = [], array $sort = [], ?string $search = null): array + { + // Set object type and filters if register and schema are provided + if($objectType === null && $register !== null && $schema !== null) { + $objectType = 'objectEntity'; + $filters['register'] = $register; + $filters['schema'] = $schema; + } + + // Get the appropriate mapper for the object type + $mapper = $this->getMapper($objectType); + + // Use the mapper to find and return all objects of the specified type + return $mapper->findAll(limit: $limit, offset: $offset, filters: $filters, sort: $sort, search: $search); + } + + /** + * Save an object + * + * @param Register|string $register The register to save the object to. + * @param Schema|string $schema The schema to save the object to. + * @param array $object The data to be saved. + * + * @return ObjectEntity The resulting object. + */ + public function saveObject(int $register, int $schema, array $object): ObjectEntity + { + // Convert register and schema to their respective objects if they are strings + if (is_string($register)) { + $register = $this->registerMapper->find($register); + } + if (is_string($schema)) { + $schema = $this->schemaMapper->find($schema); + } + + // Check if object already exists + if(isset($object['id']) === true) { + $objectEntity = $this->objectEntityMapper->findByUuid( + $this->registerMapper->find($register), + $this->schemaMapper->find($schema), + $object['id'] + ); + } + + // Create new entity if none exists + if($objectEntity === null){ + $objectEntity = new ObjectEntity(); + $objectEntity->setRegister($register); + $objectEntity->setSchema($schema); + } + + // Handle UUID assignment + if (isset($object['id']) && !empty($object['id'])) { + $objectEntity->setUuid($object['id']); + } else { + $objectEntity->setUuid(Uuid::v4()); + $object['id'] = $objectEntity->getUuid(); + } + + // Store old version for audit trail + $oldObject = clone $objectEntity; + $objectEntity->setObject($object); + + // Ensure UUID exists + if (empty($objectEntity->getUuid())) { + $objectEntity->setUuid(Uuid::v4()); + } + + // Update or insert based on whether ID exists + if($objectEntity->getId()){ + $objectEntity = $this->objectEntityMapper->update($objectEntity); + $this->auditTrailMapper->createAuditTrail(new: $objectEntity, old: $oldObject); + } + else { + $objectEntity = $this->objectEntityMapper->insert($objectEntity); + $this->auditTrailMapper->createAuditTrail(new: $objectEntity); + } + + return $objectEntity; + } + + /** + * Get an object + * + * @param Register $register The register to get the object from + * @param Schema $schema The schema of the object + * @param string $uuid The UUID of the object to get + * + * @return ObjectEntity The resulting object + * @throws \Exception If source type is unsupported + */ + public function getObject(Register $register, Schema $schema, string $uuid): ObjectEntity + { + // Handle internal source + if ($register->getSource() === 'internal' || $register->getSource() === '') { + return $this->objectEntityMapper->findByUuid($register, $schema, $uuid); + } + + //@todo mongodb support + + throw new \Exception('Unsupported source type'); + } + + /** + * Delete an object + * + * @param Register $register The register to delete from + * @param Schema $schema The schema of the object + * @param string $uuid The UUID of the object to delete + * + * @return bool True if deletion was successful + * @throws \Exception If source type is unsupported + */ + public function deleteObject(Register $register, Schema $schema, string $uuid): bool + { + // Handle internal source + if ($register->getSource() === 'internal' || $register->getSource() === '') { + $object = $this->objectEntityMapper->findByUuid(register: $register, schema: $schema, uuid: $uuid); + $this->objectEntityMapper->delete($object); + return true; + } + + //@todo mongodb support + + throw new \Exception('Unsupported source type'); + } + + /** + * Gets the appropriate mapper based on the object type. + * + * @param string|null $objectType The type of object to retrieve the mapper for + * @param int|null $register Optional register ID + * @param int|null $schema Optional schema ID + * @return mixed The appropriate mapper + * @throws \InvalidArgumentException If unknown object type + */ + public function getMapper(?string $objectType = null, ?int $register = null, ?int $schema = null) + { + // Return self if register and schema provided + if($register !== null && $schema !== null) { + $this->setSchema($schema); + $this->setRegister($register); + return $this; + } + + // Return appropriate mapper based on object type + switch ($objectType) { + case 'register': + return $this->registerMapper; + case 'schema': + return $this->schemaMapper; + case 'objectEntity': + return $this->objectEntityMapper; + default: + throw new \InvalidArgumentException("Unknown object type: $objectType"); + } + } + + /** + * Gets multiple objects based on the object type and ids. + * + * @param string $objectType The type of objects to retrieve + * @param array $ids The ids of the objects to retrieve + * @return array The retrieved objects + * @throws \InvalidArgumentException If unknown object type + */ + public function getMultipleObjects(string $objectType, array $ids) + { + // Process the ids to handle different formats + $processedIds = array_map(function($id) { + if (is_object($id) && method_exists($id, 'getId')) { + return $id->getId(); + } elseif (is_array($id) && isset($id['id'])) { + return $id['id']; + } else { + return $id; + } + }, $ids); + + // Clean up URIs to get just the ID portion + $cleanedIds = array_map(function($id) { + if (filter_var($id, FILTER_VALIDATE_URL)) { + $parts = explode('/', rtrim($id, '/')); + return end($parts); + } + return $id; + }, $processedIds); + + // Get mapper and find objects + $mapper = $this->getMapper($objectType); + return $mapper->findMultiple($cleanedIds); + } + + /** + * Extends an entity with related objects based on the extend array. + * + * @param mixed $entity The entity to extend + * @param array $extend Properties to extend with related data + * @return array The extended entity as an array + * @throws \Exception If property not found or no mapper available + */ + public function extendEntity(array $entity, array $extend): array + { + // Convert entity to array if needed + if(is_array($entity)) { + $result = $entity; + } else { + $result = $entity->jsonSerialize(); + } + + // Process each property to extend + foreach ($extend as $property) { + $singularProperty = rtrim($property, 's'); + + // Check if property exists + if (array_key_exists(key: $property, array: $result) === true) { + $value = $result[$property]; + if (empty($value)) { + continue; + } + } elseif (array_key_exists(key: $singularProperty, array: $result)) { + $value = $result[$singularProperty]; + } else { + throw new \Exception("Property '$property' or '$singularProperty' is not present in the entity."); + } + + // Try to get mapper for property + $propertyObject = $property; + try { + $mapper = $this->getMapper(objectType: $property); + $propertyObject = $singularProperty; + } catch (\Exception $e) { + try { + $mapper = $this->getMapper(objectType: $singularProperty); + $propertyObject = $singularProperty; + } catch (\Exception $e) { + throw new \Exception("No mapper available for property '$property'."); + } + } + + // Extend with related objects + if (is_array($value) === true) { + $result[$property] = $this->getMultipleObjects(objectType: $propertyObject, ids: $value); + } else { + $objectId = is_object(value: $value) ? $value->getId() : $value; + $result[$property] = $mapper->find($objectId); + } + } + + return $result; + } + + /** + * Get all registers extended with their schemas + * + * @return array The registers with schema data + * @throws \Exception If extension fails + */ + public function getRegisters(): array + { + // Get all registers + $registers = $this->registerMapper->findAll(); + + // Convert to arrays + $registers = array_map(function($object) { + return $object->jsonSerialize(); + }, $registers); + + // Extend with schemas + $extend = ['schemas']; + if(empty($extend) === false) { + $registers = array_map(function($object) use ($extend) { + return $this->extendEntity(entity: $object, extend: $extend); + }, $registers); + } + + return $registers; + } + + /** + * Get current register ID + * + * @return int The register ID + */ + public function getRegister(): int + { + return $this->register; + } + + /** + * Set current register ID + * + * @param int $register The register ID to set + */ + public function setRegister(int $register): void + { + $this->register = $register; + } + + /** + * Get current schema ID + * + * @return int The schema ID + */ + public function getSchema(): int + { + return $this->schema; + } + + /** + * Set current schema ID + * + * @param int $schema The schema ID to set + */ + public function setSchema(int $schema): void + { + $this->schema = $schema; + } + + /** + * Get the audit trail for a specific object + * + * @todo: register and schema parameters are not needed anymore + * + * @param int $register The register ID + * @param int $schema The schema ID + * @param string $id The object ID + * @return array The audit trail entries + */ + public function getAuditTrail(int $register, int $schema, string $id): array + { + $filters = [ + 'object' => $id + ]; + + return $this->auditTrailMapper->findAllUuid(idOrUuid: $id); + } } diff --git a/lib/Service/SearchService.php b/lib/Service/SearchService.php index a5cb602..9ef5d6c 100644 --- a/lib/Service/SearchService.php +++ b/lib/Service/SearchService.php @@ -7,42 +7,62 @@ use OCP\IURLGenerator; use Symfony\Component\Uid\Uuid; +/** + * Service class for handling search functionality + * + * This class provides methods for searching objects across multiple sources, + * merging results, handling facets/aggregations, and processing search parameters + */ class SearchService { + /** @var Client HTTP client for making requests */ public $client; + /** @var array Default base object configuration */ public const BASE_OBJECT = [ 'database' => 'objects', 'collection' => 'json', ]; + /** + * Constructor + * + * @param IURLGenerator $urlGenerator URL generator service + */ public function __construct( private readonly IURLGenerator $urlGenerator, ) { $this->client = new Client(); } + /** + * Merges facet counts from two aggregations + * + * @param array $existingAggregation Original aggregation array + * @param array $newAggregation New aggregation array to merge + * @return array Merged facet counts + */ public function mergeFacets(array $existingAggregation, array $newAggregation): array { $results = []; $existingAggregationMapped = []; $newAggregationMapped = []; + // Map existing aggregation counts by ID foreach ($existingAggregation as $value) { $existingAggregationMapped[$value['_id']] = $value['count']; } - + // Merge new aggregation counts, adding to existing where present foreach ($newAggregation as $value) { if (isset ($existingAggregationMapped[$value['_id']]) === true) { $newAggregationMapped[$value['_id']] = $existingAggregationMapped[$value['_id']] + $value['count']; } else { $newAggregationMapped[$value['_id']] = $value['count']; } - } - + // Format results array with merged counts foreach (array_merge(array_diff($existingAggregationMapped, $newAggregationMapped), array_diff($newAggregationMapped, $existingAggregationMapped)) as $key => $value) { $results[] = ['_id' => $key, 'count' => $value]; } @@ -50,13 +70,20 @@ public function mergeFacets(array $existingAggregation, array $newAggregation): return $results; } + /** + * Merges multiple aggregation arrays + * + * @param array|null $existingAggregations Original aggregations + * @param array|null $newAggregations New aggregations to merge + * @return array Merged aggregations + */ private function mergeAggregations(?array $existingAggregations, ?array $newAggregations): array { if ($newAggregations === null) { return []; } - + // Merge each aggregation key foreach ($newAggregations as $key => $aggregation) { if (isset($existingAggregations[$key]) === false) { $existingAggregations[$key] = $aggregation; @@ -67,18 +94,30 @@ private function mergeAggregations(?array $existingAggregations, ?array $newAggr return $existingAggregations; } + /** + * Comparison function for sorting results by score + * + * @param array $a First result array + * @param array $b Second result array + * @return int Comparison result (-1, 0, 1) + */ public function sortResultArray(array $a, array $b): int { return $a['_score'] <=> $b['_score']; } - - /** - * - */ + /** + * Main search function that queries multiple sources and merges results + * + * @param array $parameters Search parameters and filters + * @param array $elasticConfig Elasticsearch configuration + * @param array $dbConfig Database configuration + * @param array $catalogi List of catalogs to search + * @return array Combined search results with facets and pagination info + */ public function search(array $parameters, array $elasticConfig, array $dbConfig, array $catalogi = []): array { - + // Initialize results arrays $localResults['results'] = []; $localResults['facets'] = []; @@ -86,14 +125,14 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $limit = isset($parameters['.limit']) === true ? $parameters['.limit'] : 30; $page = isset($parameters['.page']) === true ? $parameters['.page'] : 1; + // Query elastic if configured if ($elasticConfig['location'] !== '') { $localResults = $this->elasticService->searchObject(filters: $parameters, config: $elasticConfig, totalResults: $totalResults,); } $directory = $this->directoryService->listDirectory(limit: 1000); -// $directory = $this->objectService->findObjects(filters: ['_schema' => 'directory'], config: $dbConfig); - + // Return early if no directory entries if (count($directory) === 0) { $pages = (int) ceil($totalResults / $limit); return [ @@ -112,7 +151,7 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $searchEndpoints = []; - + // Build search requests for each endpoint $promises = []; foreach ($directory as $instance) { if ( @@ -128,15 +167,16 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, unset($parameters['.catalogi']); + // Create async requests for each endpoint foreach ($searchEndpoints as $searchEndpoint => $catalogi) { $parameters['_catalogi'] = $catalogi; - - $promises[] = $this->client->getAsync($searchEndpoint, ['query' => $parameters]); } + // Wait for all requests to complete $responses = Utils::settle($promises)->wait(); + // Process responses and merge results foreach ($responses as $response) { if ($response['state'] === 'fulfilled') { $responseData = json_decode( @@ -157,6 +197,7 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $pages = (int) ceil($totalResults / $limit); + // Return combined results with pagination info return [ 'results' => $results, 'facets' => $aggregations, @@ -379,7 +420,6 @@ public function parseQueryString (string $queryString = ''): array ); } - return $vars; } diff --git a/lib/Service/UploadService.php b/lib/Service/UploadService.php new file mode 100644 index 0000000..f14ae18 --- /dev/null +++ b/lib/Service/UploadService.php @@ -0,0 +1,174 @@ +client = new Client([]); + } + + /** + * Gets the uploaded json from the request data. And returns it as a PHP array. + * Will first try to find an uploaded 'file', then if an 'url' is present in the body and lastly if a 'json' dump has been posted. + * + * @param array $data All request params. + * + * @return array|JSONResponse A PHP array with the uploaded json data or a JSONResponse in case of an error. + * @throws GuzzleException + */ + public function getUploadedJson(array $data): array|JSONResponse + { + 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 + return $this->getJSONfromFile(); + } + + if (empty($data['url']) === false) { + return $this->getJSONfromURL($data['url']); + } + + $phpArray = $data['json']; + if (is_string($phpArray) === true) { + $phpArray = json_decode($phpArray, associative: true); + } + + if ($phpArray === null || $phpArray === false) { + return new JSONResponse(data: ['error' => 'Failed to decode JSON input'], statusCode: 400); + } + + return $phpArray; + } + + private function getJSONfromFile(): array|JSONResponse + { + // @todo + + return new JSONResponse(data: ['error' => 'Not yet implemented'], statusCode: 501); + } + + /** + * Uses Guzzle to call the given URL and returns response as PHP array. + * + * @param string $url The URL to call. + * + * @return array|JSONResponse The response from the call converted to PHP array or JSONResponse in case of an error. + * @throws GuzzleException + */ + private function getJSONfromURL(string $url): array|JSONResponse + { + try { + $response = $this->client->request('GET', $url); + } catch (GuzzleHttp\Exception\BadResponseException $e) { + return new JSONResponse(data: ['error' => 'Failed to do a GET api-call on url: '.$url.' '.$e->getMessage()], statusCode: 400); + } + + $responseBody = $response->getBody()->getContents(); + + // Use Content-Type header to determine the format + $contentType = $response->getHeaderLine('Content-Type'); + switch ($contentType) { + case 'application/json': + $phpArray = json_decode(json: $responseBody, associative: true); + break; + case 'application/yaml': + $phpArray = Yaml::parse(input: $responseBody); + break; + default: + // If Content-Type is not specified or not recognized, try to parse as JSON first, then YAML + $phpArray = json_decode(json: $responseBody, associative: true); + if ($phpArray === null) { + $phpArray = Yaml::parse(input: $responseBody); + } + break; + } + + if ($phpArray === null || $phpArray === false) { + return new JSONResponse(data: ['error' => 'Failed to parse response body as JSON or YAML'], statusCode: 400); + } + + return $phpArray; + } + + /** + * Handles adding schemas to a register during upload. + * + * @param Register $register The register to add schemas to. + * @param array $phpArray The PHP array containing the uploaded json data. + * + * @return Register The updated register. + * @throws \OCP\DB\Exception + */ + public function handleRegisterSchemas(Register $register, array $phpArray): Register + { + // Process and save schemas + foreach ($phpArray['components']['schemas'] as $schemaName => $schemaData) { + // Check if a schema with this title already exists + $schema = $this->registerMapper->hasSchemaWithTitle(registerId: $register->getId(), schemaTitle: $schemaName); + if ($schema === false) { + // Check if a schema with this title already exists for this register + try { + $schemas = $this->schemaMapper->findAll(filters: ['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 $register; + } + +} 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/package-lock.json b/package-lock.json index 84deeda..3d59b6b 100755 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "AGPL-3.0-or-later", "dependencies": { + "@codemirror/lang-json": "^6.0.1", "@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@nextcloud/axios": "^2.5.0", @@ -32,6 +33,7 @@ "uuid": "^10.0.0", "vue": "^2.7.14", "vue-apexcharts": "^1.6.2", + "vue-codemirror6": "^1.3.4", "vue-loader": "^15.11.1 <16.0.0", "vue-loading-overlay": "^3.4.3", "vue-material-design-icons": "^5.3.0", @@ -1800,6 +1802,91 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.2.tgz", + "integrity": "sha512-wJGylKtMFR/Ds6Gh01+OovXE/pncPiKZNNBKuC39pKnH+XK5d9+WsNqcrdxPjFPFTigRBqse0rfxw9UxrfyhPg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz", + "integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz", + "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz", + "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.7.tgz", + "integrity": "sha512-6+iLsXvITWKHYlkgHPCs/qiX4dNzn8N78YfhOFvPtPYCkuXqZq10rAfsUMhOq7O/1VjJqdXRflyExlfVcu/9VQ==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "node_modules/@codemirror/view": { + "version": "6.34.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.2.tgz", + "integrity": "sha512-d6n0WFvL970A9Z+l9N2dO+Hk9ev4hDYQzIx+B9tCyBP0W5wPEszi1rhuyFesNSkLZzXbQE5FPH7F/z/TMJfoPA==", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@csstools/css-parser-algorithms": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", @@ -3056,6 +3143,37 @@ "dev": true, "peer": true }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", + "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@linusborg/vue-simple-portal": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/@linusborg/vue-simple-portal/-/vue-simple-portal-0.1.5.tgz", @@ -7235,6 +7353,20 @@ "node": ">= 0.12.0" } }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/collapse-white-space": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", @@ -7724,6 +7856,11 @@ "node": ">=8" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -19362,6 +19499,11 @@ "webpack": "^5.0.0" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "node_modules/style-search": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", @@ -21300,6 +21442,46 @@ "apexcharts": "^3.26.0" } }, + "node_modules/vue-codemirror6": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/vue-codemirror6/-/vue-codemirror6-1.3.4.tgz", + "integrity": "sha512-Gmu2t4Exz3pQdtAsb7wINREx2nswNW+FV+q8S1wmsmeC3OLio5RkybRLsErP1b8+suqsVD/7t0Cx/XmBpQnHJA==", + "dependencies": { + "codemirror": "^6.0.1", + "vue-demi": "latest" + }, + "engines": { + "yarn": ">=1.22.19" + }, + "peerDependencies": { + "vue": "^2.7.14 || ^3.4" + } + }, + "node_modules/vue-codemirror6/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/vue-color": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/vue-color/-/vue-color-2.8.1.tgz", @@ -21533,6 +21715,11 @@ "vue": "^2.5.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -23402,6 +23589,85 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@codemirror/autocomplete": { + "version": "6.18.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.2.tgz", + "integrity": "sha512-wJGylKtMFR/Ds6Gh01+OovXE/pncPiKZNNBKuC39pKnH+XK5d9+WsNqcrdxPjFPFTigRBqse0rfxw9UxrfyhPg==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/commands": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz", + "integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "@codemirror/language": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz", + "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/lint": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz", + "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/search": { + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.7.tgz", + "integrity": "sha512-6+iLsXvITWKHYlkgHPCs/qiX4dNzn8N78YfhOFvPtPYCkuXqZq10rAfsUMhOq7O/1VjJqdXRflyExlfVcu/9VQ==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "@codemirror/view": { + "version": "6.34.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.2.tgz", + "integrity": "sha512-d6n0WFvL970A9Z+l9N2dO+Hk9ev4hDYQzIx+B9tCyBP0W5wPEszi1rhuyFesNSkLZzXbQE5FPH7F/z/TMJfoPA==", + "requires": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "@csstools/css-parser-algorithms": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", @@ -24306,6 +24572,37 @@ "dev": true, "peer": true }, + "@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" + }, + "@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", + "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", + "requires": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, "@linusborg/vue-simple-portal": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/@linusborg/vue-simple-portal/-/vue-simple-portal-0.1.5.tgz", @@ -27514,6 +27811,20 @@ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true }, + "codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "collapse-white-space": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", @@ -27915,6 +28226,11 @@ } } }, + "crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -36400,6 +36716,11 @@ "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", "requires": {} }, + "style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "style-search": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", @@ -37788,6 +38109,23 @@ "integrity": "sha512-9HS3scJwWgKjmkcWIf+ndNDR0WytUJD8Ju0V2ZYcjYtlTLwJAf2SKUlBZaQTkDmwje/zMgulvZRi+MXmi+WkKw==", "requires": {} }, + "vue-codemirror6": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/vue-codemirror6/-/vue-codemirror6-1.3.4.tgz", + "integrity": "sha512-Gmu2t4Exz3pQdtAsb7wINREx2nswNW+FV+q8S1wmsmeC3OLio5RkybRLsErP1b8+suqsVD/7t0Cx/XmBpQnHJA==", + "requires": { + "codemirror": "^6.0.1", + "vue-demi": "latest" + }, + "dependencies": { + "vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "requires": {} + } + } + }, "vue-color": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/vue-color/-/vue-color-2.8.1.tgz", @@ -37957,6 +38295,11 @@ "date-format-parse": "^0.2.7" } }, + "w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index 29e5c3a..5fae2c2 100755 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "extends @nextcloud/browserslist-config" ], "dependencies": { + "@codemirror/lang-json": "^6.0.1", "@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@nextcloud/axios": "^2.5.0", @@ -43,6 +44,7 @@ "uuid": "^10.0.0", "vue": "^2.7.14", "vue-apexcharts": "^1.6.2", + "vue-codemirror6": "^1.3.4", "vue-loader": "^15.11.1 <16.0.0", "vue-loading-overlay": "^3.4.3", "vue-material-design-icons": "^5.3.0", diff --git a/src/modals/Modals.vue b/src/modals/Modals.vue index a2d5cf3..3bb513b 100755 --- a/src/modals/Modals.vue +++ b/src/modals/Modals.vue @@ -6,9 +6,11 @@ import { navigationStore } from '../store/store.js'
+ + @@ -22,8 +24,10 @@ import { navigationStore } from '../store/store.js' @@ -59,6 +65,7 @@ import { NcTextArea, NcLoadingIcon, NcNoteCard, + NcCheckboxRadioSwitch, } from '@nextcloud/vue' import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue' @@ -74,6 +81,7 @@ export default { NcButton, NcLoadingIcon, NcNoteCard, + NcCheckboxRadioSwitch, // Icons ContentSaveOutline, Cancel, @@ -89,6 +97,7 @@ export default { created: '', updated: '', }, + createAnother: false, success: false, loading: false, error: false, @@ -137,9 +146,33 @@ export default { schemaStore.saveSchema({ ...this.schemaItem, }).then(({ response }) => { - this.success = response.ok - this.error = false - response.ok && setTimeout(this.closeModal, 2000) + if (this.createAnother) { + schemaStore.setSchemaItem(null) + setTimeout(() => { + this.initializeSchemaItem() + this.schemaItem = { + title: '', + version: '0.0.1', + description: '', + summary: '', + created: '', + updated: '', + } + this.loading = false + }, 500) + setTimeout(() => { + this.success = null + }, 2000) + this.success = response.ok + this.hasUpdated = false + this.error = false + + } else { + this.success = response.ok + this.error = false + response.ok && setTimeout(this.closeModal, 2000) + } + }).catch((error) => { this.success = false this.error = error.message || 'An error occurred while saving the schema' diff --git a/src/modals/schema/UploadSchema.vue b/src/modals/schema/UploadSchema.vue new file mode 100644 index 0000000..75a076a --- /dev/null +++ b/src/modals/schema/UploadSchema.vue @@ -0,0 +1,182 @@ + + + + + + + diff --git a/src/services/getTheme.js b/src/services/getTheme.js new file mode 100644 index 0000000..bfe63e7 --- /dev/null +++ b/src/services/getTheme.js @@ -0,0 +1,25 @@ +/** + * Determines the current theme of the document. + * + * This function checks the `data-theme-light` and `data-theme-default` attributes + * on the document body to determine the theme. If `data-theme-light` is present, + * it returns 'light'. If `data-theme-default` is present, it checks the user's + * preferred color scheme using the `window.matchMedia` API and returns 'light' or 'dark' + * accordingly. If neither attribute is present, it defaults to 'dark'. + * + * @return { 'light' | 'dark' } The current theme, either 'light' or 'dark'. + */ +export const getTheme = () => { + if (document.body.hasAttribute('data-theme-light')) { + return 'light' + } + + if (document.body.hasAttribute('data-theme-dark')) { + return 'dark' + } + + if (document.body.hasAttribute('data-theme-default')) { + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark' + } + return 'light' +} diff --git a/src/store/modules/register.js b/src/store/modules/register.js index 841dd72..18765b6 100644 --- a/src/store/modules/register.js +++ b/src/store/modules/register.js @@ -133,5 +133,48 @@ export const useRegisterStore = defineStore('register', { throw new Error(`Failed to save register: ${error.message}`) } }, + // Create or save a register from store + async uploadRegister(register) { + if (!register) { + throw new Error('No register item to upload') + } + + console.log('Uploading register...') + + const isNewRegister = !this.registerItem + const endpoint = isNewRegister + ? '/index.php/apps/openregister/api/registers/upload' + : `/index.php/apps/openregister/api/registers/upload/${this.registerItem.id}` + const method = isNewRegister ? 'POST' : 'PUT' + + const response = await fetch( + endpoint, + { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(register), + }, + ) + + 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 Register(responseData) + + this.setRegisterItem(data) + this.refreshRegisterList() + + return { response, data } + + }, }, }) diff --git a/src/store/modules/schema.js b/src/store/modules/schema.js index bef57f9..f40a273 100644 --- a/src/store/modules/schema.js +++ b/src/store/modules/schema.js @@ -4,7 +4,7 @@ import { Schema } from '../../entities/index.js' export const useSchemaStore = defineStore('schema', { state: () => ({ - schemaItem: false, + schemaItem: null, schemaPropertyKey: null, // holds a UUID of the property to edit schemaList: [], }), @@ -37,14 +37,14 @@ export const useSchemaStore = defineStore('schema', { return { response, data } }, // Function to get a single schema - async getSchema(id) { + async getSchema(id, options = { setItem: false }) { const endpoint = `/index.php/apps/openregister/api/schemas/${id}` try { const response = await fetch(endpoint, { method: 'GET', }) const data = await response.json() - this.setSchemaItem(data) + options.setItem && this.setSchemaItem(data) return data } catch (err) { console.error(err) @@ -129,6 +129,103 @@ export const useSchemaStore = defineStore('schema', { return { response, data } }, + // Create or save a schema from store + async uploadSchema(schema) { + 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( + endpoint, + { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(schema), + }, + ) + + 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 } + + }, + async downloadSchema(schema) { + if (!schema) { + throw new Error('No schema item to download') + } + if (!(schema instanceof Schema)) { + throw new Error('Invalid schema item to download') + } + if (!schema?.id) { + throw new Error('No schema item ID to download') + } + + console.log('Downloading schema...') + + const response = await fetch( + `/index.php/apps/openregister/api/schemas/${schema.id}/download`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + + if (!response.ok) { + console.error(response) + throw new Error(response.statusText) + } + + const data = await response.json() + + // Convert JSON to a prettified string + const jsonString = JSON.stringify(data, null, 2) + + // Create a Blob from the JSON string + const blob = new Blob([jsonString], { type: 'application/json' }) + + // Create a URL for the Blob + const url = URL.createObjectURL(blob) + + // Create a temporary anchor element + const a = document.createElement('a') + a.href = url + a.download = `${schema.title}.json` + + // Temporarily add the anchor to the DOM and trigger the download + document.body.appendChild(a) + a.click() + + // Clean up + document.body.removeChild(a) + URL.revokeObjectURL(url) + + return { response } + }, // schema properties setSchemaPropertyKey(schemaPropertyKey) { this.schemaPropertyKey = schemaPropertyKey diff --git a/src/views/register/RegisterDetails.vue b/src/views/register/RegisterDetails.vue index 3151c84..7121938 100644 --- a/src/views/register/RegisterDetails.vue +++ b/src/views/register/RegisterDetails.vue @@ -21,6 +21,12 @@ import { registerStore, navigationStore, schemaStore } from '../../store/store.j Edit + + + Upload + Refresh + + + Upload Register + Add Property + + + Upload + + + + Download + Refresh + + + Upload + Add Property + + + Download +