Command Operations
Overview
Command operations execute special business logic that doesn't fit the standard CRUD pattern. These include data exports, system maintenance tasks, background processing, and custom workflows.
Key Characteristics:
- Special-purpose operations beyond CRUD
- Can be long-running or background processes
- Execute Symfony Console Commands via HTTP API
- Include export, import, maintenance, and custom logic
- Often asynchronous with status endpoints
- Return structured logs and execution results
Command Types
1. Export Operations
Purpose: Generate data exports (CSV, Excel, PDF) from dimensions
Endpoints:
GET /api/v4/export/{dim}- Trigger exportGET /api/v4/export/status/{resourceId}- Check export status
Use Cases:
- Product catalog export
- Customer data export
- Order history reports
- Bulk data extraction
2. System Maintenance
Purpose: Perform system cleanup and maintenance tasks
Endpoint: POST /api/v4/core/command/{commandName}
Examples:
cleanExpiredSessions- Remove expired JWT sessionscleanupExportFiles- Delete old export filescleanupPrintFiles- Delete old print jobs
3. Custom Business Logic
Purpose: Execute dimension-specific or tenant-specific workflows
Endpoint: POST /api/v4/core/command/{commandName}
Use Cases:
- Data transformations
- Batch calculations
- External system integrations
- Complex validations
Export Operations
Trigger Export
Endpoint: GET /api/v4/export/{dim}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
dim | path | ✅ | Dimension code (PRD, CLI, ORD) |
source | query | ✅ | Security area |
$filter | query | ❌ | Filter exported records |
$select | query | ❌ | Select specific fields |
$order | query | ❌ | Sort export data |
Example:
curl -X GET "https://coreservice.q01.io/api/v4/export/PRD?source=productExport&\$filter=XPRD06 eq true&\$order=XPRD01 ASC" \
-H "Authorization: Bearer eyJhbGc..."
Response:
{
"resourceId": "exp_20251219_abc123",
"status": "processing",
"createdAt": "2025-12-19T14:30:00Z",
"estimatedCompletion": "2025-12-19T14:31:00Z"
}
resourceId: Unique identifier for tracking export status
Check Export Status
Endpoint: GET /api/v4/export/status/{resourceId}
Example:
curl -X GET "https://coreservice.q01.io/api/v4/export/status/exp_20251219_abc123" \
-H "Authorization: Bearer eyJhbGc..."
Response (Processing):
{
"resourceId": "exp_20251219_abc123",
"status": "processing",
"progress": 45,
"totalRecords": 10000,
"processedRecords": 4500,
"createdAt": "2025-12-19T14:30:00Z"
}
Response (Completed):
{
"resourceId": "exp_20251219_abc123",
"status": "completed",
"downloadUrl": "https://storage.googleapis.com/q01-exports/tenant123/export_PRD_20251219.csv",
"fileSize": 2048576,
"recordCount": 10000,
"completedAt": "2025-12-19T14:31:15Z",
"expiresAt": "2025-12-20T14:31:15Z"
}
Response (Failed):
{
"resourceId": "exp_20251219_abc123",
"status": "failed",
"error": "Export failed: Database connection timeout",
"failedAt": "2025-12-19T14:30:45Z"
}
JavaScript Export Client
class ExportManager {
constructor(apiBase, token) {
this.apiBase = apiBase;
this.token = token;
this.pollInterval = 2000; // 2 seconds
}
async triggerExport(dimension, filters = {}) {
const url = new URL(`${this.apiBase}/api/v4/export/${dimension}`);
url.searchParams.append('source', filters.source || 'export');
if (filters.$filter) {
url.searchParams.append('$filter', filters.$filter);
}
if (filters.$select) {
url.searchParams.append('$select', filters.$select);
}
if (filters.$order) {
url.searchParams.append('$order', filters.$order);
}
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${this.token}` }
});
if (!response.ok) {
throw new Error('Export trigger failed');
}
const result = await response.json();
return result.resourceId;
}
async getExportStatus(resourceId) {
const response = await fetch(
`${this.apiBase}/api/v4/export/status/${resourceId}`,
{
headers: { 'Authorization': `Bearer ${this.token}` }
}
);
if (!response.ok) {
throw new Error('Status check failed');
}
return response.json();
}
async pollExport(resourceId, onProgress) {
return new Promise((resolve, reject) => {
const poll = async () => {
try {
const status = await this.getExportStatus(resourceId);
if (onProgress) {
onProgress(status);
}
if (status.status === 'completed') {
resolve(status);
} else if (status.status === 'failed') {
reject(new Error(status.error));
} else {
setTimeout(poll, this.pollInterval);
}
} catch (error) {
reject(error);
}
};
poll();
});
}
async exportAndDownload(dimension, filters = {}) {
// Trigger export
const resourceId = await this.triggerExport(dimension, filters);
// Poll until complete
const result = await this.pollExport(resourceId, (status) => {
console.log(`Export progress: ${status.progress || 0}%`);
});
// Download file
window.location.href = result.downloadUrl;
return result;
}
}
// Usage
const exporter = new ExportManager(apiBase, token);
async function exportProducts() {
try {
const result = await exporter.exportAndDownload('PRD', {
source: 'productExport',
$filter: 'XPRD06 eq true',
$order: 'XPRD01 ASC'
});
console.log(`Export completed: ${result.recordCount} records`);
} catch (error) {
console.error('Export failed:', error);
}
}
System Maintenance Commands
Clean Expired Sessions
Endpoint: POST /api/v4/core/command/cleanExpiredSessions
Purpose: Remove expired JWT sessions from TB_ANAG_SESSIONS00
Authentication: Required (admin privileges)
Request:
POST /api/v4/core/command/cleanExpiredSessions
Content-Type: application/json
Authorization: Bearer {token}
{
"source": "systemMaintenance"
}
Response:
{
"status": 200,
"log": "cleanExpiredSessions successfully executed",
"full_message": "CoreApi Expired Sessions Cleanup\n[OK] 15 expired sessions deleted from server!\n[NOTE] There are currently 42 active sessions"
}
How It Works:
- Fetches all sessions from TB_ANAG_SESSIONS00 where TREC != 'P'
- Calculates max life:
XSESSIONS05 + JWT_INACTIVITY - Deletes sessions where
maxLife < now - Returns count of deleted and active sessions
Environment Variables:
JWT_INACTIVITY: Session inactivity timeout (seconds)
Implementation Details
CoreCommandController.php:32-66
public function executeCommand(Request $request, KernelInterface $kernel)
{
$commandName = $routeArgs['commandName'];
$data_form = json_decode($request->getContent(), true);
// Build command name with optional package prefix
$cmdPkg = isset($data_form['cmdPkg']) ? $data_form['cmdPkg'] . ":" : "";
$data_form['command'] = $cmdPkg . $commandName;
// Execute Symfony Console Command via HTTP
$application = new Application($kernel);
$application->setAutoExit(false);
$input = new ArrayInput($data_form);
$output = new BufferedOutput();
$returnCode = $application->run($input, $output);
// Return command output
$content = $output->fetch();
return json([
"status" => 200,
"log" => "$commandName successfully executed",
"full_message" => rtrim($content)
]);
}
Custom Command Pattern
Creating Custom Commands
1. Define Symfony Console Command:
<?php
namespace App\Command\Custom;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MyCustomCommand extends Command
{
protected $commandName = "myPkg:myCustomCommand";
protected $commandDescription = "Execute custom business logic";
protected function configure()
{
$this
->setName($this->commandName)
->setDescription($this->commandDescription);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// Custom logic here
$output->writeln("Executing custom command...");
// Return status code
return Command::SUCCESS;
}
}
2. Register Command:
# config/services.yaml
services:
App\Command\Custom\MyCustomCommand:
tags:
- { name: 'console.command' }
3. Expose via HTTP:
POST /api/v4/core/command/myCustomCommand
Content-Type: application/json
Authorization: Bearer {token}
{
"cmdPkg": "myPkg",
"source": "customOperation"
}
4. Response:
{
"status": 200,
"log": "myCustomCommand successfully executed",
"full_message": "Executing custom command...\nCompleted successfully."
}
Best Practices
✅ DO
Use polling for long-running exports:
const result = await exporter.pollExport(resourceId, (status) => {
updateProgressBar(status.progress);
});
Set reasonable poll intervals:
// ✅ Good - 2-5 seconds
this.pollInterval = 2000;
// ❌ Bad - too frequent
this.pollInterval = 100;
Handle export expiry:
const status = await exporter.getExportStatus(resourceId);
if (status.expiresAt < new Date()) {
// Re-trigger export
resourceId = await exporter.triggerExport(dimension, filters);
}
Provide user feedback:
await exporter.pollExport(resourceId, (status) => {
showNotification(`Processing: ${status.processedRecords}/${status.totalRecords}`);
});
❌ DON'T
Don't poll completed exports:
// ❌ Wrong - wastes resources
setInterval(() => checkStatus(resourceId), 2000);
// ✅ Correct - stop polling when complete
if (status.status === 'completed') {
clearInterval(pollTimer);
}
Don't ignore export failures:
// ❌ Wrong - silent failure
const status = await getExportStatus(resourceId);
// ✅ Correct - handle errors
if (status.status === 'failed') {
showError(status.error);
logError('Export failed', { resourceId, error: status.error });
}
Don't trigger duplicate exports:
// ❌ Wrong - multiple concurrent exports
onClick={() => triggerExport());
// ✅ Correct - prevent duplicates
if (!this.exportInProgress) {
this.exportInProgress = true;
await triggerExport();
this.exportInProgress = false;
}
Error Handling
Export Errors
Timeout:
{
"resourceId": "exp_20251219_abc123",
"status": "failed",
"error": "Export timeout: Processing exceeded 5 minutes",
"failedAt": "2025-12-19T14:35:00Z"
}
Permission Denied:
{
"code": 403,
"message": "Insufficient permissions for export operation",
"required_grant": 3
}
Invalid Filters:
{
"code": 400,
"message": "Invalid $filter syntax",
"errors": [
{
"field": "$filter",
"message": "Unexpected token 'XPRD_INVALID'"
}
]
}
Command Errors
Command Not Found:
{
"code": 404,
"message": "Command not found: invalidCommand"
}
Execution Failed:
{
"status": 500,
"log": "cleanExpiredSessions execution failed",
"full_message": "Database connection error: Connection refused"
}
Use Case: Product Catalog Export
class ProductCatalogExporter {
constructor(apiBase, token) {
this.exporter = new ExportManager(apiBase, token);
}
async exportActiveProducts(categoryId = null) {
// Build filter
let filter = 'XPRD06 eq true';
if (categoryId) {
filter += ` AND XPRD05 eq '${categoryId}'`;
}
// Trigger export with progress UI
const progressModal = this.showProgressModal();
try {
const resourceId = await this.exporter.triggerExport('PRD', {
source: 'productExport',
$filter: filter,
$select: 'PRD_ID,XPRD01,XPRD02,XPRD05',
$order: 'XPRD01 ASC'
});
// Poll with progress updates
const result = await this.exporter.pollExport(resourceId, (status) => {
progressModal.update({
progress: status.progress || 0,
message: `Processing ${status.processedRecords || 0} of ${status.totalRecords || 0} products...`
});
});
// Download completed export
progressModal.hide();
this.downloadFile(result.downloadUrl, 'products.csv');
return result;
} catch (error) {
progressModal.hide();
this.showError(`Export failed: ${error.message}`);
throw error;
}
}
showProgressModal() {
// Create progress modal
const modal = document.createElement('div');
modal.className = 'export-progress-modal';
modal.innerHTML = `
<div class="progress-content">
<h3>Exporting Products</h3>
<div class="progress-bar">
<div class="progress-fill" style="width: 0%"></div>
</div>
<p class="progress-message">Preparing export...</p>
</div>
`;
document.body.appendChild(modal);
return {
update: ({ progress, message }) => {
modal.querySelector('.progress-fill').style.width = `${progress}%`;
modal.querySelector('.progress-message').textContent = message;
},
hide: () => modal.remove()
};
}
downloadFile(url, filename) {
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
showError(message) {
alert(message);
}
}
Summary
Command operations provide:
- ✅ Data export with async processing
- ✅ System maintenance tasks
- ✅ Custom business logic execution
- ✅ Status polling for long-running operations
- ✅ Structured command output
- ✅ Symfony Console integration via HTTP
Key Takeaways:
- Use
GET /api/v4/export/{dim}for data exports - Poll
GET /api/v4/export/status/{resourceId}for export status - Use
POST /api/v4/core/command/{commandName}for system commands - Commands execute Symfony Console Commands via HTTP
- Always handle async operations with polling
- Provide user feedback during long-running operations
- Commands require appropriate permissions (admin for maintenance)
Next: Batch Operations →
Related Concepts
- Read Operations - Filters apply to exports
- Permissions - Command authorization
- Write Operations - Background processing patterns