From 307154130be801d71a1501c29cc2e09ce98b2b46 Mon Sep 17 00:00:00 2001 From: jessevz Date: Thu, 19 Dec 2024 11:05:23 +0100 Subject: [PATCH 1/4] FEAT updated openAPI schema to latest API updates --- doc/changelog.md | 1 + src/inc/apiv2/common/openAPISchema.routes.php | 316 +++++++++++++++--- 2 files changed, 279 insertions(+), 38 deletions(-) diff --git a/doc/changelog.md b/doc/changelog.md index 182ebba60..54b5d5382 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -4,6 +4,7 @@ ## Enhancements - Use utf8mb4 as default encoding in order to support the full unicode range +- Updated OpenAPI docs to latest API updates ## Bugfixes diff --git a/src/inc/apiv2/common/openAPISchema.routes.php b/src/inc/apiv2/common/openAPISchema.routes.php index b70fcefe7..0f6918224 100644 --- a/src/inc/apiv2/common/openAPISchema.routes.php +++ b/src/inc/apiv2/common/openAPISchema.routes.php @@ -47,9 +47,135 @@ function typeLookup($feature): array { return $result; }; -function makeProperties($features): array { + +// "jsonapi": { +// "version": "1.1", +// "ext": [ +// "https://jsonapi.org/profiles/ethanresnick/cursor-pagination" +// ] +// }, +function makeJsonApiHeader(): array { + return ["jsonapi" => [ + "type" => "object", + "properties" => [ + "version" => [ + "type" => "string", + "default" => "1.1" + ], + "ext" => [ + "type" => "string", + "default" => "https://jsonapi.org/profiles/ethanresnick/cursor-pagination" + ] + ] + ]]; +} + +// "links": { +// "self": "/api/v2/ui/hashlists?page[size]=10000", +// "first": "/api/v2/ui/hashlists?page[size]=10000&page[after]=0", +// "last": "/api/v2/ui/hashlists?page[size]=10000&page[before]=345", +// "next": null, +// "prev": "/api/v2/ui/hashlists?page[size]=10000&page[before]=114" +// }, +function makeLinks($uri): array { + $self = $uri . "?page[size]=25"; + return ["links" => [ + "type" => "object", + "properties" => [ + "self" => [ + "type" => "string", + "default" => $self + ], + "first" => [ + "type" => "string", + "default" => $self . "&page[after]=0" + ], + "last" => [ + "type" => "string", + "default" => $self . "&page[before]=500" + ], + "next" => [ + "type" => "string", + "default" => $self . "&page[after]=25" + ], + "previous" => [ + "type" => "string", + "default" => $self . "&page[before]=25" + ] + ] + ]]; +} + +//TODO relationship array is unnecessarily indexed in the swagger UI +function makeRelationships($class, $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; + array_push($properties, + [ + "properties" => [ + $relationshipName => [ + "type" => "object", + "properties" => [ + "links" => [ + "type" => "object", + "properties" => [ + "self" => [ + "type" => "string", + "default" => $self + ], + "related" => [ + "type" => "string", + "default" => $related + ] + ] + ] + ] + ] + + ] + ]); + } + return $properties; +} + +//TODO expandables array is unnecessarily indexed in the swagger UI +function makeExpandables($class, $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); + array_push($properties, + [ + "properties" => [ + "id" => [ + "type" => "integer" + ], + "type" => [ + "type" => "string", + "default" => $expand + ], + "attributes" => [ + "type" => "object", + "properties" => makeProperties($expandApiClass->getAliasedFeatures()) + ] + ] + ] + ); + }; + return $properties; +} + +function makeProperties($features, $skipPK=false): array { $propertyVal = []; foreach ($features as $feature) { + if ($skipPK && $feature['pk']) { + continue; + } $ret = typeLookup($feature); $propertyVal[$feature['alias']]["type"] = $ret["type"]; if ($ret["type_format"] !== null) { @@ -62,6 +188,71 @@ function makeProperties($features): array { return $propertyVal; }; +function buildPatchPost($properties, $id=null): array { + $result = ["data" => [ + "type" => "object", + "properties" => [ + "type" => [ + "type" => "string" + ], + "attributes" => [ + "type" => "object", + "properties" => $properties + ] + ] + ] + ]; + + if ($id) { + $result["data"]["properties"]["id"] = [ + "type" => "integer", + ]; + } + return $result; +} + +function makeDescription($isRelation, $method, $singleObject): string { + $description = ""; + switch ($method) { + case "get": + if ($isRelation) { + if($singleObject) { + $description = "GET request for for a to-one relationship link. Returns the resource record of the object that is part of the specified relation."; + } else { + $description = "GET request for a to-many relationship link. Returns a list of resource records of objects that are part of the specified relation."; + } + } else { + if ($singleObject) { + $description = "GET request to retrieve a single object."; + } else { + $description = "GET many request to retrieve multiple objects."; + } + } + case "post": + if ($isRelation) { + if ($singleObject) { + "POST request to create a to-one relationship link."; + } else { + "POST request to create a to-many relationship link."; + } + } else { + $description = "POST request to create a new object. The request must contain the resource record as data with the attributes of the new object." + . "To add relationships, a relationships object can be added with the resource records of the relations that are part of this object."; + } + case "patch": + if ($isRelation) { + if ($singleObject) { + "PATCH request to update a to one relationship."; + } else { + "PATCH request to update a to-many relationship link."; + } + } else { + $description = "PATCH request to update attributes of a single object." ; + } + } + return $description; +} + $app->group("/api/v2/openapi.json", function (RouteCollectorProxy $group) use ($app) { /* Allow CORS preflight requests */ $group->options('', function (Request $request, Response $response): Response { @@ -80,17 +271,17 @@ function makeProperties($features): array { "type" => "string", "example" => "hashlist", ], - "startsAt" => [ + "page[after]" => [ "type" => "integer", "example" => 0 ], - "maxResults" => [ + "page[before]" => [ "type" => "integer", - "example" => 100 + "example" => 0 ], - "total" => [ + "page[size]" => [ "type" => "integer", - "example" => 200 + "example" => 100 ] ] ]; @@ -178,39 +369,63 @@ function makeProperties($features): array { /* Quick to find out if single parameter object is used */ $singleObject = ((strstr($path, '/{id:')) !== false); $name = substr($class->getDBAClass(), 4); + $uri = $class->getBaseUri(); + $isRelation = (strstr($path , "{relation:")) !== false; + + $expandables = implode(",", $class->getExpandables()); /** * Create component objects */ if (array_key_exists($name, $components) == false) { - $properties_get = [ - "_id" => [ + $properties_return_post_patch = [ + "id" => [ "type" => "integer", ], - "_self" => [ + "type" => [ "type" => "string", + "default" => $name ], - "_expandables" => [ - "type" => "string", - "default" => $class->getExpandables(), + "data" => [ + "type" => "object", + "properties" => makeProperties($class->getAliasedFeatures(), true) ] ]; - $properties_create = makeProperties($class->getCreateValidFeatures()); - $properties_get = array_merge($properties_get, makeProperties($class->getAliasedFeatures())); - $properties_patch = makeProperties($class->getPatchValidFeatures()); - - $components[$name . "Create"] = - [ + $relationships = ["relationships" =>[ "type" => "object", - "properties" => $properties_create, + "properties" => makeRelationships($class, $uri) + ] ]; + $included = ["included" => [ + "type" => "array", + "items" => [ + "type" => "object", + "properties" => makeExpandables($class, $app->getContainer()) + ], + ] + ]; + + $properties_get_single = array_merge($properties_return_post_patch, $relationships, $included); + + $json_api_header = makeJsonApiHeader(); + $links = makeLinks($uri); + $properties_return_post_patch = array_merge($json_api_header, $properties_return_post_patch); + $properties_create = buildPatchPost(makeProperties($class->getCreateValidFeatures(), true)); + $properties_get = array_merge($json_api_header, $links, $properties_get_single, $included); + $properties_patch = buildPatchPost(makeProperties($class->getPatchValidFeatures(), true)); + + $components[$name . "Create"] = + [ + "type" => "object", + "properties" => $properties_create, + ]; $components[$name . "Patch"] = - [ - "type" => "object", - "properties" => $properties_patch, - ]; + [ + "type" => "object", + "properties" => $properties_patch, + ]; $components[$name . "Response"] = [ @@ -218,6 +433,18 @@ function makeProperties($features): array { "properties" => $properties_get, ]; + $components[$name . "SingleResponse"] = + [ + "type" => "object", + "properties" => $properties_get_single + ]; + + $components[$name . "PostPatchResponse"] = + [ + "type" => "object", + "properties" => $properties_return_post_patch + ]; + $components[$name . "ListResponse"] = [ "allOf" => [ @@ -283,6 +510,8 @@ function makeProperties($features): array { ] ]; + $paths[$path][$method]["description"] = makeDescription($isRelation, $method, $singleObject); + if ($singleObject) { /* Single objects could not exists */ $paths[$path][$method]["responses"]["404"] = @@ -328,7 +557,7 @@ function makeProperties($features): array { "content" => [ "application/json" => [ "schema" => [ - '$ref' => "#/components/schemas/" . $name . "Response" + '$ref' => "#/components/schemas/" . $name . "PostPatchResponse" ] ] ] @@ -403,7 +632,7 @@ function makeProperties($features): array { "content" => [ "application/json" => [ "schema" => [ - '$ref' => "#/components/schemas/" . $name . "Response" + '$ref' => "#/components/schemas/" . $name . "PostPatchResponse" ] ] ] @@ -423,9 +652,8 @@ function makeProperties($features): array { throw new HttpErrorException("Method '$method' not implemented"); } } - - if ($singleObject) { + if ($singleObject && $method == 'get') { $paths[$path][$method]["responses"]["200"] = [ "description" => "successful operation", "content" => [ @@ -451,52 +679,65 @@ function makeProperties($features): array { if ($method == 'get') { array_push($parameters, [ - "name" => "expand", + "name" => "include", "in" => "query", "schema" => [ "type" => "string" ], - "description" => "Items to expand" + "description" => "Items to include. Comma seperated" ]); }; } else { if ($method == 'get') { $parameters = [ [ - "name" => "startsAt", + "name" => "page[after]", + "in" => "query", + "schema" => [ + "type" => "integer", + "format" => "int32" + ], + "example" => 0, + "description" => "Pointer to paginate to retrieve the data after the value provided" + ], + [ + "name" => "page[before]", "in" => "query", "schema" => [ "type" => "integer", "format" => "int32" ], "example" => 0, - "description" => "The starting index of the values" + "description" => "Pointer to paginate to retrieve the data before the value provided" ], [ - "name" => "maxResults", + "name" => "page[size]", "in" => "query", "schema" => [ "type" => "integer", "format" => "int32" ], "example" => 100, - "description" => "The maximum number of issues to return per page." + "description" => "Amout of data to retrieve inside a single page" ], [ "name" => "filter", "in" => "query", + "style" => "deepobject", + "explode" => true, "schema" => [ - "type" => "string" + "type" => "object", ], - "description" => "Filters results using a query." + "description" => "Filters results using a query", + "example" => '"filter[hashlistId__gt]": 200' ], [ - "name" => "expand", + "name" => "include", "in" => "query", "schema" => [ "type" => "string" ], - "description" => "Items to expand" + "description" => "Items to include, comma seperated. Possible options: " . $expandables ] ]; } else { @@ -506,7 +747,6 @@ function makeProperties($features): array { $paths[$path][$method]["parameters"] = $parameters; }; - /** * Build static entries */ From 65b558e962b21cfdeb4d3e351b2c794bc813d0b9 Mon Sep 17 00:00:00 2001 From: jessevz Date: Mon, 6 Jan 2025 14:50:51 +0100 Subject: [PATCH 2/4] Fixed bug where openAPI scheme would show formfields in API responses --- src/inc/apiv2/common/AbstractBaseAPI.class.php | 6 +++++- src/inc/apiv2/common/AbstractModelAPI.class.php | 11 ++++++++++- src/inc/apiv2/common/openAPISchema.routes.php | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/inc/apiv2/common/AbstractBaseAPI.class.php b/src/inc/apiv2/common/AbstractBaseAPI.class.php index 87d0ccb5c..7c355b438 100644 --- a/src/inc/apiv2/common/AbstractBaseAPI.class.php +++ b/src/inc/apiv2/common/AbstractBaseAPI.class.php @@ -112,7 +112,7 @@ protected function getFeatures(): array $features = []; foreach($this->getFormFields() as $key => $feature) { /* Innitate default values */ - $features[$key] = $feature + ['null' => False, 'protected' => False, 'private' => False, 'choices' => "unset", 'pk' => False]; + $features[$key] = $feature + ['null' => False, 'protected' => False, 'private' => False, 'choices' => "unset", 'pk' => False, 'read_only' => True]; if (!array_key_exists('alias', $feature)) { $features[$key]['alias'] = $key; } @@ -128,6 +128,10 @@ protected function getFeatures(): array public function getAliasedFeatures(): array { $features = $this->getFeatures(); + return $this->mapFeatures($features); + } + + final protected function mapFeatures($features) { $mappedFeatures = []; foreach ($features as $key => $value) { $mappedFeatures[$value['alias']] = $value; diff --git a/src/inc/apiv2/common/AbstractModelAPI.class.php b/src/inc/apiv2/common/AbstractModelAPI.class.php index 4caea6340..1a74669d4 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.class.php +++ b/src/inc/apiv2/common/AbstractModelAPI.class.php @@ -110,6 +110,15 @@ final protected function getFeatures(): array ); } + /** + * Seperate get features function to get features without the formfields. This is needed to generate the openAPI documentation + * TODO: This function could probably be used in the patch endpoints aswell, since formfields are not relevant there. + */ + public function getFeaturesWithoutFormfields(): array { + $features = call_user_func($this->getDBAclass() . '::getFeatures'); + return $this->mapFeatures($features); + } + /** * Get features based on DBA model features * @@ -1157,7 +1166,7 @@ protected function updateObject(object $object, array $data, array $processed = */ final public function getPatchValidFeatures(): array { - $aliasedfeatures = $this->getAliasedFeatures(); + $aliasedfeatures = $this->getFeaturesWithoutFormfields(); $validFeatures = []; // Generate listing of validFeatures diff --git a/src/inc/apiv2/common/openAPISchema.routes.php b/src/inc/apiv2/common/openAPISchema.routes.php index 0f6918224..eeb681e01 100644 --- a/src/inc/apiv2/common/openAPISchema.routes.php +++ b/src/inc/apiv2/common/openAPISchema.routes.php @@ -388,7 +388,7 @@ function makeDescription($isRelation, $method, $singleObject): string { ], "data" => [ "type" => "object", - "properties" => makeProperties($class->getAliasedFeatures(), true) + "properties" => makeProperties($class->getFeaturesWithoutFormfields(), true) ] ]; From f2d26940731f8dd63760b0dc928a12d8cccc2e2d Mon Sep 17 00:00:00 2001 From: jessevz Date: Mon, 6 Jan 2025 15:32:26 +0100 Subject: [PATCH 3/4] Fixed bug in creating description for endpoints --- src/inc/apiv2/common/openAPISchema.routes.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/inc/apiv2/common/openAPISchema.routes.php b/src/inc/apiv2/common/openAPISchema.routes.php index eeb681e01..89ca40198 100644 --- a/src/inc/apiv2/common/openAPISchema.routes.php +++ b/src/inc/apiv2/common/openAPISchema.routes.php @@ -228,6 +228,7 @@ function makeDescription($isRelation, $method, $singleObject): string { $description = "GET many request to retrieve multiple objects."; } } + break; case "post": if ($isRelation) { if ($singleObject) { @@ -238,7 +239,8 @@ function makeDescription($isRelation, $method, $singleObject): string { } else { $description = "POST request to create a new object. The request must contain the resource record as data with the attributes of the new object." . "To add relationships, a relationships object can be added with the resource records of the relations that are part of this object."; - } + } + break; case "patch": if ($isRelation) { if ($singleObject) { From 0025b75d6a78d910a61b2e2daef2b010213f8aa8 Mon Sep 17 00:00:00 2001 From: jessevz Date: Wed, 8 Jan 2025 15:23:09 +0100 Subject: [PATCH 4/4] FEAT: added overidable patch to many relationship function to change logic where needed in model endpoints ex. supertasks route --- ci/apiv2/hashtopolis.py | 29 +++++++++++++-- ci/apiv2/test_supertask.py | 2 +- .../apiv2/common/AbstractModelAPI.class.php | 23 +++++++----- src/inc/apiv2/common/openAPISchema.routes.php | 2 +- src/inc/apiv2/model/supertasks.routes.php | 36 +++++++++++-------- 5 files changed, 65 insertions(+), 27 deletions(-) diff --git a/ci/apiv2/hashtopolis.py b/ci/apiv2/hashtopolis.py index b29d4fe39..03c548a5b 100644 --- a/ci/apiv2/hashtopolis.py +++ b/ci/apiv2/hashtopolis.py @@ -252,11 +252,29 @@ def patch_one(self, obj): payload = self.create_payload(obj, attributes, id=obj.id) logger.debug("Sending PATCH payload: %s to %s", json.dumps(payload), uri) r = requests.patch(uri, headers=headers, data=json.dumps(payload)) - self.validate_status_code(r, [201], "Patching failed") + self.validate_status_code(r, [200], "Patching failed") # TODO: Validate if return objects matches digital twin obj.set_initial(self.resp_to_json(r)['data'].copy()) + def send_patch(self, uri, data): + self.authenticate() + headers = self._headers + headers['Content-Type'] = 'application/json' + logger.debug("Sending PATCH payload: %s to %s", json.dumps(data), uri) + r = requests.patch(uri, headers=headers, data=json.dumps(data)) + self.validate_status_code(r, [204], "Patching failed") + + def patch_to_many_relationships(self, obj): + for k, v in obj.diff_includes().items(): + attributes = [] + logger.debug("Going to patch object '%s' property '%s' from '%s' to '%s'", obj, k, v[0], v[1]) + for include_id in v[1]: + attributes.append({"type": k, "id": include_id}) + data = {"data": attributes} + uri = self._hashtopolis_uri + obj.uri + "/relationships/" + k + self.send_patch(uri, data) + def create(self, obj): # Check if object to be created is new assert obj._new_model is True @@ -426,6 +444,8 @@ def all(cls): @classmethod def patch(cls, obj): + # TODO also patch to one relationships + cls.get_conn().patch_to_many_relationships(obj) cls.get_conn().patch_one(obj) @classmethod @@ -452,7 +472,6 @@ def get(cls, **filters): def count(cls, **filters): return cls.get_conn().count(filter=filters) - @classmethod def paginate(cls, **pages): return QuerySet(cls, pages=pages) @@ -605,6 +624,10 @@ def diff(self): if v_current != v_innitial: diffs.append((key, (v_innitial, v_current))) + return dict(diffs) + + def diff_includes(self): + diffs = [] # Find includeables sets which have changed for include in self.__included: if include.endswith('_set'): @@ -618,7 +641,7 @@ def diff(self): # Use ID of ojbects as new current/update identifiers if sorted(v_innitial_ids) != sorted(v_current_ids): diffs.append((innitial_name, (v_innitial_ids, v_current_ids))) - + return dict(diffs) def has_changed(self): diff --git a/ci/apiv2/test_supertask.py b/ci/apiv2/test_supertask.py index 2214e2a53..ac8874bea 100644 --- a/ci/apiv2/test_supertask.py +++ b/ci/apiv2/test_supertask.py @@ -31,7 +31,7 @@ def test_new_pretasks(self): # Quirk for expanding object to allow update to take place work_obj = Supertask.objects.prefetch_related('pretasks').get(pk=model_obj.id) - new_pretasks = [self.create_pretask() for i in range(2)] + new_pretasks = [self.create_pretask(file_id="003") for i in range(2)] selected_pretasks = [work_obj.pretasks_set[0], new_pretasks[1]] work_obj.pretasks_set = selected_pretasks work_obj.save() diff --git a/src/inc/apiv2/common/AbstractModelAPI.class.php b/src/inc/apiv2/common/AbstractModelAPI.class.php index 1a74669d4..47f057f68 100644 --- a/src/inc/apiv2/common/AbstractModelAPI.class.php +++ b/src/inc/apiv2/common/AbstractModelAPI.class.php @@ -352,7 +352,7 @@ protected function getFilterACL(): array /** * Helper function to determine if $resourceRecord is a valid resource record - * returns true if it is a valid resource record and false if it is an invallid resource record + * returns true if it is a valid resource record and false if it is an invalid resource record */ final protected function validateResourceRecord(mixed $resourceRecord): bool { @@ -365,7 +365,7 @@ final protected function ResourceRecordArrayToUpdateArray($data, $parentId) foreach ($data as $item) { if (!$this->validateResourceRecord($item)) { $encoded_item = json_encode($item); - throw new HttpErrorException('Invallid resource record given in list! invalid resource record: ' . $encoded_item); + throw new HttpErrorException('Invalid resource record given in list! invalid resource record: ' . $encoded_item); } $updates[] = new MassUpdateSet($item["id"], $parentId); } @@ -389,7 +389,7 @@ public static function getManyResources(object $apiClass, Request $request, Resp $pageAfter = $apiClass->getQueryParameterFamilyMember($request, 'page', 'after') ?? 0; $pageSize = $apiClass->getQueryParameterFamilyMember($request, 'page', 'size') ?? $defaultPageSize; if ($pageSize < 0) { - throw new HttpErrorException("Invallid parameter, page[size] must be a positive integer", 400); + throw new HttpErrorException("Invalid parameter, page[size] must be a positive integer", 400); } elseif ($pageSize > $maxPageSize) { throw new HttpErrorException(sprintf("You requested a size of %d, but %d is the maximum.", $pageSize, $maxPageSize), 400); } @@ -722,7 +722,7 @@ public function patchOne(Request $request, Response $response, array $args): Res // Return updated object $newObject = $this->getFactory()->get($object->getId()); - return self::getOneResource($this, $newObject, $request, $response, 201); + return self::getOneResource($this, $newObject, $request, $response, 200); } @@ -1011,8 +1011,18 @@ public function patchToManyRelationshipLink(Request $request, Response $response if ($jsonBody === null || !array_key_exists('data', $jsonBody) || !is_array($jsonBody['data'])) { throw new HttpErrorException('No data was sent! Send the json data in the following format: {"data":[{"type": "foo", "id": 1}}]'); } + $data = $jsonBody['data']; + $this->updateToManyRelationship($request, $data, $args); + + return $response->withStatus(204) + ->withHeader("Content-Type", "application/vnd.api+json"); + } + /** + * Overidable function to update the to many relationship + */ + protected function updateToManyRelationship(Request $request, array $data, array $args): void { $relation = $this->getToManyRelationships()[$args['relation']]; $primaryKey = $this->getPrimaryKeyOther($relation['relationType']); $relationKey = $relation['relationKey']; @@ -1038,7 +1048,7 @@ public function patchToManyRelationshipLink(Request $request, Response $response foreach ($data as $item) { if (!$this->validateResourceRecord($item)) { $encoded_item = json_encode($item); - throw new HttpErrorException('Invallid resource record given in list! invalid resource record: ' . $encoded_item); + throw new HttpErrorException('Invalid resource record given in list! invalid resource record: ' . $encoded_item); } $updates[] = new MassUpdateSet($item["id"], $args["id"]); unset($modelsDict[$item["id"]]); @@ -1058,9 +1068,6 @@ public function patchToManyRelationshipLink(Request $request, Response $response if (!$factory->getDB()->commit()) { throw new HttpErrorException("Was not able to update to many relationship"); } - - return $response->withStatus(204) - ->withHeader("Content-Type", "application/vnd.api+json"); } /** diff --git a/src/inc/apiv2/common/openAPISchema.routes.php b/src/inc/apiv2/common/openAPISchema.routes.php index 89ca40198..f7d372306 100644 --- a/src/inc/apiv2/common/openAPISchema.routes.php +++ b/src/inc/apiv2/common/openAPISchema.routes.php @@ -554,7 +554,7 @@ function makeDescription($isRelation, $method, $singleObject): string { // ]]; } elseif ($method == 'patch') { - $paths[$path][$method]["responses"]["201"] = [ + $paths[$path][$method]["responses"]["200"] = [ "description" => "successful operation", "content" => [ "application/json" => [ diff --git a/src/inc/apiv2/model/supertasks.routes.php b/src/inc/apiv2/model/supertasks.routes.php index d4d37e146..b6b193296 100644 --- a/src/inc/apiv2/model/supertasks.routes.php +++ b/src/inc/apiv2/model/supertasks.routes.php @@ -7,6 +7,10 @@ use DBA\Supertask; use DBA\SupertaskPretask; +use Middlewares\Utils\HttpErrorException; + +use Psr\Http\Message\ServerRequestInterface as Request; + require_once(dirname(__FILE__) . "/../common/AbstractModelAPI.class.php"); @@ -61,19 +65,19 @@ protected function createObject(array $data): int { return $objects[0]->getId(); } - public function updateObject(object $object, $data, $processed = []): void { - $key = "pretasks"; - if (array_key_exists($key, $data)) { - array_push($processed, $key); - - // Retrieve requested pretasks + public function updateToManyRelationship(Request $request, array $data, array $args): void { + $id = $args['id']; $wantedPretasks = []; - foreach(self::db2json($this->getAliasedFeatures()['pretasks'], $data[$key]) as $pretaskId) { - array_push($wantedPretasks, self::getPretask($pretaskId)); + foreach($data as $pretask) { + if (!$this->validateResourceRecord($pretask)) { + $encoded_pretask = json_encode($pretask); + throw new HttpErrorException('Invalid resource record given in list! invalid resource record: ' . $encoded_pretask); + } + array_push($wantedPretasks, self::getPretask($pretask["id"])); } // Find out which to add and remove - $currentPretasks = SupertaskUtils::getPretasksOfSupertask($object->getId()); + $currentPretasks = SupertaskUtils::getPretasksOfSupertask($id); function compare_ids($a, $b) { return ($a->getId() - $b->getId()); @@ -81,16 +85,20 @@ function compare_ids($a, $b) $toAddPretasks = array_udiff($wantedPretasks, $currentPretasks, 'compare_ids'); $toRemovePretasks = array_udiff($currentPretasks, $wantedPretasks, 'compare_ids'); - // Update model + $factory = $this->getFactory(); + $factory->getDB()->beginTransaction(); //start transaction to be able roll back + + // Update models foreach($toAddPretasks as $pretask) { - SupertaskUtils::addPretaskToSupertask($object->getId(), $pretask->getId()); + SupertaskUtils::addPretaskToSupertask($id, $pretask->getId()); } foreach($toRemovePretasks as $pretask) { - SupertaskUtils::removePretaskFromSupertask($object->getId(), $pretask->getId()); + SupertaskUtils::removePretaskFromSupertask($id, $pretask->getId()); } - } - parent::updateObject($object, $data, $processed); + if (!$factory->getDB()->commit()) { + throw new HttpErrorException("Was not able to update to many relationship"); + } } protected function deleteObject(object $object): void {