Skip to content

Bulk Operations

End-to-end flows for bulk delete, bulk update, and bulk export. All bulk operations are asynchronous — they create a job, process it via RabbitMQ, and report progress through SSE events.

Common Pattern

All three bulk operations follow the same async job pattern:

Frontend (Spread)                  Quarkus REST              RabbitMQ              Quarkus Consumer
─────────────────                  ────────────              ────────              ────────────────
User selects rows +
triggers bulk action


NgRx action: bulk{Action}


SelectionBulkEffects
    │ POST /api/data/bulk/
    │   {viewUuid}/{action}

BulkActionResource


BulkActionService

    ├── Create BulkActionJob
    │   (status: PENDING)

    └── Publish to RabbitMQ
        (bulk-actions exchange)


        BulkActionConsumer

            ├── Update job: IN_PROGRESS

            ├── Process rows in batches
            │   │
            │   ├── Execute SQL per batch
            │   │
            │   └── SSE: progress event
            │       { processedRows,
            │         totalRows,
            │         successCount,
            │         errorCount }

            ├── Update job: COMPLETED

            └── SSE: completion event

    ▼                                         ◄─── SSE events stream back ───
Frontend receives SSE

    ├── Progress events
    │   → Update progress bar

    └── Completion event
        → Clear job state,
          reload view data

Response: 202 Accepted (with jobId in response body)

Row Selection Modes

Bulk operations support two selection modes, allowing efficient operations on large datasets:

ModePayloadUse Case
SELECTED_IDS{ rowIds: [1, 2, 3] }User manually selected specific rows
FILTERED{ excludedRowIds: [5] }User selected "all" with current filters, then deselected some

The FILTERED mode is critical for performance — selecting 100,000 rows doesn't send 100,000 IDs. Instead, the backend applies the current filter/sort criteria and excludes only the deselected rows.


Bulk Delete

Deletes multiple rows asynchronously with progress tracking.

POST /api/data/bulk/{viewUuid}/delete

Body (SELECTED_IDS):
{
  "selectionMode": "SELECTED_IDS",
  "rowIds": [1, 2, 3, 4, 5]
}

Body (FILTERED):
{
  "selectionMode": "FILTERED",
  "excludedRowIds": [5],
  "filterRules": [...],
  "sortRules": [...]
}

Response: 202 Accepted
{
  "jobId": "uuid",
  "status": "PENDING",
  "totalRows": 5
}

SSE Events

EventPayloadWhen
data.bulk.delete.progress{ jobId, processedRows, totalRows, successCount, errorCount }After each batch
data.bulk.delete{ jobId, status: "COMPLETED", totalRows, successCount, errorCount }On completion

Frontend State

typescript
// Bulk operation state in ViewState
bulkJobId: string | null;
bulkOperationType: 'delete' | 'update' | 'export' | null;
bulkStatus: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | null;
bulkTotalRows: number;
bulkProcessedRows: number;
bulkSuccessCount: number;
bulkErrorCount: number;
bulkErrors: BulkError[];        // max 100 stored

The SelectionBulkEffects dispatches the API call, stores the jobId, and listens for SSE progress/completion events matching that jobId.


Bulk Update

Updates a field value across multiple rows asynchronously.

POST /api/data/bulk/{viewUuid}/update

Body:
{
  "selectionMode": "SELECTED_IDS",
  "rowIds": [1, 2, 3],
  "updates": {
    "columnUuid": "new-value"
  }
}

Response: 202 Accepted
{
  "jobId": "uuid",
  "status": "PENDING",
  "totalRows": 3
}

SSE Events

EventPayloadWhen
data.bulk.update.progress{ jobId, processedRows, totalRows, successCount, errorCount }After each batch
data.bulk.update{ jobId, status: "COMPLETED", totalRows, successCount, errorCount }On completion

Bulk Export

Exports selected rows to a file asynchronously. The export is processed as a job — the file is generated server-side and made available for download.

POST /api/data/bulk/{viewUuid}/export

Body:
{
  "selectionMode": "FILTERED",
  "excludedRowIds": [],
  "filterRules": [...],
  "format": "csv"
}

Response: 202 Accepted
{
  "jobId": "uuid",
  "status": "PENDING",
  "totalRows": 10000
}

SSE Events

EventPayloadWhen
data.bulk.export.progress{ jobId, processedRows, totalRows }After each batch
data.bulk.export{ jobId, status: "COMPLETED", fileMetadata: { ... } }On completion

Download Flow

After the export job completes:

  1. SSE completion event includes file metadata (name, size, format)
  2. Frontend receives the event and enables the download action
  3. User clicks download (or auto-download triggers)
  4. GET /api/download/{jobId} serves the file
  5. File is streamed from S3 (or local filesystem in dev)

Progress Tracking

All bulk operations track progress with the same fields:

FieldTypeDescription
totalRowsintTotal rows to process
processedRowsintRows processed so far
successCountintRows successfully processed
errorCountintRows that failed
errorsBulkError[]Error details (max 100 stored)

Progress SSE events are sent after each batch is processed, allowing the frontend to show a real-time progress bar.

Error Handling

  • Individual row failures do not abort the entire job — processing continues
  • Errors are collected with the row ID and error message
  • A maximum of 100 errors are stored per job (to prevent unbounded memory usage)
  • The final completion event includes successCount and errorCount
  • If all rows fail, the job status is still COMPLETED (not FAILED) — FAILED is reserved for infrastructure failures (e.g. lost DB connection)

Job Lifecycle

PENDING → IN_PROGRESS → COMPLETED
                      → FAILED (infrastructure error only)

The BulkActionJob entity tracks the full lifecycle. Jobs are persisted in the metadata database and can be queried for status after the fact.


Bulk vs Batch vs Single

SingleBatchBulk
ExampleEdit one cellFill column for selected rowsDelete 10,000 rows
ProcessingSyncSyncAsync (RabbitMQ)
Response200200202
ProgressInstantInstantSSE events
PageData OperationsData OperationsThis page

Response Code Summary

OperationMethodSuccess CodeNotes
Bulk DeletePOST202 AcceptedReturns jobId
Bulk UpdatePOST202 AcceptedReturns jobId
Bulk ExportPOST202 AcceptedReturns jobId
Download ExportGET200 OKStreams file

Cross-References

SchemaStack Internal Developer Documentation