# save_composition_api Tool Specification
**Version**: 1.0.0
**Created**: 2025-07-10
**Purpose**: Save formatted composition to EuConquisto Composer via API with enhanced error reporting
---
## Overview
The `save_composition_api` tool handles the technical aspects of saving a formatted composition to EuConquisto Composer through their API, with detailed error reporting and debugging capabilities to resolve 500 status errors.
## Tool Interface
### Input Schema
```typescript
interface SaveCompositionApiInput {
composerJSON: ComposerComposition;
page: PlaywrightPage; // Browser page with authentication context
}
interface ComposerComposition {
uid: string;
metadata: ComposerMetadata;
structure: ComposerWidget[];
assets: string[];
}
```
### Output Schema
```typescript
interface SaveCompositionApiOutput {
success: boolean;
data?: {
compositionUid: string;
apiResponse: any;
uploadDetails: UploadDetails;
authenticationUsed: AuthenticationDetails;
};
error?: {
code: string;
message: string;
details: ApiError;
};
debug?: {
timestamp: string;
processingTime: number;
apiLog: ApiLogEntry[];
authenticationLog: AuthLogEntry[];
};
}
interface UploadDetails {
fileName: string;
fileSize: number;
connectorUsed: string;
projectUid: string;
apiEndpoint: string;
}
interface AuthenticationDetails {
tokenType: string;
tokenPreview: string; // First 50 characters
projectUid: string;
connectorUid: string;
extractionSuccess: boolean;
}
interface ApiError {
httpStatus: number;
responseText: string;
requestDetails: RequestDetails;
possibleCauses: string[];
suggestedFixes: string[];
}
```
## Authentication Processing
### 1. Authentication Data Extraction
```typescript
async function extractAuthenticationData(page: PlaywrightPage): Promise<AuthenticationData> {
return await page.evaluate(() => {
console.error('=== AUTHENTICATION EXTRACTION v1.0 ===');
// Extract localStorage data
const activeProject = localStorage.getItem('rdp-composer-active-project');
const userData = localStorage.getItem('rdp-composer-user-data');
if (!activeProject || !userData) {
throw new Error('Authentication data not found in localStorage');
}
const projectData = JSON.parse(activeProject);
const userDataParsed = JSON.parse(userData);
const result = {
projectUid: projectData.uid,
connectors: projectData.connectors || [],
accessToken: userDataParsed.access_token,
tokenType: userDataParsed.token_type || 'Bearer'
};
// Validation
if (!result.projectUid) throw new Error('Project UID not found');
if (!result.accessToken) throw new Error('Access token not found');
if (result.connectors.length === 0) throw new Error('No connectors available');
return result;
});
}
```
### 2. Connector Selection Logic
```typescript
function selectOptimalConnector(connectors: Connector[]): Connector {
// Priority 1: ContentManager connector (recommended by tech team)
const contentManager = connectors.find(c =>
c.name && c.name.toLowerCase().includes('contentmanager')
);
if (contentManager) return contentManager;
// Priority 2: Composer-specific connector
const composer = connectors.find(c =>
c.name && c.name.toLowerCase().includes('composer')
);
if (composer) return composer;
// Priority 3: First available connector with upload capability
const uploadCapable = connectors.find(c =>
c.permissions && c.permissions.includes('upload')
);
if (uploadCapable) return uploadCapable;
// Fallback: First connector
if (connectors.length > 0) return connectors[0];
throw new Error('No suitable connector found for composition upload');
}
```
## API Communication
### 1. Request Preparation
```typescript
async function prepareApiRequest(composition: ComposerComposition, auth: AuthenticationData): Promise<RequestData> {
// Create .rdpcomposer file blob
const compositionBlob = new Blob(
[JSON.stringify(composition, null, 2)],
{ type: 'application/json' }
);
// Generate unique filename
const fileName = `composition_${Date.now()}_${composition.metadata.name.replace(/[^a-zA-Z0-9]/g, '_')}.rdpcomposer`;
// Prepare FormData
const formData = new FormData();
formData.append('file', compositionBlob, fileName);
// Select connector
const connector = selectOptimalConnector(auth.connectors);
// Build API endpoint
const apiEndpoint = `https://api.digitalpages.com.br/storage/v1.0/upload/connector/uid/${connector.uid}?manual_project_uid=${auth.projectUid}`;
// Prepare headers
const headers = {
'Authorization': `${auth.tokenType} ${auth.accessToken}`,
'Project-Key': 'e3894d14dbb743d78a7efc5819edc52e',
'Api-Env': 'prd'
};
return {
formData,
headers,
apiEndpoint,
fileName,
fileSize: compositionBlob.size,
connector
};
}
```
### 2. API Call Execution
```typescript
async function executeApiCall(requestData: RequestData, page: PlaywrightPage): Promise<ApiResponse> {
return await page.evaluate(async ({ formData, headers, apiEndpoint }) => {
console.error('=== API CALL EXECUTION v1.0 ===');
console.error('Endpoint:', apiEndpoint);
console.error('Headers:', Object.keys(headers));
try {
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: headers,
body: formData
});
const responseText = await response.text();
console.error('Response Status:', response.status);
console.error('Response Headers:', Object.fromEntries(response.headers.entries()));
console.error('Response Text (first 500 chars):', responseText.substring(0, 500));
if (!response.ok) {
return {
success: false,
status: response.status,
statusText: response.statusText,
responseText: responseText,
error: `HTTP ${response.status}: ${response.statusText}`
};
}
// Parse JSON response
let parsedResponse;
try {
parsedResponse = JSON.parse(responseText);
} catch (e) {
return {
success: false,
status: response.status,
error: 'Invalid JSON response',
responseText: responseText
};
}
return {
success: true,
status: response.status,
data: parsedResponse,
responseText: responseText
};
} catch (error) {
console.error('Fetch Error:', error);
return {
success: false,
error: error.message,
stack: error.stack
};
}
}, requestData);
}
```
### 3. Response Processing
```typescript
function processApiResponse(apiResponse: ApiResponse): ProcessedResponse {
if (!apiResponse.success) {
return {
success: false,
error: {
code: `API_ERROR_${apiResponse.status || 'UNKNOWN'}`,
message: apiResponse.error || 'Unknown API error',
details: {
httpStatus: apiResponse.status,
responseText: apiResponse.responseText,
possibleCauses: analyzePossibleCauses(apiResponse),
suggestedFixes: generateSuggestedFixes(apiResponse)
}
}
};
}
// Extract composition UID from successful response
const compositionUid = extractCompositionUid(apiResponse.data);
if (!compositionUid) {
return {
success: false,
error: {
code: 'UID_EXTRACTION_ERROR',
message: 'Could not extract composition UID from API response',
details: {
responseStructure: Object.keys(apiResponse.data),
fullResponse: apiResponse.data
}
}
};
}
return {
success: true,
compositionUid: compositionUid,
apiResponse: apiResponse.data
};
}
```
## Error Analysis & Debugging
### 1. HTTP Status Code Analysis
```typescript
function analyzePossibleCauses(apiResponse: ApiResponse): string[] {
const status = apiResponse.status;
switch (status) {
case 400:
return [
'Invalid composition JSON structure',
'Missing required form data fields',
'Malformed request parameters'
];
case 401:
return [
'Access token expired or invalid',
'Missing Authorization header',
'Token format incorrect'
];
case 403:
return [
'Insufficient permissions for selected connector',
'Project access denied',
'API key restrictions'
];
case 404:
return [
'Connector UID not found',
'Project UID not found',
'API endpoint incorrect'
];
case 500:
return [
'Composer server internal error',
'Database operation failed',
'File processing error on server',
'Composition data structure incompatible'
];
default:
return ['Unknown error - check API response details'];
}
}
```
### 2. Suggested Fixes Generation
```typescript
function generateSuggestedFixes(apiResponse: ApiResponse): string[] {
const status = apiResponse.status;
switch (status) {
case 401:
return [
'Re-authenticate by refreshing the browser page',
'Check if JWT token file is current',
'Verify localStorage contains valid user data'
];
case 403:
return [
'Try a different connector with upload permissions',
'Verify project access permissions',
'Contact administrator for permission review'
];
case 500:
return [
'Validate composition JSON structure',
'Check for malformed widget content',
'Retry with minimal composition to isolate issue',
'Review quiz questions format (common cause)',
'Check for invalid URLs in assets'
];
default:
return ['Check API documentation for status code details'];
}
}
```
## Composition UID Extraction
### 1. Multiple Response Format Support
```typescript
function extractCompositionUid(apiResponseData: any): string | null {
// Array response format
if (Array.isArray(apiResponseData) && apiResponseData[0]) {
const firstItem = apiResponseData[0];
return firstItem.uid || firstItem.id || firstItem.compositionId || null;
}
// Direct object response
if (typeof apiResponseData === 'object') {
return apiResponseData.uid ||
apiResponseData.id ||
apiResponseData.compositionId ||
apiResponseData.composition_uid ||
null;
}
// String response (direct UID)
if (typeof apiResponseData === 'string' && apiResponseData.length > 10) {
return apiResponseData;
}
return null;
}
```
## Error Codes
- **AUTH_EXTRACTION_ERROR**: Failed to extract authentication from localStorage
- **CONNECTOR_SELECTION_ERROR**: No suitable connector found
- **REQUEST_PREPARATION_ERROR**: Failed to prepare API request
- **API_CALL_ERROR**: API request execution failed
- **UID_EXTRACTION_ERROR**: Could not extract composition UID from response
- **NETWORK_ERROR**: Network connectivity issues
## Debug Output Standards
```typescript
interface ApiLogEntry {
timestamp: string;
action: string;
details: any;
success: boolean;
}
// Example log entries:
[
{
timestamp: '2025-07-10T10:30:00Z',
action: 'AUTH_EXTRACTION',
details: { projectUid: 'abc123', tokenPreview: 'eyJ0eXAi...', connectorsCount: 3 },
success: true
},
{
timestamp: '2025-07-10T10:30:01Z',
action: 'CONNECTOR_SELECTION',
details: { selectedConnector: 'ContentManager', connectorUid: 'def456' },
success: true
},
{
timestamp: '2025-07-10T10:30:02Z',
action: 'API_REQUEST',
details: { endpoint: 'https://api...', fileSize: 125432 },
success: true
}
]
```
## Integration Notes
- **Input**: Formatted composition from `format_for_composer`
- **Output**: Composition UID for `open_composition_editor`
- **Browser Context**: Requires authenticated browser page
- **Error Isolation**: Specific error codes for each failure point
---
**Status**: Specification Complete
**Ready for Implementation**: Yes
**Dependencies**: Authenticated browser page, Composer API access