SOFA icon indicating copy to clipboard operation
SOFA copied to clipboard

How to specify an array of objects as a query parameter?

Open andyredhead opened this issue 2 years ago • 3 comments

We have a REST endpoint based on SOFA wrapping a graphql endpoint.

The query exposed by the REST endpoint can take an array of "objects" as one of the parameters.

If we provide a single object, everything works ok, however we are struggling to figure out how to specify more than one object in our query request... any guidance will be greatly appreciated

The endpoint lets us ask when "Articles" will be available to collect from a "Branch", where articles consist of a SKU and quantity while a branch just has a simple string id.

The graphql endpoint looks like:

query OrderDeliveryPromises($locale: String!, $articles: [Article!]!, $fulfilmentDetails: FulfilmentDetails!) {
  orderDeliveryPromises(locale: $locale, articles: $articles, fulfilmentDetails: $fulfilmentDetails) {
    articleId
    badge
    message
    status
    statusCode
    date
    quantity
    addToCartEnabled
    backOrderAllowed
    promiseSchedule {
      message
      date
      quantity
    }
  }
}

Typical variables for the query would look like (note the repeated entries in "articles"):

{  
  "locale": "uk",
  "articles": [
    {"articleId": "6086476", "quantity": 1} ,
    {"articleId": "6880401", "quantity": 10}
    ],
  "fulfilmentDetails": { 
    "type": "COLLECTION", 
    "collectionBranchId": "GB51"
  }
}

The sofa generated swagger.json for the query endpoint is (note articles is of type array with items "$ref": "#/components/schemas/Article"):

    "/api/order-delivery-promises": {
      "get": {
        "tags": [],
        "description": "",
        "summary": "",
        "operationId": "orderDeliveryPromises_query",
        "parameters": [
          {
            "in": "query",
            "name": "locale",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "articles",
            "required": true,
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Article"
              }
            }
          },
          {
            "in": "query",
            "name": "fulfilmentDetails",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/FulfilmentDetails"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/DeliveryPromise"
                  }
                }
              }
            }
          }
        }
      }
    }

The Article type is defined as:

      "Article": {
        "type": "object",
        "required": [
          "articleId",
          "quantity"
        ],
        "properties": {
          "articleId": {
            "type": "string"
          },
          "quantity": {
            "type": "integer",
            "format": "int32"
          }
        }
      } 

To request details for a single article we can make a request of the form:

https://our-sofa-server/api/order-delivery-promises?locale=uk&articles={
  "articleId": "444444",
  "quantity": 1
}&fulfilmentDetails={"type": "DELIVERY", "collectionBranchId": "GB51"}

Using curl (with url encoding added) with a command as shown below works as expected:

curl -H  'accept: application/json' \
-X 'GET' \
'https://our-sofa-server/api/order-delivery-promises?locale=uk&articles=%7B%0A%20%20%22articleId%22%3A%20%22444444%22%2C%0A%20%20%22quantity%22%3A%201%0A%7D&fulfilmentDetails=%7B%22type%22%3A%20%22DELIVERY%22%2C%20%22collectionBranchId%22%3A%20%22GB51%22%7D'

We had expected something like articles=[{"articleId": "44444", "quantity": 1},{"articleId": "555555", "quantity": 1}] to work but it doesn't (Bad Gateway error).

We cannot find a way to express more than one article in the query :(

Is there something we've overlooked?

Also, a follow-up question (assuming that supplying an array of object is possible), sending the query as a query string may not scale well if the query wants to include "lots" of articles... is there a way of sending the query as the request body instead?

andyredhead avatar Jan 26 '23 19:01 andyredhead

Having investigated a similar issue recently, I came to conclusion that this is actually not a SOFA issue, but rather an OpenAPI issue - openAPI does not support lists of objects in query parameters. You can see the still-open 5yo discussion here: https://github.com/OAI/OpenAPI-Specification/issues/1706

The workaround I've found is exposing the list argument in the request body rather than in the query arguments.

To support this over SOFA (without patching or modifying the resulting openapi schema), you may initiate SOFA with a custom route for this query, exposing it as HTTP POST with query body. This would look somewhere in the line of the following code:

const routes = {
    'Query.orderDeliveryPromises': { method: 'POST' , path: '/order-delivery-promises?locale=:locale' },
}

useSofa({
   ...,
   routes,
})

For the generated openapi schema, you will see that the locale parameter appears as a query argument, while articles & fulfilmentDetails appear as part of the request body.

This is a controversial yet acceptable compromise for passing complex query arguments in HTTP request body. You could technically also do this with HTTP GET, but not all client libraries support request body in HTTP GET (as it's not part of HTTP specs), and what's worse - SOFA does not currently support it.

PS: supporting query params in HTTP POST requires SOFA version 0.15.5 or later. See https://github.com/Urigo/SOFA/issues/1255.

amitlicht avatar Feb 05 '23 18:02 amitlicht

Thanks for your response :)

I've also come to the conclusion that OpenAPI does not support lists of objects in query parameters.

We started working around this by doing direct HTTP connections to the GraphQL endpoint.

The suggestion of using a combination of URL parameters and a POST query body looks useful :)

andyredhead avatar Feb 10 '23 14:02 andyredhead

Having investigated a similar issue recently, I came to conclusion that this is actually not a SOFA issue, but rather an OpenAPI issue - openAPI does not support lists of objects in query parameters. You can see the still-open 5yo discussion here: OAI/OpenAPI-Specification#1706

The workaround I've found is exposing the list argument in the request body rather than in the query arguments.

To support this over SOFA (without patching or modifying the resulting openapi schema), you may initiate SOFA with a custom route for this query, exposing it as HTTP POST with query body. This would look somewhere in the line of the following code:

const routes = {
    'Query.orderDeliveryPromises': { method: 'POST' , path: '/order-delivery-promises?locale=:locale' },
}

useSofa({
   ...,
   routes,
})

For the generated openapi schema, you will see that the locale parameter appears as a query argument, while articles & fulfilmentDetails appear as part of the request body.

This is a controversial yet acceptable compromise for passing complex query arguments in HTTP request body. You could technically also do this with HTTP GET, but not all client libraries support request body in HTTP GET (as it's not part of HTTP specs), and what's worse - SOFA does not currently support it.

PS: supporting query params in HTTP POST requires SOFA version 0.15.5 or later. See #1255.

Is there a way to specify that all queries by default be via POST?

tkosminov avatar Mar 15 '23 09:03 tkosminov