Panduan lengkap untuk mengunggah berkas ke Blue menggunakan mutasi GraphQL atau REST API
Ikhtisar
Panduan ini menunjukkan cara mengunggah berkas ke Blue menggunakan dua pendekatan yang berbeda:
- Unggahan GraphQL Langsung (Direkomendasikan) - Unggahan satu langkah yang sederhana dengan batas ukuran berkas 256MB
- Unggahan REST API - Proses tiga langkah yang mendukung berkas yang lebih besar hingga 4.8GB
Ini adalah perbandingan dari kedua metode:
Fitur | Unggahan GraphQL | Unggahan REST API |
---|---|---|
Complexity | Simple (one request) | Complex (three steps) |
File Size Limit | 256MB per file | 4.8GB per file |
Batch Upload | Up to 10 files | Single file only |
Implementation | Direct mutation | Multi-step process |
Best For | Most use cases | Large files only |
Unggahan Berkas GraphQL
Metode unggahan GraphQL menyediakan cara yang sederhana dan langsung untuk mengunggah berkas dengan satu permintaan.
uploadFile
Mengunggah satu berkas ke sistem penyimpanan berkas dan membuat referensi berkas di database.
Input:
file: Upload!
- Berkas yang akan diunggah (menggunakan multipart/form-data)projectId: String!
- ID proyek atau slug tempat berkas akan disimpancompanyId: String!
- ID perusahaan atau slug tempat berkas akan disimpan
Mengembalikan: File!
- Objek berkas yang dibuat
Contoh:
mutation UploadFile($input: UploadFileInput!) {
uploadFile(input: $input) {
id
uid
name
size
type
extension
shared
createdAt
project {
id
name
}
folder {
id
title
}
}
}
uploadFiles
Mengunggah beberapa berkas ke sistem penyimpanan berkas dan membuat referensi berkas di database.
Input:
files: [Upload!]!
- Array berkas yang akan diunggah (maks 10)projectId: String!
- ID proyek atau slug tempat berkas akan disimpancompanyId: String!
- ID perusahaan atau slug tempat berkas akan disimpan
Mengembalikan: [File!]!
- Array objek berkas yang dibuat
Contoh:
mutation UploadFiles($input: UploadFilesInput!) {
uploadFiles(input: $input) {
id
uid
name
size
type
extension
shared
createdAt
}
}
Implementasi Klien
Klien Apollo (JavaScript)
Unggahan Berkas Tunggal:
import { gql } from '@apollo/client';
const UPLOAD_FILE = gql`
mutasi UploadFile($input: UploadFileInput!) {
uploadFile(input: $input) {
id
uid
nama
ukuran
tipe
ekstensi
dibagikan
dibuatPada
}
}
`;
// Using file input
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
const { data } = await uploadFile({
variables: {
input: {
file: file,
projectId: "project_123", // or "my-project-slug"
companyId: "company_456" // or "my-company-slug"
}
}
});
Multiple Files Upload:
const UPLOAD_FILES = gql`
mutasi UploadFiles($input: UploadFilesInput!) {
uploadFiles(input: $input) {
id
uid
nama
ukuran
tipe
ekstensi
dibagikan
dibuatPada
}
}
`;
// Using multiple file inputs
const fileInputs = document.querySelectorAll('input[type="file"]');
const files = Array.from(fileInputs).map(input => input.files[0]).filter(Boolean);
const { data } = await uploadFiles({
variables: {
input: {
files: files,
projectId: "project_123", // or "my-project-slug"
companyId: "company_456" // or "my-company-slug"
}
}
});
@@CB##ba81f4d5-8d2f-42d8-9e21-bc464ab5d4f8##CB@@html
<!-- HTML -->
<input type="file" id="fileInput" />
<button onclick="uploadFile()">Upload File</button>
async function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('Please select a file');
return;
}
// Create GraphQL mutation
const query = `
mutasi UploadFile($input: UploadFileInput!) {
uploadFile(input: $input) {
id
nama
ukuran
tipe
ekstensi
dibuatPada
}
}
`;
// Prepare form data
const formData = new FormData();
formData.append('operations', JSON.stringify({
query: query,
variables: {
input: {
file: null, // Will be replaced by file
projectId: "your_project_id", // or "your-project-slug"
companyId: "your_company_id" // or "your-company-slug"
}
}
}));
formData.append('map', JSON.stringify({
"0": ["variables.input.file"]
}));
formData.append('0', file);
try {
const response = await fetch('/graphql', {
method: 'POST',
body: formData,
headers: {
// Don't set Content-Type - let browser set it with boundary
'Authorization': 'Bearer your_auth_token'
}
});
const result = await response.json();
if (result.errors) {
console.error('Upload failed:', result.errors);
alert('Upload failed: ' + result.errors[0].message);
} else {
console.log('Upload successful:', result.data.uploadFile);
alert('File uploaded successfully!');
}
} catch (error) {
console.error('Network error:', error);
alert('Network error during upload');
}
}
Multiple Files Upload:
<!-- HTML -->
<input type="file" id="filesInput" multiple />
<button onclick="uploadFiles()">Upload Files</button>
async function uploadFiles() {
const filesInput = document.getElementById('filesInput');
const files = Array.from(filesInput.files);
if (files.length === 0) {
alert('Please select files');
return;
}
if (files.length > 10) {
alert('Maximum 10 files allowed');
return;
}
const query = `
mutasi UploadFiles($input: UploadFilesInput!) {
uploadFiles(input: $input) {
id
nama
ukuran
tipe
ekstensi
dibuatPada
}
}
`;
const formData = new FormData();
// Create file placeholders for variables
const fileVariables = files.map((_, index) => null);
formData.append('operations', JSON.stringify({
query: query,
variables: {
input: {
files: fileVariables,
projectId: "your_project_id", // or "your-project-slug"
companyId: "your_company_id" // or "your-company-slug"
}
}
}));
// Create map for file replacements
const map = {};
files.forEach((_, index) => {
map[index.toString()] = [`variables.input.files.${index}`];
});
formData.append('map', JSON.stringify(map));
// Append actual files
files.forEach((file, index) => {
formData.append(index.toString(), file);
});
try {
const response = await fetch('/graphql', {
method: 'POST',
body: formData,
headers: {
'Authorization': 'Bearer your_auth_token'
}
});
const result = await response.json();
if (result.errors) {
console.error('Upload failed:', result.errors);
alert('Upload failed: ' + result.errors[0].message);
} else {
console.log('Upload successful:', result.data.uploadFiles);
alert(`${result.data.uploadFiles.length} berkas berhasil diunggah!`);
}
} catch (error) {
console.error('Network error:', error);
alert('Network error during upload');
}
}
@@CB##227af489-35a4-4b52-9772-08770ec140db##CB@@bash
# Single file upload with cURL
curl -X POST \
-H "Authorization: Bearer your_auth_token" \
-F 'operations={"query":"mutation UploadFile($input: UploadFileInput!) { uploadFile(input: $input) { id name size type extension createdAt } }","variables":{"input":{"file":null,"projectId":"your_project_id","companyId":"your_company_id"}}}' \
-F 'map={"0":["variables.input.file"]}' \
-F '0=@/path/to/your/file.jpg' \
https://your-api.com/graphql
REST API Upload
Use this method for files larger than 256MB (up to 4.8GB). This approach uses a three-step process: request upload credentials, upload to storage, then register the file in the database.
Prerequisites:
- Python 3.x installed
requests
library installed:pip install requests
- A valid X-Bloo-Token-ID and X-Bloo-Token-Secret for Blue API authentication
- The file to upload (e.g., test.jpg) in the same directory as the script
This method covers two scenarios:
- Uploading to the "File Tab"
- Uploading to the "Todo File Custom Field"
Configuration
Define these constants at the top of your script:
FILENAME = "test.jpg"
TOKEN_ID = "YOUR_TOKEN_ID"
TOKEN_SECRET = "YOUR_TOKEN_SECRET"
COMPANY_ID = "YOUR_COMPANY_ID_OR_SLUG"
PROJECT_ID = "YOUR_PROJECT_ID_OR_SLUG"
BASE_URL = "https://api.blue.cc"\
This is diagram that shows the flow of the upload process:
Uploading to File Tab
::code-group
import requests
import json
import os
# Configuration
FILENAME = "test.jpg"
TOKEN_ID = "YOUR_TOKEN_ID"
TOKEN_SECRET = "YOUR_TOKEN_SECRET"
COMPANY_ID = "YOUR_COMPANY_ID_OR_SLUG"
PROJECT_ID = "YOUR_PROJECT_ID_OR_SLUG"
BASE_URL = "https://api.blue.cc"
# Headers for Blue API
HEADERS = {
"X-Bloo-Token-ID": TOKEN_ID,
"X-Bloo-Token-Secret": TOKEN_SECRET,
"X-Bloo-Company-ID": COMPANY_ID,
"X-Bloo-Project-ID": PROJECT_ID,
}
# Step 1: Get upload credentials
def get_upload_credentials():
url = f"{BASE_URL}/uploads?filename={FILENAME}"
response = requests.get(url, headers=HEADERS)
if response.status_code != 200:
raise Exception(f"Failed to fetch upload credentials: {response.status_code} - {response.text}")
return response.json()
# Step 2: Upload file to S3
def upload_to_s3(credentials):
s3_url = credentials["url"]
fields = credentials["fields"]
files = {
"acl": (None, fields["acl"]),
"Content-Disposition": (None, fields["Content-Disposition"]),
"Key": (None, fields["Key"]),
"X-Key": (None, fields["X-Key"]),
"Content-Type": (None, fields["Content-Type"]),
"bucket": (None, fields["bucket"]),
"X-Amz-Algorithm": (None, fields["X-Amz-Algorithm"]),
"X-Amz-Credential": (None, fields["X-Amz-Credential"]),
"X-Amz-Date": (None, fields["X-Amz-Date"]),
"Policy": (None, fields["Policy"]),
"X-Amz-Signature": (None, fields["X-Amz-Signature"]),
"file": (FILENAME, open(FILENAME, "rb"), fields["Content-Type"])
}
response = requests.post(s3_url, files=files)
if response.status_code != 204:
raise Exception(f"S3 upload failed: {response.status_code} - {response.text}")
print("S3 upload successful")
# Step 3: Register file with Blue
def register_file(file_uid):
graphql_url = f"{BASE_URL}/graphql"
headers = HEADERS.copy()
headers["Content-Type"] = "application/json"
query = """
mutation CreateFile($uid: String!, $name: String!, $type: String!, $extension: String!, $size: Float!, $projectId: String!, $companyId: String!) {
createFile(input: {uid: $uid, name: $name, type: $type, size: $size, extension: $extension, projectId: $projectId, companyId: $companyId}) {
id
uid
name
__typename
}
}
"""
variables = {
"uid": file_uid,
"name": FILENAME,
"type": "image/jpeg",
"extension": "jpg",
"size": float(os.path.getsize(FILENAME)), # Dynamic file size
"projectId": PROJECT_ID,
"companyId": COMPANY_ID
}
payload = {
"operationName": "CreateFile",
"query": query,
"variables": variables
}
response = requests.post(graphql_url, headers=headers, json=payload)
if response.status_code != 200:
raise Exception(f"GraphQL registration failed: {response.status_code} - {response.text}")
print("File registration successful:", response.json())
# Main execution
def main():
try:
if not os.path.exists(FILENAME):
raise Exception(f"File '{FILENAME}' not found")
# Step 1: Fetch credentials
credentials = get_upload_credentials()
print("Upload credentials fetched:", credentials)
# Step 2: Upload to S3
upload_to_s3(credentials)
# Step 3: Register file
file_uid = credentials["fields"]["Key"].split("/")[0]
register_file(file_uid)
print("Upload completed successfully!")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
::
Steps Explained
Step 1: Request Upload Credentials
- Send a GET request to
https://api.blue.cc/uploads?filename=test.jpg@@CODE##0431e7f8-9537-4129-b975-0d46adc04d68##CODE@@files
parameter inrequests.post
to send a multipart/form-data request to the S3 URL - Ensures all fields match the policy exactly, avoiding curl's formatting issues
Step 3: Register File Metadata
- Sends a GraphQL mutation to
https://api.blue.cc/graphql@@CODE##3b7ba14f-cdd0-4cd9-aada-8cbeb7abc8d8##CODE@@os.path.getsize
Sample Response:
@@CB##5faec871-5ced-4126-bc4c-efc482feb130##CB@@
Uploading to Custom Field
::code-group
import requests
import json
import os
# Configuration
FILENAME = "test.jpg"
TOKEN_ID = "YOUR_TOKEN_ID"
TOKEN_SECRET = "YOUR_TOKEN_SECRET"
COMPANY_ID = "YOUR_COMPANY_ID_OR_SLUG"
PROJECT_ID = "YOUR_PROJECT_ID_OR_SLUG"
BASE_URL = "https://api.blue.cc"
# Headers for Blue API
HEADERS = {
"X-Bloo-Token-ID": TOKEN_ID,
"X-Bloo-Token-Secret": TOKEN_SECRET,
"X-Bloo-Company-ID": COMPANY_ID,
"X-Bloo-Project-ID": PROJECT_ID,
}
# Step 1: Get upload credentials
def get_upload_credentials():
url = f"{BASE_URL}/uploads?filename={FILENAME}"
response = requests.get(url, headers=HEADERS)
if response.status_code != 200:
raise Exception(f"Failed to fetch upload credentials: {response.status_code} - {response.text}")
return response.json()
# Step 2: Upload file to S3
def upload_to_s3(credentials):
s3_url = credentials["url"]
fields = credentials["fields"]
files = {
"acl": (None, fields["acl"]),
"Content-Disposition": (None, fields["Content-Disposition"]),
"Key": (None, fields["Key"]),
"X-Key": (None, fields["X-Key"]),
"Content-Type": (None, fields["Content-Type"]),
"bucket": (None, fields["bucket"]),
"X-Amz-Algorithm": (None, fields["X-Amz-Algorithm"]),
"X-Amz-Credential": (None, fields["X-Amz-Credential"]),
"X-Amz-Date": (None, fields["X-Amz-Date"]),
"Policy": (None, fields["Policy"]),
"X-Amz-Signature": (None, fields["X-Amz-Signature"]),
"file": (FILENAME, open(FILENAME, "rb"), fields["Content-Type"])
}
response = requests.post(s3_url, files=files)
if response.status_code != 204:
raise Exception(f"S3 upload failed: {response.status_code} - {response.text}")
print("S3 upload successful")
# Step 3: Register file with Blue
def register_file(file_uid):
graphql_url = f"{BASE_URL}/graphql"
headers = HEADERS.copy()
headers["Content-Type"] = "application/json"
query = """
mutation CreateFile($uid: String!, $name: String!, $type: String!, $extension: String!, $size: Float!, $projectId: String!, $companyId: String!) {
createFile(input: {uid: $uid, name: $name, type: $type, size: $size, extension: $extension, projectId: $projectId, companyId: $companyId}) {
id
uid
name
__typename
}
}
"""
variables = {
"uid": file_uid,
"name": FILENAME,
"type": "image/jpeg",
"extension": "jpg",
"size": float(os.path.getsize(FILENAME)),
"projectId": PROJECT_ID,
"companyId": COMPANY_ID
}
payload = {
"operationName": "CreateFile",
"query": query,
"variables": variables
}
response = requests.post(graphql_url, headers=headers, json=payload)
if response.status_code != 200:
raise Exception(f"GraphQL registration failed: {response.status_code} - {response.text}")
print("File registration successful:", response.json())
return file_uid
# Step 4: Associate file with Todo Custom Field
def associate_file_with_todo(file_uid):
graphql_url = f"{BASE_URL}/graphql"
headers = HEADERS.copy()
headers["Content-Type"] = "application/json"
query = """
mutation CreateTodoCustomFieldFile($input: CreateTodoCustomFieldFileInput!) {
createTodoCustomFieldFile(input: $input)
}
"""
variables = {
"input": {
"todoId": "YOUR_TODO_ID",
"customFieldId": "YOUR_CUSTOM_FIELD_ID",
"fileUid": file_uid
}
}
payload = {
"operationName": "CreateTodoCustomFieldFile",
"query": query,
"variables": variables
}
response = requests.post(graphql_url, headers=headers, json=payload)
if response.status_code != 200:
raise Exception(f"Todo association failed: {response.status_code} - {response.text}")
print("Todo association successful:", response.json())
# Main execution
def main():
try:
if not os.path.exists(FILENAME):
raise Exception(f"File '{FILENAME}' not found")
# Step 1: Fetch credentials
credentials = get_upload_credentials()
print("Upload credentials fetched:", credentials)
# Step 2: Upload to S3
upload_to_s3(credentials)
# Step 3: Register file
file_uid = credentials["fields"]["Key"].split("/")[0]
register_file(file_uid)
# Step 4: Associate with Todo
associate_file_with_todo(file_uid)
print("Upload completed successfully!")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
::
Steps 1-3 are the same as the File Tab process (fetch credentials, upload to S3, and register file metadata).
For step 4, you need to associate the file with a todo custom field.
- Sends a GraphQL mutation to link the file to a todo custom field
- Uses
todoId@@CODE##9ee9a251-4b97-49e3-a57d-f42308ef06d5##CODE@@customFieldId
specific to your setup
Sample Response:
{
"data": {
"createTodoCustomFieldFile": true
}
}