Repository: saleor/saleor

Issue:
Urls returned by GraphQL API should be absolute
### What I'm trying to achieve
Right now default media storage returns relative assets URLs. For instance:
```
/media/__sized__/products/saleor/static/placeholders/t-shirts/6-crop-c0-5__0-5-255x255-70.jpg
```
For API consumers will be much easier to work with relative URLs like:
```
https://example.com/media/__sized__/products/saleor/static/placeholders/t-shirts/6-crop-c0-5__0-5-255x255-70.jpg
```

### Describe a proposed solution
My current solution fixes only product images and looks like:

```
def resolve_thumbnail_url(self, info, *, size=None):
        if not size:
            size = 255
        thumbnail_url = get_thumbnail(self.get_first_image(), size)
        is_absolute_url = urlparse(thumbnail_url).netloc
        if not is_absolute_url:
            return info.context.build_absolute_uri(thumbnail_url) 
        return thumbnail_url
```

But we need a solution which will be working for all media urls.



Discussion:
Nitpick: `/foo/` is an absolute URL but I agree that the URLs should contain both the protocol and the FQDN.
Also todo in this issue - make sure that `backgroundImage` fields return correct URL. Right now there is a bug and a path to the file is returned if I'm correct. Also, it would be nice if those fields would allow fetching image in different sizes, just as product images do.
In a real deployment scenario, the URLs will be absolute with FQDN because of external storage like S3 or Google Cloud Storage. Prefix (FQDN) will be controlled with `STATIC_URL` and `MEDIA_URL` variables which could be controlled via environment variables.

The other case that @maarcingebala reported with `backgroundImage` URL  is really a bug which I will fix in this issue.


Resolving patch:
diff --git a/saleor/graphql/product/types.py b/saleor/graphql/product/types.py
--- a/saleor/graphql/product/types.py
+++ b/saleor/graphql/product/types.py
@@ -128,6 +128,21 @@ class Meta:
         description = 'Represents availability of a product in the storefront.'
 
 
+class Image(graphene.ObjectType):
+    url = graphene.String(
+        required=True,
+        description='The URL of the image.',
+        size=graphene.Int(description='Size of the image'))
+
+    class Meta:
+        description = 'Represents an image.'
+
+    def resolve_url(self, info, size=None):
+        if size:
+            return get_thumbnail(self, size, method='thumbnail')
+        return self.url
+
+
 class Product(CountableDjangoObjectType):
     url = graphene.String(
         description='The storefront URL for the product.', required=True)
@@ -162,7 +177,7 @@ class Meta:
     def resolve_thumbnail_url(self, info, *, size=None):
         if not size:
             size = 255
-        return get_thumbnail(self.get_first_image(), size)
+        return get_thumbnail(self.get_first_image(), size, method='thumbnail')
 
     def resolve_url(self, info):
         return self.get_absolute_url()
@@ -220,6 +235,7 @@ class Collection(CountableDjangoObjectType):
     products = DjangoFilterConnectionField(
         Product, filterset_class=ProductFilterSet,
         description='List of collection products.')
+    background_image = graphene.Field(Image)
 
     class Meta:
         description = "Represents a collection of products."
@@ -250,6 +266,7 @@ class Category(CountableDjangoObjectType):
         lambda: Category,
         filterset_class=DistinctFilterSet,
         description='List of children of the category.')
+    background_image = graphene.Field(Image)
 
     class Meta:
         description = """Represents a single category of products. Categories
@@ -293,5 +310,5 @@ class Meta:
 
     def resolve_url(self, info, *, size=None):
         if size:
-            return get_thumbnail(self.image, size, 'crop')
+            return get_thumbnail(self.image, size, method='thumbnail')
         return self.image.url
diff --git a/saleor/product/templatetags/product_images.py b/saleor/product/templatetags/product_images.py
--- a/saleor/product/templatetags/product_images.py
+++ b/saleor/product/templatetags/product_images.py
@@ -80,7 +80,7 @@ def get_thumbnail_size(size, method):
 
 
 @register.simple_tag()
-def get_thumbnail(instance, size, method='crop'):
+def get_thumbnail(instance, size, method):
     if instance:
         used_size = get_thumbnail_size(size, method)
         try:
diff --git a/saleor/settings.py b/saleor/settings.py
--- a/saleor/settings.py
+++ b/saleor/settings.py
@@ -396,12 +396,12 @@ def get_host():
 
 VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = {
     'products': [
-        ('product_gallery', 'crop__540x540'),
-        ('product_gallery_2x', 'crop__1080x1080'),
-        ('product_small', 'crop__60x60'),
-        ('product_small_2x', 'crop__120x120'),
-        ('product_list', 'crop__255x255'),
-        ('product_list_2x', 'crop__510x510')]}
+        ('product_gallery', 'thumbnail__540x540'),
+        ('product_gallery_2x', 'thumbnail__1080x1080'),
+        ('product_small', 'thumbnail__60x60'),
+        ('product_small_2x', 'thumbnail__120x120'),
+        ('product_list', 'thumbnail__255x255'),
+        ('product_list_2x', 'thumbnail__510x510')]}
 
 VERSATILEIMAGEFIELD_SETTINGS = {
     # Images should be pre-generated on Production environment


Test patch:
diff --git a/tests/api/test_product.py b/tests/api/test_product.py
--- a/tests/api/test_product.py
+++ b/tests/api/test_product.py
@@ -1287,3 +1287,52 @@ def test_product_variants_no_ids_list(user_api_client, variant):
     assert 'errors' not in content
     data = content['data']['productVariants']
     assert len(data['edges']) == ProductVariant.objects.count()
+
+
+def test_category_image_query(user_api_client, non_default_category):
+    category = non_default_category
+    image_file, image_name = create_image()
+    category.background_image = image_file
+    category.save()
+    category_id = graphene.Node.to_global_id('Category', category.pk)
+    query = """
+        query fetchCategory($id: ID!){
+            category(id: $id) {
+                backgroundImage {
+                   url(size: 120)
+                }
+            }
+        }
+    """
+    variables = json.dumps({'id': category_id})
+    response = user_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    assert 'errors' not in content
+    data = content['data']['category']
+    thumbnail_url = category.background_image.thumbnail['120x120'].url
+    assert data['backgroundImage']['url'] == thumbnail_url
+
+
+def test_collection_image_query(user_api_client, collection):
+    image_file, image_name = create_image()
+    collection.background_image = image_file
+    collection.save()
+    collection_id = graphene.Node.to_global_id('Collection', collection.pk)
+    query = """
+        query fetchCollection($id: ID!){
+            collection(id: $id) {
+                backgroundImage {
+                   url(size: 120)
+                }
+            }
+        }
+    """
+    variables = json.dumps({'id': collection_id})
+    response = user_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    assert 'errors' not in content
+    data = content['data']['collection']
+    thumbnail_url = collection.background_image.thumbnail['120x120'].url
+    assert data['backgroundImage']['url'] == thumbnail_url