Uploading media

Last updated

Uploading product media using AWS S3 pre-signed POST:#

AWS Pre-signed POST data enables direct uploads to S3 storage in a controlled and safe way.

The client makes request for obtaining pre-signed POST Policy containing URL and data fields required to prepare multipart/form-data POST request to upload a file directly to S3. S3 will respond with successful response or error message if upload fails.

The policy expires after 20 minutes by default and its expiration is adjustable by configuration.

How does it work with GraphQL Integration API:#

Product media upload flow consists of 3 steps.

  1. Obtain the policy from Integration API mutation,
  2. Upload file to S3 using that policy,
  3. Assign uploaded file to a proper object - either Product or ProductVariant.

Files that are uploaded but not assigned to any existing Product or ProductVariant will be removed after 2 days.

Required permission is ProductMedia:write

Product media upload flow:#

  • Use createMediaUpload mutation to obtain pre-signed POST policy along with unique identifier of the file.
  • Use policy data to construct an upload request with your file to S3 (see code examples).
  • After successful upload use completeMediaUpload mutation with the unique identifier to assign uploaded media to proper Product or ProductVariant. If your file wasn't uploaded successfully and cannot be found by given identifier, the mutation will fail.

Notes#

  • Content-Type field in form data is obligatory due to mime type restrictions and must be set to the value of uploaded file type before the actual file input.
  • Supported file formats are png, jpg and gif.
  • Successful response from S3 file upload request is indicated by 204 status code.

Code examples:

  • JavaScript upload function using XHR request:
1const uploadFile = (uploadPolicy, file) => { 2 return new Promise((resolve, reject) => { 3 4 const formData = new FormData(); 5 Object.keys(uploadPolicy.fields).forEach(field => { 6 formData.append(field.name, field.value); 7 }); 8 const actionAttribute = uploadPolicy.attributes.find(attribute => attribute.name === 'action'); 9 const url = actionAttribute.value; 10 formData.append('Content-Type', file.type); 11 formData.append("file", file); 12 const xhr = new XMLHttpRequest(); 13 xhr.open("POST", url, true); 14 xhr.send(formData); 15 xhr.onload = function() { 16 this.status === 204 ? resolve() : reject(this.responseText); 17 }; 18 }); 19}; 20
  • HTML template with dynamically created media upload form:
