From abeac239dae6874d9c8429800094e131e8162e3b Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 26 Mar 2026 07:58:59 +0100 Subject: [PATCH 1/6] Fixed openapi doc errors --- src/inc/apiv2/common/OpenAPISchemaUtils.php | 69 +++-- src/inc/apiv2/common/openAPISchema.routes.php | 260 +++++++++++------- src/inc/apiv2/helper/CurrentUserHelperAPI.php | 5 +- .../apiv2/helper/GetAccessGroupsHelperAPI.php | 4 +- src/inc/apiv2/helper/GetBestTasksAgent.php | 4 +- .../apiv2/helper/GetCracksOfTaskHelper.php | 4 +- .../helper/GetUserPermissionHelperAPI.php | 4 +- src/inc/apiv2/helper/ImportFileHelperAPI.php | 8 +- src/inc/apiv2/model/LogEntryAPI.php | 5 + 9 files changed, 220 insertions(+), 143 deletions(-) diff --git a/src/inc/apiv2/common/OpenAPISchemaUtils.php b/src/inc/apiv2/common/OpenAPISchemaUtils.php index ae6ac950e..322a7e774 100644 --- a/src/inc/apiv2/common/OpenAPISchemaUtils.php +++ b/src/inc/apiv2/common/OpenAPISchemaUtils.php @@ -126,16 +126,13 @@ static function makeLinks($uri): array { } //TODO relationship array is unnecessarily indexed in the swagger UI - static function makeRelationships($class, $uri): array { + static function makeRelationships($relationshipsNames, $uri): array { $properties = []; - $relationshipsNames = array_merge(array_keys($class->getToOneRelationships()), array_keys($class->getToManyRelationships())); sort($relationshipsNames); foreach ($relationshipsNames as $relationshipName) { $self = $uri . "/relationships/" . $relationshipName; $related = $uri . "/" . $relationshipName; - $properties[] = [ - "properties" => [ - $relationshipName => [ + $properties[$relationshipName] = [ "type" => "object", "properties" => [ "links" => [ @@ -152,9 +149,6 @@ static function makeRelationships($class, $uri): array { ] ] ] - ] - - ] ]; } return $properties; @@ -166,19 +160,18 @@ static function getTUSHeader(): array { Must always be set to `1.0.0` in compliant servers.", "schema" => [ "type" => "string", - "enum" => "enum: ['1.0.0']" + "enum" => ['1.0.0'] ] ]; } //TODO expandables array is unnecessarily indexed in the swagger UI - static function makeExpandables($class, $container): array { + static function makeExpandables($expandables, $container): array { $properties = []; - $expandables = array_merge($class->getToOneRelationships(), $class->getToManyRelationships()); foreach ($expandables as $expand => $expandVal) { $expandClass = $expandVal["relationType"]; $expandApiClass = new ($container->get('classMapper')->get($expandClass))($container); - $properties[] = [ + $properties[$expand] = [ "properties" => [ "id" => [ "type" => "integer" @@ -197,20 +190,44 @@ static function makeExpandables($class, $container): array { return $properties; } - static function mapToProperties($map): array { - $properties = array_map(function ($value) { - return [ - "type" => "string", - "default" => $value, - ]; - }, $map); - return [ - "type" => "array", - "items" => [ - "type" => "object", - "properties" => $properties - ] - ]; + static function mapToProperties(mixed $value): array { + if (is_null($value)) { + return ["nullable" => true, "type" => "string"]; + } elseif (is_bool($value)) { + return ["type" => "boolean", "example" => $value]; + } elseif (is_int($value)) { + return ["type" => "integer", "example" => $value]; + } elseif (is_float($value)) { + return ["type" => "number", "example" => $value]; + } elseif (is_string($value)) { + return ["type" => "string", "example" => $value]; + } elseif (is_array($value)) { + if (empty($value)) { + return ["type" => "array"]; + } + if (array_is_list($value)) { + /* Merge properties from all items to capture the most complete schema */ + $mergedProperties = []; + foreach ($value as $item) { + $itemSchema = self::mapToProperties($item); + if (isset($itemSchema['properties'])) { + $mergedProperties = array_merge($mergedProperties, $itemSchema['properties']); + } + } + $itemSchema = self::mapToProperties($value[0]); + if (!empty($mergedProperties)) { + $itemSchema['properties'] = $mergedProperties; + } + return ["type" => "array", "items" => $itemSchema]; + } else { + $properties = []; + foreach ($value as $key => $val) { + $properties[$key] = self::mapToProperties($val); + } + return ["type" => "object", "properties" => $properties]; + } + } + return ["type" => "string"]; } /** diff --git a/src/inc/apiv2/common/openAPISchema.routes.php b/src/inc/apiv2/common/openAPISchema.routes.php index 0ad869c81..3285bf258 100644 --- a/src/inc/apiv2/common/openAPISchema.routes.php +++ b/src/inc/apiv2/common/openAPISchema.routes.php @@ -120,7 +120,9 @@ $apiClassName = $explodedCallable[0]; $apiMethod = $explodedCallable[1]; $class = new $apiClassName($app->getContainer()); + + $path = preg_replace('/\{([^:}]+):(.+)\}/', '{$1}', $path); if (!($class instanceof AbstractModelAPI)) { $name_parts = explode('\\', $class::class); $name = end($name_parts); @@ -129,12 +131,15 @@ $paths[$path][$method]["description"] = OpenAPISchemaUtils::parsePhpDoc($reflectionApiMethod->getDocComment()); $parameters = $class->getCreateValidFeatures(); $properties = OpenAPISchemaUtils::makeProperties($parameters); - $components[$name] = - [ - "type" => "object", - "properties" => $properties, - ]; - if ($method == "post") { + $amountProperties = count($properties); + if ($amountProperties > 0) { + $components[$name] = + [ + "type" => "object", + "properties" => $properties, + ]; + } + if ($method == "post" && $amountProperties > 0) { $reflectionMethodFormFields = new ReflectionMethod($class::class, "getFormFields"); $bodyDescription = OpenAPISchemaUtils::parsePhpDoc($reflectionMethodFormFields->getDocComment()); $paths[$path][$method]["requestBody"] = [ @@ -187,7 +192,8 @@ }; /* Quick to find out if single parameter object is used */ - $singleObject = ((strstr($path, '/{id:')) !== false); + error_log($path); + $singleObject = ((strstr($path, '/{id}')) !== false); $name_parts = explode('\\', $class->getDBAClass()); $name = end($name_parts); $uri = $class->getBaseUri(); @@ -199,6 +205,11 @@ $isToOne = array_key_exists($relation, $class::getToOneRelationships()); assert(!($isToMany && $isToOne), "An relationship cant be a to one and to many at the same time."); } else { + $availableMethods = $class->getAvailableMethods(); + $method_to_check = strtoupper($method); + if ($method_to_check != "GET" && !in_array($method_to_check, $availableMethods)) { + continue; + } $isToMany = $isToOne = false; $relation = null; } @@ -230,72 +241,91 @@ ] ]; - $relationships = ["relationships" => [ - "type" => "object", - "properties" => OpenAPISchemaUtils::makeRelationships($class, $uri) - ] - ]; - $included = ["included" => [ - "type" => "array", - "items" => [ + $relationshipsNames = array_merge(array_keys($class->getToOneRelationships()), array_keys($class->getToManyRelationships())); + if (count($relationshipsNames) > 0) { + $relationships = ["relationships" => [ "type" => "object", - "properties" => OpenAPISchemaUtils::makeExpandables($class, $app->getContainer()) - ], - ] + "properties" => OpenAPISchemaUtils::makeRelationships($relationshipsNames, $uri) + ] + ]; + } + $expandables = array_merge($class->getToOneRelationships(), $class->getToManyRelationships()); + if (count($expandables) > 0) { + $included = ["included" => [ + "type" => "array", + "items" => [ + "type" => "object", + "properties" => OpenAPISchemaUtils::makeExpandables($expandables, $app->getContainer()) + ], + ] ]; + } $properties_get_single = array_merge($properties_return_post_patch, $relationships, $included); $json_api_header = OpenAPISchemaUtils::makeJsonApiHeader(); $links = OpenAPISchemaUtils::makeLinks($uri); $properties_return_post_patch = array_merge($json_api_header, $properties_return_post_patch); - $properties_create = OpenAPISchemaUtils::buildPatchPost(OpenAPISchemaUtils::makeProperties($class->getAllPostParameters($class->getCreateValidFeatures())), $name); + $postProperties = OpenAPISchemaUtils::makeProperties($class->getAllPostParameters($class->getCreateValidFeatures())); $properties_get = array_merge($json_api_header, $links, $properties_get_single, $included); - $properties_patch = OpenAPISchemaUtils::buildPatchPost(OpenAPISchemaUtils::makeProperties($class->getPatchValidFeatures(), true), $name); - $properties_patch_post_relation = OpenAPISchemaUtils::buildPostPatchRelation($relation, ($isToMany && !$isToOne)); - $responseGetRelation = $properties_patch_post_relation; + $patch_properties = OpenAPISchemaUtils::makeProperties($class->getAllPostParameters($class->getCreateValidFeatures())); - $components[$name . "Create"] = - [ - "type" => "object", - "properties" => $properties_create, - ]; + if (count($postProperties) > 0) { + $properties_create = OpenAPISchemaUtils::buildPatchPost(OpenAPISchemaUtils::makeProperties($class->getAllPostParameters($class->getCreateValidFeatures())), $name); + $components[$name . "Create"] = + [ + "type" => "object", + "properties" => $properties_create, + ]; + } - $components[$name . "Patch"] = - [ - "type" => "object", - "properties" => $properties_patch, - ]; + if (count($patch_properties) > 0) { + $properties_patch = OpenAPISchemaUtils::buildPatchPost($patch_properties, $name); + $components[$name . "Patch"] = + [ + "type" => "object", + "properties" => $properties_patch, + ]; + } + if (count($properties_get) > 0) { $components[$name . "Response"] = [ "type" => "object", "properties" => $properties_get, ]; + } - $components[$name . "Relation" . ucfirst($relation)] = - [ - "type" => "object", - "properties" => $properties_patch_post_relation, - ]; - - $components[$name . "Relation" . ucfirst($relation) . "GetResponse"] = - [ - "type" => "object", - "properties" => $responseGetRelation - ]; + if ($relation) { + $properties_patch_post_relation = OpenAPISchemaUtils::buildPostPatchRelation($relation, ($isToMany && !$isToOne)); + $responseGetRelation = $properties_patch_post_relation; + $components[$name . "Relation" . ucfirst($relation)] = + [ + "type" => "object", + "properties" => $properties_patch_post_relation, + ]; + $components[$name . "Relation" . ucfirst($relation) . "GetResponse"] = + [ + "type" => "object", + "properties" => $responseGetRelation + ]; + } - $components[$name . "SingleResponse"] = - [ - "type" => "object", - "properties" => $properties_get_single - ]; + if (count($properties_get_single) > 0) { + $components[$name . "SingleResponse"] = + [ + "type" => "object", + "properties" => $properties_get_single + ]; + } - $components[$name . "PostPatchResponse"] = - [ - "type" => "object", - "properties" => $properties_return_post_patch - ]; + if (count($properties_return_post_patch) > 0) { + $components[$name . "PostPatchResponse"] = + [ + "type" => "object", + "properties" => $properties_return_post_patch + ]; + } $components[$name . "ListResponse"] = [ @@ -355,9 +385,7 @@ ], "security" => [ [ - "bearerAuth" => [ - $required_scopes - ] + "bearerAuth" => $required_scopes ] ] ]; @@ -481,26 +509,18 @@ } else { /* Empty JSON object required */ - $paths[$path][$method]["requestBody"] = [ - "required" => true, - "content" => [ - "application/json" => [], - ] - ]; + // $paths[$path][$method]["requestBody"] = [ + // "required" => false, + // "content" => [ + // "application/json" => [], + // ] + // ]; } } elseif ($method == 'post') { $paths[$path][$method]["responses"]["204"] = [ "description" => "successfully created", ]; - - /* Empty JSON object required */ - $paths[$path][$method]["requestBody"] = [ - "required" => true, - "content" => [ - "application/json" => [], - ] - ]; } else { throw new HttpErrorException("Method '$method' not implemented"); @@ -574,17 +594,21 @@ } elseif ($method == 'patch') { - // TODO add patch many here + $paths[$path][$method]["responses"]["204"] = [ + "description" => "successfully patched", + ]; } elseif ($method == 'delete') { - // TODO add delete many here + $paths[$path][$method]["responses"]["200"] = [ + "description" => "successfully deleted", + ]; } else { throw new HttpErrorException("Method '$method' not implemented"); } } - if ($singleObject && $method == 'get') { + if ($singleObject) { $parameters = [ [ "name" => "id", @@ -614,51 +638,55 @@ $parameters = [ [ "name" => "page[after]", - "in" => "path", + "in" => "query", "schema" => [ "type" => "integer", "format" => "int32" ], "example" => 0, + "required" => false, "description" => "Pointer to paginate to retrieve the data after the value provided" ], [ "name" => "page[before]", - "in" => "path", + "in" => "query", "schema" => [ "type" => "integer", "format" => "int32" ], "example" => 0, + "required" => false, "description" => "Pointer to paginate to retrieve the data before the value provided" ], [ "name" => "page[size]", - "in" => "path", + "in" => "query", "schema" => [ "type" => "integer", "format" => "int32" ], + "required" => false, "example" => 100, "description" => "Amout of data to retrieve inside a single page" ], [ "name" => "filter", - "in" => "path", - "style" => "deepobject", + "in" => "query", + "style" => "deepObject", "explode" => true, "schema" => [ - "type" => "object", + "type" => "string", ], "description" => "Filters results using a query", "example" => '"filter[hashlistId__gt]": 200' ], [ "name" => "include", - "in" => "path", + "in" => "query", "schema" => [ "type" => "string" ], + "required" => false, "description" => "Items to include, comma seperated. Possible options: " . $expandables ] ]; @@ -782,7 +810,7 @@ [ "name" => "Upload-Metadata", "in" => "header", - "required" => "true", + "required" => true, "schema" => [ "type" => "string", "pattern" => '^([a-zA-Z0-9]+ [A-Za-z0-9+/=]+)(,[a-zA-Z0-9]+ [A-Za-z0-9+/=]+)*$' @@ -816,43 +844,72 @@ ] ]; - $paths["/api/v2/helper/importFile/{id:[0-9]{14}-[0-9a-f]{32}}"]["patch"]["parameters"] = [ + $paths["/api/v2/helper/importFile/{id}"]["head"]["parameters"] = [ + [ + "name" => "id", + "in" => "path", + "required" => true, + "schema" => [ + "type"=> "string", + "pattern"=> "^[0-9]{14}-[0-9a-f]{32}$" + ] + ] + ]; + $paths["/api/v2/helper/importFile/{id}"]["delete"]["parameters"] = [ + [ + "name" => "id", + "in" => "path", + "required" => true, + "schema" => [ + "type"=> "string", + "pattern"=> "^[0-9]{14}-[0-9a-f]{32}$" + ] + ] + ]; + $paths["/api/v2/helper/importFile/{id}"]["patch"]["parameters"] = [ [ "name" => "Upload-Offset", "in" => "header", - "required" => "true", + "required" => true, "schema" => [ "type" => "integer", ], - "example" => "512", + "example" => 512, "description" => " The Upload-Offset header’s value MUST be equal to the current offset of the resource" ], + [ + "name" => "id", + "in" => "path", + "required" => true, + "schema" => [ + "type"=> "string", + "pattern"=> "^[0-9]{14}-[0-9a-f]{32}$" + ] + ], [ "name" => "Content-Type", "in" => "header", - "required" => "true", + "required" => true, "schema" => [ "type" => "string", "enum" => ["application/offset+octet-stream"] ], ], ]; - $paths["/api/v2/helper/importFile/{id:[0-9]{14}-[0-9a-f]{32}}"]["patch"]["requestBody"] = [ - [ - "required" => "true", - "description" => "The binary data to push to the file", - "content" => [ - "application/offset+octet-stream" => [ - "schema" => [ - "type" => "string", - "format" => "binary" - ] + $paths["/api/v2/helper/importFile/{id}"]["patch"]["requestBody"] = [ + "required" => true, + "description" => "The binary data to push to the file", + "content" => [ + "application/offset+octet-stream" => [ + "schema" => [ + "type" => "string", + "format" => "binary" ] ] ] ]; - $paths["/api/v2/helper/importFile/{id:[0-9]{14}-[0-9a-f]{32}}"]["head"]["responses"]["200"] = [ + $paths["/api/v2/helper/importFile/{id}"]["head"]["responses"]["200"] = [ "description" => "successful request", "headers" => [ "Tus-Resumable" => OpenAPISchemaUtils::getTUSHeader(), @@ -882,6 +939,9 @@ ] ] ]; + $paths["/api/v2/helper/importFile/{id}"]["delete"]["responses"]["204"] = [ + "description" => "successful operation" + ]; $paths["/api/v2/helper/importFile"]["post"]["responses"]["201"] = [ "description" => "successful operation", @@ -896,12 +956,14 @@ ], "content" => [ "application/pdf" => [ - "type" => "string", - "format" => "binary" + "schema" => [ + "type" => "string", + "format" => "obinary" + ] ] ] ]; - $paths["/api/v2/helper/importFile/{id:[0-9]{14}-[0-9a-f]{32}}"]["patch"]["responses"]["204"] = [ + $paths["/api/v2/helper/importFile/{id}"]["patch"]["responses"]["204"] = [ "description" => "Chunk accepted", "headers" => [ "Tus-Resumable" => OpenAPISchemaUtils::getTUSHeader(), diff --git a/src/inc/apiv2/helper/CurrentUserHelperAPI.php b/src/inc/apiv2/helper/CurrentUserHelperAPI.php index a51b2c6ff..6da458fe2 100644 --- a/src/inc/apiv2/helper/CurrentUserHelperAPI.php +++ b/src/inc/apiv2/helper/CurrentUserHelperAPI.php @@ -85,10 +85,7 @@ static public function register($app): void { $app->patch($baseUri, "Hashtopolis\\inc\\apiv2\\helper\\CurrentUserHelperAPI:actionPatch"); } - /** - * getCurrentUser is different because it returns via another function - */ public static function getResponse(): array|string|null { - return null; + return "User"; } } diff --git a/src/inc/apiv2/helper/GetAccessGroupsHelperAPI.php b/src/inc/apiv2/helper/GetAccessGroupsHelperAPI.php index 1a265adba..9d3231bc7 100644 --- a/src/inc/apiv2/helper/GetAccessGroupsHelperAPI.php +++ b/src/inc/apiv2/helper/GetAccessGroupsHelperAPI.php @@ -76,7 +76,7 @@ static public function register($app): void { /** * getAccessGroups is different because it returns via another function */ - public static function getResponse(): array|string|null { - return null; + public static function getResponse(): string { + return "AccessGroup"; } } diff --git a/src/inc/apiv2/helper/GetBestTasksAgent.php b/src/inc/apiv2/helper/GetBestTasksAgent.php index 35b343125..be9945d2a 100644 --- a/src/inc/apiv2/helper/GetBestTasksAgent.php +++ b/src/inc/apiv2/helper/GetBestTasksAgent.php @@ -25,8 +25,8 @@ public function getRequiredPermissions(string $method): array { return [Agent::PERM_READ, Task::PERM_READ]; } - public static function getResponse(): null { - return null; + public static function getResponse(): string { + return "Task"; } diff --git a/src/inc/apiv2/helper/GetCracksOfTaskHelper.php b/src/inc/apiv2/helper/GetCracksOfTaskHelper.php index fe97ddf5e..8245f1825 100644 --- a/src/inc/apiv2/helper/GetCracksOfTaskHelper.php +++ b/src/inc/apiv2/helper/GetCracksOfTaskHelper.php @@ -34,8 +34,8 @@ public function getRequiredPermissions(string $method): array { return [Hashlist::PERM_READ, Hash::PERM_READ, Task::PERM_READ]; } - public static function getResponse(): null { - return null; + public static function getResponse(): string { + return "Hash"; } diff --git a/src/inc/apiv2/helper/GetUserPermissionHelperAPI.php b/src/inc/apiv2/helper/GetUserPermissionHelperAPI.php index 7fc1b380f..e568114da 100644 --- a/src/inc/apiv2/helper/GetUserPermissionHelperAPI.php +++ b/src/inc/apiv2/helper/GetUserPermissionHelperAPI.php @@ -70,8 +70,8 @@ static public function register($app): void { /** * getAccessGroups is different because it returns via another function */ - public static function getResponse(): array|string|null { - return null; + public static function getResponse(): string { + return "RightGroup"; } } diff --git a/src/inc/apiv2/helper/ImportFileHelperAPI.php b/src/inc/apiv2/helper/ImportFileHelperAPI.php index 680c955a1..6651768ea 100644 --- a/src/inc/apiv2/helper/ImportFileHelperAPI.php +++ b/src/inc/apiv2/helper/ImportFileHelperAPI.php @@ -130,11 +130,8 @@ function processHead(Request $request, Response $response, array $args): Respons } } - /** - * getfile is different because it returns actual binary data. - */ - public static function getResponse(): null { - return null; + public static function getResponse(): array { + return ["file" => "abc.txt", "size" => 123]; } /** File import API @@ -423,7 +420,6 @@ function processGet(Request $request, Response $response, array $args): Response return self::getMetaResponse($importFiles, $request, $response); } - static public function register(App $app): void { $me = get_called_class(); $baseUri = $me::getBaseUri(); diff --git a/src/inc/apiv2/model/LogEntryAPI.php b/src/inc/apiv2/model/LogEntryAPI.php index b7a840c9e..479c263c9 100644 --- a/src/inc/apiv2/model/LogEntryAPI.php +++ b/src/inc/apiv2/model/LogEntryAPI.php @@ -29,4 +29,9 @@ protected function createObject(array $data): int { protected function deleteObject(object $object): void { throw new HttpError("Logentries cannot be deleted via API"); } + + public static function getAvailableMethods(): array { + return ['GET']; + } + } From 08589362023cb19bc4bef174ca9071da28370192 Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 26 Mar 2026 08:00:31 +0100 Subject: [PATCH 2/6] Added openapi validation to pipeline --- .github/workflows/openapi-lint.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/openapi-lint.yml diff --git a/.github/workflows/openapi-lint.yml b/.github/workflows/openapi-lint.yml new file mode 100644 index 000000000..eb2e36bb1 --- /dev/null +++ b/.github/workflows/openapi-lint.yml @@ -0,0 +1,28 @@ +name: OpenAPI Lint + +on: + push: + branches: + - master + - dev + pull_request: + branches: + - master + - dev + +jobs: + openapi-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Start Hashtopolis server + uses: ./.github/actions/start-hashtopolis + with: + db_system: mysql + - name: Install Spectral + run: npm install -g @stoplight/spectral-cli + - name: Download JSON:API ruleset + run: curl -fsSL https://raw.githubusercontent.com/jmlue42/spectral-jsonapi-ruleset/main/.spectral.yml -o .spectral.yml + - name: Lint OpenAPI schema + run: spectral lint http://localhost:8080/api/v2/openapi.json --ruleset .spectral.yml -D From 3f52123628d17207acb993a7710a3559a7ddb10d Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 26 Mar 2026 08:14:43 +0100 Subject: [PATCH 3/6] Fixed phpstan errors --- src/inc/apiv2/common/openAPISchema.routes.php | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/inc/apiv2/common/openAPISchema.routes.php b/src/inc/apiv2/common/openAPISchema.routes.php index 3285bf258..cde6b1131 100644 --- a/src/inc/apiv2/common/openAPISchema.routes.php +++ b/src/inc/apiv2/common/openAPISchema.routes.php @@ -242,6 +242,7 @@ ]; $relationshipsNames = array_merge(array_keys($class->getToOneRelationships()), array_keys($class->getToManyRelationships())); + $relationships = []; if (count($relationshipsNames) > 0) { $relationships = ["relationships" => [ "type" => "object", @@ -250,6 +251,7 @@ ]; } $expandables = array_merge($class->getToOneRelationships(), $class->getToManyRelationships()); + $included = []; if (count($expandables) > 0) { $included = ["included" => [ "type" => "array", @@ -288,13 +290,11 @@ ]; } - if (count($properties_get) > 0) { $components[$name . "Response"] = [ "type" => "object", "properties" => $properties_get, ]; - } if ($relation) { $properties_patch_post_relation = OpenAPISchemaUtils::buildPostPatchRelation($relation, ($isToMany && !$isToOne)); @@ -311,21 +311,17 @@ ]; } - if (count($properties_get_single) > 0) { - $components[$name . "SingleResponse"] = - [ - "type" => "object", - "properties" => $properties_get_single - ]; - } + $components[$name . "SingleResponse"] = + [ + "type" => "object", + "properties" => $properties_get_single + ]; - if (count($properties_return_post_patch) > 0) { - $components[$name . "PostPatchResponse"] = - [ - "type" => "object", - "properties" => $properties_return_post_patch - ]; - } + $components[$name . "PostPatchResponse"] = + [ + "type" => "object", + "properties" => $properties_return_post_patch + ]; $components[$name . "ListResponse"] = [ From ce24b633c70622100add8b511f2e0496adc91a2c Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 26 Mar 2026 13:11:30 +0100 Subject: [PATCH 4/6] Fixed copilot suggestions --- src/inc/apiv2/common/openAPISchema.routes.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/inc/apiv2/common/openAPISchema.routes.php b/src/inc/apiv2/common/openAPISchema.routes.php index cde6b1131..ab8c12b86 100644 --- a/src/inc/apiv2/common/openAPISchema.routes.php +++ b/src/inc/apiv2/common/openAPISchema.routes.php @@ -167,10 +167,6 @@ else if (is_string($request_response)) { $ref = "#/components/schemas/" . $request_response . "SingleResponse"; } - else if ($name == "ImportFileHelperAPI") { - //ImportFileHelperAPI is hardcoded, because its different than other helpers. - continue; - } if (isset($ref)) { $paths[$path][$method]["responses"]["200"] = [ "description" => "successful operation", @@ -192,7 +188,6 @@ }; /* Quick to find out if single parameter object is used */ - error_log($path); $singleObject = ((strstr($path, '/{id}')) !== false); $name_parts = explode('\\', $class->getDBAClass()); $name = end($name_parts); @@ -250,14 +245,14 @@ ] ]; } - $expandables = array_merge($class->getToOneRelationships(), $class->getToManyRelationships()); + $expandables_array = array_merge($class->getToOneRelationships(), $class->getToManyRelationships()); $included = []; - if (count($expandables) > 0) { + if (count($expandables_array) > 0) { $included = ["included" => [ "type" => "array", "items" => [ "type" => "object", - "properties" => OpenAPISchemaUtils::makeExpandables($expandables, $app->getContainer()) + "properties" => OpenAPISchemaUtils::makeExpandables($expandables_array, $app->getContainer()) ], ] ]; @@ -270,7 +265,7 @@ $properties_return_post_patch = array_merge($json_api_header, $properties_return_post_patch); $postProperties = OpenAPISchemaUtils::makeProperties($class->getAllPostParameters($class->getCreateValidFeatures())); $properties_get = array_merge($json_api_header, $links, $properties_get_single, $included); - $patch_properties = OpenAPISchemaUtils::makeProperties($class->getAllPostParameters($class->getCreateValidFeatures())); + $patch_properties = OpenAPISchemaUtils::makeProperties($class->getPatchValidFeatures($class->getCreateValidFeatures())); if (count($postProperties) > 0) { $properties_create = OpenAPISchemaUtils::buildPatchPost(OpenAPISchemaUtils::makeProperties($class->getAllPostParameters($class->getCreateValidFeatures())), $name); @@ -954,7 +949,7 @@ "application/pdf" => [ "schema" => [ "type" => "string", - "format" => "obinary" + "format" => "binary" ] ] ] From f4aae64baf2ff2db1d253b8bcb22fd1452790feb Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 26 Mar 2026 13:28:31 +0100 Subject: [PATCH 5/6] Fixed typo --- src/inc/apiv2/common/openAPISchema.routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inc/apiv2/common/openAPISchema.routes.php b/src/inc/apiv2/common/openAPISchema.routes.php index ab8c12b86..816f7064a 100644 --- a/src/inc/apiv2/common/openAPISchema.routes.php +++ b/src/inc/apiv2/common/openAPISchema.routes.php @@ -265,7 +265,7 @@ $properties_return_post_patch = array_merge($json_api_header, $properties_return_post_patch); $postProperties = OpenAPISchemaUtils::makeProperties($class->getAllPostParameters($class->getCreateValidFeatures())); $properties_get = array_merge($json_api_header, $links, $properties_get_single, $included); - $patch_properties = OpenAPISchemaUtils::makeProperties($class->getPatchValidFeatures($class->getCreateValidFeatures())); + $patch_properties = OpenAPISchemaUtils::makeProperties($class->getPatchValidFeatures()); if (count($postProperties) > 0) { $properties_create = OpenAPISchemaUtils::buildPatchPost(OpenAPISchemaUtils::makeProperties($class->getAllPostParameters($class->getCreateValidFeatures())), $name); From 31f3ce47e2659455adda7ea005d344c1c60026b5 Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 26 Mar 2026 14:18:40 +0100 Subject: [PATCH 6/6] Added aggregated data to get request openapi docs --- src/inc/apiv2/common/AbstractBaseAPI.php | 12 +++++++ src/inc/apiv2/common/openAPISchema.routes.php | 31 +++++++++++++++++++ src/inc/apiv2/model/TaskAPI.php | 12 +++++++ 3 files changed, 55 insertions(+) diff --git a/src/inc/apiv2/common/AbstractBaseAPI.php b/src/inc/apiv2/common/AbstractBaseAPI.php index 52baa3d13..8d294f2a9 100644 --- a/src/inc/apiv2/common/AbstractBaseAPI.php +++ b/src/inc/apiv2/common/AbstractBaseAPI.php @@ -186,6 +186,18 @@ protected function getUpdateHandlers($id, $current_user): array { public function aggregateData(object $object, array &$includedData = [], ?array $aggregateFieldsets = null): array { return []; } + + /** + * Return supported aggregate fieldsets/options for this endpoint. + * + * Format: + * [ + * 'resourceKey' => ['option1', 'option2'] + * ] + */ + public function getAggregateFieldsets(): array { + return []; + } /** * Take all the dba features and converts them to a list. diff --git a/src/inc/apiv2/common/openAPISchema.routes.php b/src/inc/apiv2/common/openAPISchema.routes.php index 816f7064a..dc38f880f 100644 --- a/src/inc/apiv2/common/openAPISchema.routes.php +++ b/src/inc/apiv2/common/openAPISchema.routes.php @@ -681,6 +681,37 @@ "description" => "Items to include, comma seperated. Possible options: " . $expandables ] ]; + + $aggregateFieldsets = $class->getAggregateFieldsets(); + if (!empty($aggregateFieldsets)) { + $aggregateExamples = []; + $aggregateDescriptionParts = []; + foreach ($aggregateFieldsets as $fieldset => $options) { + if (empty($options)) { + continue; + } + $aggregateExamples["aggregate[" . $fieldset . "]"] = implode(",", $options); + $aggregateDescriptionParts[] = $fieldset . ": " . implode(", ", $options); + } + + if (!empty($aggregateExamples)) { + $parameters[] = [ + "name" => "aggregate", + "in" => "query", + "style" => "deepObject", + "explode" => true, + "schema" => [ + "type" => "object", + "additionalProperties" => [ + "type" => "string" + ] + ], + "required" => false, + "description" => "Aggregated fields to include by type (comma separated values). Possible options: " . implode(" | ", $aggregateDescriptionParts), + "example" => $aggregateExamples + ]; + } + } } else { $parameters = []; diff --git a/src/inc/apiv2/model/TaskAPI.php b/src/inc/apiv2/model/TaskAPI.php index 6827b9141..309fa5ef1 100644 --- a/src/inc/apiv2/model/TaskAPI.php +++ b/src/inc/apiv2/model/TaskAPI.php @@ -136,6 +136,18 @@ public function getFormFields(): array { "files" => ['type' => 'array', 'subtype' => 'int'], ]; } + + public function getAggregateFieldsets(): array { + return [ + 'task' => [ + 'assignedAgents', + 'dispatched', + 'searched', + 'isActive', + 'taskExtraDetails', + ] + ]; + } /** * @throws HttpError