Skip to main content

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 export
  • GET /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 sessions
  • cleanupExportFiles - Delete old export files
  • cleanupPrintFiles - 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:

ParameterTypeRequiredDescription
dimpathDimension code (PRD, CLI, ORD)
sourcequerySecurity area
$filterqueryFilter exported records
$selectquerySelect specific fields
$orderquerySort 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:

  1. Fetches all sessions from TB_ANAG_SESSIONS00 where TREC != 'P'
  2. Calculates max life: XSESSIONS05 + JWT_INACTIVITY
  3. Deletes sessions where maxLife < now
  4. 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:

  1. Use GET /api/v4/export/{dim} for data exports
  2. Poll GET /api/v4/export/status/{resourceId} for export status
  3. Use POST /api/v4/core/command/{commandName} for system commands
  4. Commands execute Symfony Console Commands via HTTP
  5. Always handle async operations with polling
  6. Provide user feedback during long-running operations
  7. Commands require appropriate permissions (admin for maintenance)

Next: Batch Operations →