vendredi 10 juin 2016

REST API - Co-dependent Endpoint Dry Run Flow?

I'm currently developing a private rest api and am running into the issue of codependency of subresources when posting to create a parent level resource.

For example..

I have the following parent endpoint..

/products

And the following child endpoints relative to /products..

/products/[PRODUCT_ID]/categories
/products/[PRODUCT_ID]/media
/products/[PRODUCT_ID]/shippingZones
/products/[PRODUCT_ID]/variants

With this rest api, /products also has the ability to accept post payload for the following keys: 'categories', 'media', 'shippingZones', 'variants'.

If any of these keys are set, the /products endpoint will drill down and also create and associate sub resources based on the payload keys respectively for the current post request at /products.

Here's the code executed on a POST request to /products, showing how this is presently being handled. I'll get to the issue at hand after you take a moment to glance through below, maybe you'll see the issue before I explain it?

protected function post()
    {
        if (!$this->validatePermissions()) {
            return;
        }

        $productsM = new productsModel();
        $filesM = new filesModel();

        $userId = $this->controller->user['id'];
        $productId = $this->getResourceIdByName('products');

        $productCategories = $this->controller->payload['productCategories'];
        $productMedia = $this->controller->payload['productMedia'];
        $productShippingZones = $this->controller->payload['productShippingZones'];
        $productVariants = $this->controller->payload['productVariants'];

        $existingProduct = ($productId) ? $productsM->getSingle(array( 'id' => $productId, 'userId' => $userId )) : array();
        $product = array_merge($existingProduct, $this->controller->getFilteredPayload(array(
            'title',
            'description',
            'shippingType',
            'fileId',
            'hasVariants',
            'isHidden'
        )));

        $this->validateParameters(array( 'title' => $product['title'] ));

        if ($productId && !$existingProduct) {
            $this->addResponseError('productId');
        }

        if ($product['shippingType'] && !in_array($product['shippingType'], array( 'free', 'flat', 'calculated' ))) {
            $this->addResponseError('shippingType');
        }

        if ($product['fileId'] && !$filesM->getNumRows(array( 'id' => $product['fileId'], 'userId' => $userId ))) {
            $this->addResponseError('fileId');
        }

        if ($this->hasResponseErrors()) {
            return;
        }

        $lastCreatedProduct = (!$existingProduct) ? $productsM->getSingle(array( 'userId' => $userId ), array( 'publicId' => 'DESC' )) : array();

        $product = $productsM->upsert(array( 'id' => $productId, 'userId' => $userId ), array_merge($product, array(
            'publicId' => $lastCreatedProduct['publicId'] + 1,
            'userId' => $userId,
            'isActive' => 1,
            'modified' => time(),
            'created' => time()
        )), array( 'publicId' ));

        // product categories subresource
        if (is_array($productCategories)) {
            foreach ($productCategories as $index => $productCategory) {
                $endpoint = "/products/{$product['id']}/categories/{$productCategory['id']}";
                $requestMethod = ($productCategory['isActive'] !== '0') ? endpoint::REQUEST_METHOD_POST : endpoint::REQUEST_METHOD_DELETE;

                $productCategory = $this->executeEndpointByPath($endpoint, $requestMethod, $productCategory);
                foreach ($productCategory['errors'] as $error) {
                    $this->addResponseError($error['parameter'], $error['message'], array( 'productCategories', $index, $error['parameter'] ));
                }

                $product['productCategories'][$index] = $productCategory['data'];
            }
        }

        // product media subresource
        if (is_array($productMedia)) {
            foreach ($productMedia as $index => $media) {
                $endpoint = "/products/{$product['id']}/media/{$media['id']}";
                $requestMethod = ($media['isActive'] !== '0') ? endpoint::REQUEST_METHOD_POST : endpoint::REQUEST_METHOD_DELETE;

                $media = $this->executeEndpointByPath($endpoint, $requestMethod, $media);
                foreach ($media['errors'] as $error) {
                    $this->addResponseError($error['parameter'], $error['message'], array( 'productMedia', $index, $error['parameter'] ));
                }

                $product['productMedia'][$index] = $media['data'];
            }
        }

        // product shipping zones subresource
        if (is_array($productShippingZones)) {
            foreach ($productShippingZones as $index => $productShippingZone) {
                $endpoint = "/products/{$product['id']}/shippingZones/{$productShippingZone['id']}";
                $requestMethod = ($productShippingZone['isActive'] !== '0') ? endpoint::REQUEST_METHOD_POST : endpoint::REQUEST_METHOD_DELETE;

                $productShippingZone = $this->executeEndpointByPath($endpoint, $requestMethod, $productShippingZone);
                foreach ($productShippingZone['errors'] as $error) {
                    $this->addResponseError($error['parameter'], $error['message'], array( 'productShippingZones', $index, $error['parameter'] ));                  
                }

                $product['productShippingZones'][$index] = $productShippingZone['data'];
            }
        }

        // product variants subresource
        if (is_array($productVariants)) {
            foreach ($productVariants as $index => $productVariant) {
                $endpoint = "/products/{$product['id']}/variants/{$productVariant['id']}";
                $requestMethod = ($productVariant['isActive'] !== '0') ? endpoint::REQUEST_METHOD_POST : endpoint::REQUEST_METHOD_DELETE;

                $productVariant = $this->executeEndpointByPath($endpoint, $requestMethod, $productVariant);
                foreach ($productVariant['errors'] as $error) {
                    $this->addResponseError($error['parameter'], $error['message'], array( 'productVariants', $index, $error['parameter'] ));
                }

                $product['productVariants'][$index] = $productVariant['data'];
            }
        }

        return $product;
    }

Alright! Now to the problem. With this flow, the subresource creation for /products on a newly created product becomes dependent on a new row being inserted into the products database table and returning a product id prior to iterating down and creating the sub resources, as the subresources will throw errors if not passed a productId in their endpoint uri.

This creates an issue of codependency and maintaining ALL or NOTHING principles.

In the event that a new product is created and the initial /products error checking is completed, the product gets a new row in the products database table.. However, if after this is done and it goes on to subresource creation and any subresource creation fails due to the data passed in the initial request, the initial request only partially succeeds as the errors from those sub resources will prevent the specifically erroring subresource from being created and associated with the initially created product.

So here's a few of my ideas..

I'd like to potentially implement a dry run approach that totally ignores inserts / updates and runs the data through all parent / child endpoint error handling to see if the data is clean. I'm not completely sure how to merge this into the flow of the endpoints without overly complexifying and breaking the readability of the code flow.

Any other ideas or changes to execution flow that would resolve this would really be appreciated to get me pointed in the proper direction of the best approach.

Thanks!

Aucun commentaire:

Enregistrer un commentaire