api-platform / core

The server component of API Platform: hypermedia and GraphQL APIs in minutes
https://api-platform.com
MIT License
2.45k stars 877 forks source link

Subresource Creation Fails When Attempting to Create Only the Subresource #6622

Open Rafikooo opened 2 months ago

Rafikooo commented 2 months ago

API Platform Version(s) Affected: 3.3.12

Description: Subresource creation using URI templates is not functioning as expected. When attempting to create only the subresource via the parent resource’s identifier in the URI, several issues occur, such as errors when the parent resource doesn’t exist, unexpected overriding of existing subresources, and failures when multiple subresources are involved. This behavior persists across different scenarios and requires attention to properly handle subresource creation through URI templates.

Steps to Reproduce:

  1. Configure two resources that are related.
  2. Set up a subresource POST operation using the following URI template: resources/{resourceId}/subresource.
  3. Attempt to create only the subresource using the ID of an existing parent resource: resources/{existingId}/subresource.

Our resource configuration example with some outcomes:

<resource class="Sylius\Component\Core\Model\PromotionCoupon">
    <operations>
        <operation class="ApiPlatform\Metadata\Post"
                   uriTemplate="/admin/promotions/{promotionCode}/coupons">
            <uriVariables>
                <uriVariable parameterName="promotionCode" fromClass="Sylius\Component\Core\Model\Promotion"
                             fromProperty="coupons"/>
            </uriVariables>
        </operation>
    </operations>
</resource>

We tested several scenarios (for clarity, required payload data is omitted in the examples below):

  1. URI: /promotions/non-existent-code/coupons
    Operation: POST
    Initial state: None
    Expected: Parent resource not found
    Actual Result:

    {
       "@context": "/api/v2/contexts/Error",
       "@type": "hydra:Error",
       "hydra:title": "An error occurred",
       "hydra:description": "PropertyAccessor requires a graph of objects or arrays to operate on, but it found type 'NULL' while trying to traverse path 'promotion.code' at property 'code'."
    }
  2. URI: /promotions/autumn/coupons
    Operation: POST
    Initial state: Promotion exists, no coupons
    Expected: Subresource created
    Actual Result:

    {
       "@context": "/api/v2/contexts/Error",
       "@type": "hydra:Error",
       "hydra:title": "An error occurred",
       "hydra:description": "PropertyAccessor requires a graph of objects or arrays to operate on, but it found type 'NULL' while trying to traverse path 'promotion.code' at property 'code'."
    }
  3. URI: /promotions/autumn/coupons
    Operation: POST
    Initial state: Promotion exists, one coupon exists
    Expected: Second coupon created
    Actual Result: 201, but the existing subresource was overridden instead of creating a new one.

  4. URI: /promotions/autumn/coupons
    Operation: POST
    Initial state: Promotion exists, two coupons exist
    Expected: Third coupon created
    Actual Result:

    {
       "@context": "/api/v2/contexts/Error",
       "@type": "hydra:Error",
       "hydra:title": "An error occurred",
       "hydra:description": "More than one result was found for query although one row or none was expected."
    }

Workarounds: We observed interesting behavior when allowing the payload to include relation information, although these scenarios are not valid. They might provide insight into the core issue.

  1. URI: /promotions/autumn/coupons
    Operation: POST
    Initial state: Promotion exists, no coupons
    Payload:

    {
       ...
       "promotion": "api/v2/admin/promotions/autumn"
    }

    Expected: Coupon created
    Actual Result:

    • First request: coupon created.
    • Second identical request: coupon overridden instead of creating a new one.
  2. URI: /promotions/autumn/coupons
    Operation: POST
    Initial state: Promotion exists, two coupons
    Payload:

    {
       ...
       "promotion": "api/v2/admin/promotions/autumn"
    }

    Expected: Third coupon created
    Actual Result:

    {
       "@context": "/api/v2/contexts/Error",
       "@type": "hydra:Error",
       "hydra:title": "An error occurred",
       "hydra:description": "More than one result was found for query although one row or none was expected."
    }
  3. URI: /promotions/different-promotion-code-or-made-up-one/coupons
    Operation: POST
    Initial state: Promotion exists (autumn promotion), any number of coupons
    Payload:

    {
       ...
       "promotion": "api/v2/admin/promotions/autumn"
    }

    Expected: Each request creates a new coupon resource
    Actual Result: Expected behavior.

Conclusion: Scenario seven works, but it’s tricky since the operation is based on the payload rather than URL parameters. The issue seems related to how API Platform resolves links, particularly when the base resource is a subresource (in this example, <resource class="Sylius\Component\Core\Model\PromotionCoupon">).

stale[bot] commented 15 hours ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.