1<!DOCTYPE html> 2<html> 3 4<head> 5 <title> 6 Test AWS s3 pre-signed post form media upload 7 </title> 8 <style type="text/css"> 9 body { 10 margin: 50px auto; 11 text-align: center; 12 width: 33%; 13 } 14 div { 15 margin-top: 15px; 16 } 17 tt { 18 padding: 2px 4px; 19 background-color: #eee; 20 border: 1px solid #aaa; 21 border-radius: 5px; 22 } 23 #gql-form { 24 width: 100%; 25 text-align: left; 26 } 27 #gql-input { 28 width: 100%; 29 height: 300px; 30 } 31 #uuid-container { 32 width: 250px; 33 line-height: 2.5em; 34 text-align: center; 35 background-color: #eee; 36 border: 1px solid #aaa; 37 border-radius: 5px; 38 } 39 </style> 40</head> 41 42<body> 43 44 45<form id='gql-form'> 46 <h3>Usage</h3> 47 <ol> 48 <li>Call `createMediaUpload` mutation, eg: 49 <pre> 50mutation { 51 createMediaUpload( 52 input: { 53 mediaType: IMAGE 54 } 55 ) { 56 UUID 57 uploadPolicy { 58 attributes { name value } 59 fields { name value } 60 } 61 userErrors { message path } 62 } 63} 64 </pre></li> 65 <li>Copy response and paste in the input below</li> 66 <li>Use <tt>Generate Form</tt> button to create an upload form</li> 67 <li>Once the form is generated, select file using <tt>Choose File</tt> button and then click 68 <tt>upload</tt></li> 69 </ol> 70 <h3>Generate upload form</h3> 71 <label for='gql-input'>Paste GraphQL response:</label><br/> 72 <textarea id='gql-input'></textarea><br/> 73 <button type="submit">Generate Form</button> 74</form> 75<div><b>UUID:</b><input type="text" readonly="readonly" id='uuid-container'/></div> 76<div id="test-form-container"> 77</div> 78 79<script> 80 function generateForm(e) { 81 e.preventDefault(); 82 let input = document.getElementById('gql-input'); 83 let parsed = JSON.parse(input.value); 84 if (parsed === false) { 85 console.error('Invalid response -- not a valid JSON'); 86 } 87 createForm(parsed); 88 89 return false; 90 } 91 92 window.onload = function exampleFunction() { 93 94 document.getElementById('gql-form').addEventListener('submit', generateForm); 95 } 96 97 function createForm(gqlResponse) { 98 let formContainer = document.getElementById('test-form-container'); 99 formContainer.innerHTML = ''; 100 document.getElementById('uuid-container').value = gqlResponse.data.createMediaUpload.UUID; 101 let policy = gqlResponse.data.createMediaUpload.uploadPolicy; 102 let formAttributes = policy.attributes; 103 let formInputs = policy.fields; 104 105 var form = document.createElement("form"); 106 formAttributes.forEach(function (attr) { 107 form.setAttribute(attr.name, attr.value) 108 }); 109 110 formInputs.forEach(function(input) { 111 let inputElem = document.createElement("input"); 112 inputElem.setAttribute('type', 'hidden'); 113 114 inputElem.setAttribute('name', input.name); 115 inputElem.setAttribute('value', input.value); 116 117 form.appendChild(inputElem); 118 }); 119 120 let fileInput = document.createElement("input"); 121 fileInput.setAttribute('type', 'file'); 122 fileInput.setAttribute('name', 'file'); 123 form.appendChild(fileInput); 124 125 let submit = document.createElement("input"); 126 submit.setAttribute('type', 'submit'); 127 submit.setAttribute('value', 'upload'); 128 form.appendChild(submit) 129 130 fileInput.addEventListener('input', function(e) { 131 let inputElem = document.createElement("input"); 132 inputElem.setAttribute('type', 'hidden'); 133 inputElem.setAttribute('name', "Content-Type"); 134 inputElem.setAttribute('value', fileInput.files[0].type); 135 form.insertBefore(inputElem, fileInput); 136 }); 137 138 formContainer.appendChild(form); 139 } 140</script> 141</body> 142 143</html> 144

Create a new batch upload#

To initiate a new batch, call the createMediaBatch mutation:

There is only one requirement: imported images must be accessible via the Internet by a URL.

1mutation createMB { 2 createMediaBatch(input: { 3 productMedia: [ 4 { 5 productId: 1 6 variantId: 1445 7 url: "https://picsum.photos/id/1072/3872/2592" 8 metaDataJSON: "{\"my-data\": \"Anything can go here\"}" 9 }, 10 { 11 productId: 1 12 url: "https://picsum.photos/id/1003/1181/1772" 13 } 14 ] 15 }) { 16 queueId 17 userErrors { 18 message 19 path 20 } 21 } 22} 23

A few important notes:

  1. You can connect media directly to a product variant if it only applies to one variant.

  2. The maximum number of media to be imported at once is 100.

  3. You can add your own metadata and later read it back from a ProductMedia object. Metadata should be a JSON object (not a list or a scalar), but the keys can store any type of value.

  4. Save the queueId value returned from the mutation if you want to check the progress.

Beside your own values, the product media metadata will have some additional keys added automatically by this process. They are especially useful to determine, whether given ProductMedia is the same image you want, or not.

  • originalUrl – the imported url.
  • originalSha1 – an SHA-1 checksum of the original file contents.
  • originalWidth + originalHeight – dimensions of the originally uploaded image.

Check the batch status#

Because processing of your batch upload is asynchronous, you may want to check its progress.

1query MBstatus { 2 mediaBatch(queueId: "acd5518727f54c5c9a5b2e31d6d742d2") { # insert your queueId 3 status 4 productMedia { 5 productId 6 variantId 7 mediaType 8 url 9 key 10 completed 11 } 12 } 13} 14

See the BatchStatus enum values for possible statuses.

Fetch the new media from a product query#

When your batch is COMPLETED, you should see the new media on a product:

1query lastMedia { 2 product(id: 1) { 3 media(sort: id_DESC, limit: 1) { 4 id 5 source(sizeName: "standard") { 6 mediaSize { 7 name 8 quality 9 maxWidth 10 maxHeight 11 } 12 url 13 mimeType 14 } 15 metaDataJSON 16 } 17 } 18} 19

Add media to displays and arrange their order#

When creating or modifying displays, you can first add addProductMedia.id to your display:

1mutation { 2 createDisplay( 3 input: { 4 name: "display-name" 5 status: ACTIVE 6 store: { id: 1 } 7 product: { id: 1 } 8 addProductMedia: [ 9 {productMedia: {id: 467}} 10 ] 11 } 12 ) { 13 userErrors { message path } 14 display { 15 id 16 media {id} 17 } 18 } 19} 20

Later you can manipulate and re-order these productMedia objects as necessary:

1mutation { 2 updateDisplay( 3 id: 1234 4 input: { 5 addProductMedia: [{ productMedia: { id: 469 }, before: { id: 466 } }] 6 removeProductMedia: [{ id: 467 }] 7 } 8 ) { 9 userErrors { 10 message 11 path 12 } 13 display { 14 id 15 media { 16 id 17 } 18 } 19 } 20} 21

Uploading individual images to static (mapped) custom attributes#

Another use case when you would like to upload images would be to add them to your static custom attributes, like Showroom Swatches. Let's take a simple attribute for example:

pr_extra_info

1{ 2 "desc": "Product Extra Info", 3 "readonly": true, 4 "group": "product", 5 "elements": { 6 "image": { 7 "desc": "Product Extra Image", 8 "type": "image", 9 "size": "0x0" 10 } 11 } 12} 13

Let's start by fetching the attribute definition, to make sure it's available in GQL:

Get attribute definition information

1query attributeTypes { 2 attributeTypes(where: {name: {equals: ["pr_extra_info"]}}) { 3 name 4 description 5 isMapped 6 isMulti 7 objectType 8 elements { 9 key 10 description 11 kind 12 isMulti 13 paramsJSON 14 } 15 } 16} 17

Response:

1{ 2 "data": { 3 "attributeTypes": [ 4 { 5 "name": "pr_extra_info", 6 "description": "Product Extra Info", 7 "isMapped": true, 8 "isMulti": false, 9 "objectType": "Product", 10 "elements": [ 11 { 12 "key": "image", 13 "description": "Product Extra Image", 14 "kind": "IMAGE", 15 "isMulti": false, 16 "paramsJSON": "{\"size\":\"0x0\"}" 17 } 18 ] 19 } 20 ] 21 }, 22 "extensions": { 23 "complexity": 110, 24 "permissionsUsed": [ 25 "Attribute:read" 26 ], 27 "appVersion": "v1.23.0" 28 } 29} 30

Now, let's upload the media that we wish to assign to the attribute. For that purpose we will create a media upload policy that will later accept the uploaded file:

1mutation mediaUploadTest { 2 createMediaUpload( 3 input: { 4 mediaType: IMAGE 5 } 6 ) { 7 UUID 8 uploadPolicy { 9 attributes { name value } 10 fields { name value } 11 } 12 userErrors { message path } 13 userWarnings { message path } 14 } 15} 16

Response:

1{ 2 "data": { 3 "createMediaUpload": { 4 "UUID": "4573c32c27e05fce541ed2d4b75a5904", 5 "uploadPolicy": { 6 "attributes": [ 7 { 8 "name": "action", 9 "value": "https://centra-webinar-silk-staging.s3.eu-west-1.amazonaws.com" 10 }, 11 { 12 "name": "method", 13 "value": "POST" 14 }, 15 { 16 "name": "enctype", 17 "value": "multipart/form-data" 18 } 19 ], 20 "fields": [ 21 { 22 "name": "acl", 23 "value": "private" 24 }, 25 { 26 "name": "key", 27 "value": "media/client/dynamic/temp/4573c32c27e05fce541ed2d4b75a5904" 28 }, 29 { 30 "name": "X-Amz-Credential", 31 "value": "AKIARA6PLGVYP5LNTAHT/20240619/eu-west-1/s3/aws4_request" 32 }, 33 { 34 "name": "X-Amz-Algorithm", 35 "value": "AWS4-HMAC-SHA256" 36 }, 37 { 38 "name": "X-Amz-Date", 39 "value": "20240619T121408Z" 40 }, 41 { 42 "name": "Policy", 43 "value": "eyJleHBpcmF0aW9uIjoiMjAyNC0wNi0xOVQxMjozNDowOFoiLCJjb25kaXRpb25zIjpbeyJhY2wiOiJwcml2YXRlIn0seyJidWNrZXQiOiJjZW50cmEtd2ViaW5hci1zaWxrLXN0YWdpbmcifSxbImVxIiwiJGtleSIsIm1lZGlhXC9jbGllbnRcL2R5bmFtaWNcL3RlbXBcLzQ1NzNjMzJjMjdlMDVmY2U1NDFlZDJkNGI3NWE1OTA0Il0sWyJzdGFydHMtd2l0aCIsIiRDb250ZW50LVR5cGUiLCIiXSx7IlgtQW16LURhdGUiOiIyMDI0MDYxOVQxMjE0MDhaIn0seyJYLUFtei1DcmVkZW50aWFsIjoiQUtJQVJBNlBMR1ZZUDVMTlRBSFRcLzIwMjQwNjE5XC9ldS13ZXN0LTFcL3MzXC9hd3M0X3JlcXVlc3QifSx7IlgtQW16LUFsZ29yaXRobSI6IkFXUzQtSE1BQy1TSEEyNTYifV19" 44 }, 45 { 46 "name": "X-Amz-Signature", 47 "value": "68df754455158a673f6fbe400f0407b7f53c58c5b37446a3c9d07f75ff327a08" 48 } 49 ] 50 }, 51 "userErrors": [], 52 "userWarnings": [] 53 } 54 }, 55 "extensions": { 56 "complexity": 242, 57 "permissionsUsed": [ 58 "ProductMedia:write" 59 ], 60 "appVersion": "v1.23.0" 61 } 62} 63

Now you must upload your image file to that UUID. Use our example of a simple website linked under the "Notes" above.

When you submit your createMediaUpload response, you can then choose the file to be uploaded. Select the file, click "Upload". This will upload your file directly to the right S3 bucket.

Once done, you can then assign your UUID as the image parameter on the new custom attribute:

1mutation createAttribute { 2 createAttribute(input: { 3 attributeTypeName: "pr_extra_info" 4 name: "Product Extra Info test1" 5 imageElements: [ 6 {key: "image", uploadUUID: "4573c32c27e05fce541ed2d4b75a5904"} 7 ] 8 }) { 9 attribute { 10 ...attributeData 11 } 12 userErrors { message path } 13 userWarnings { message path } 14 } 15} 16

Response:

1{ 2 "data": { 3 "createAttribute": { 4 "attribute": { 5 "id": 30, 6 "name": "Product Extra Info test1", 7 "type": { 8 "name": "pr_extra_info" 9 }, 10 "elements": [ 11 { 12 "key": "image", 13 "description": "Product Extra Image", 14 "kind": "IMAGE", 15 "url": "http://centra-webinar.centraqa.com/client/dynamic/attributes/4573c32c27e05fce541ed2d4b75a5904.jpg", 16 "height": null, 17 "width": null, 18 "mimeType": "image/jpg" 19 } 20 ] 21 }, 22 "userErrors": [], 23 "userWarnings": [] 24 } 25 }, 26 "extensions": { 27 "complexity": 263, 28 "permissionsUsed": [ 29 "Attribute:write", 30 "Attribute:read" 31 ], 32 "appVersion": "v1.23.0" 33 } 34} 35

As you can see in AMS backend, attribute now exists with the uploaded image:

You can now assign this attribute (id: 30) to your products.

One more thing: You can also first create the attributes, and later upload the images to them. To create an empty attribute without an image, simply pass null in the UUID attribute:

1mutation createAttribute { 2 createAttribute(input: { 3 attributeTypeName: "pr_extra_info" 4 name: "Product Extra Info test2" 5 imageElements: [ 6 {key: "image", uploadUUID: null} 7 ] 8 }) { 9 attribute { 10 ...attributeData 11 } 12 userErrors { message path } 13 userWarnings { message path } 14 } 15} 16