@ea-lab/reactive-json-docs
Version:
Complete documentation for Reactive-JSON - Components, examples and LLM-parsable guides
374 lines (299 loc) • 11.5 kB
Markdown
# Data Processors
Data Processors are a powerful feature in Reactive-JSON that allow you to intercept and modify data received via `fetchData`, `submitData`, and `additionalDataSources`. This enables you to implement data transformation, validation, security filtering, and other data processing logic in a centralized and reusable way.
## How Data Processors Work
When Reactive-JSON receives data from HTTP requests, it automatically passes the data through all registered Data Processors in order. Each processor:
1. **Examines the request and response context** (URL, method, headers, status, etc.)
2. **Receives the current data** being processed
3. **Must return the data** - either transformed or unchanged
4. **Chains with other processors** (processed data flows to the next processor)
## Basic Structure
A Data Processor is a function that receives context and data, then **must return** the processed data:
```javascript
const myDataProcessor = ({ requestContext, responseContext, dataToProcess, originalDataToProcess }) => {
// Check if we want to process this data
if (!requestContext.url.includes('/api/users')) {
return dataToProcess; // Return unchanged data
}
// Check response status
if (responseContext.status >= 400) {
return dataToProcess; // Return unchanged data
}
// Transform the data
const processedData = {
...dataToProcess,
timestamp: new Date().toISOString(),
source: requestContext.url
};
return processedData; // Return transformed data
};
```
## Function Parameters
### requestContext
Information about the HTTP request:
- `url`: The URL that was called
- `method`: HTTP method (GET, POST, etc.)
- `headers`: Request headers object
- `body`: Request body (for POST, PUT, etc.)
### responseContext
Information about the HTTP response:
- `headers`: Response headers object
- `status`: HTTP status code (200, 404, etc.)
- `data`: Raw response data
### dataToProcess
The data currently being processed. This may have been modified by previous Data Processors in the chain.
### originalDataToProcess
The original data before any processing, useful for comparison or logging.
## Plugin Registration
Data Processors are registered through the plugin system:
```javascript
import { mergeComponentCollections } from "@ea-lab/reactive-json";
const myPlugins = {
element: {
// Your custom components
},
dataProcessor: {
"timestamp-processor": {
callback: timestampProcessor,
order: 0
},
"security-filter": {
callback: securityProcessor,
order: 10
}
}
};
export const MyReactiveJsonRoot = (props) => {
const plugins = mergeComponentCollections([myPlugins]);
return <ReactiveJsonRoot {...props} plugins={plugins} />;
};
```
### Plugin Structure
- **Key**: Unique identifier for the processor
- **callback**: The processor function
- **order**: Execution order (lower numbers run first)
## Common Use Cases
### Adding Timestamps
```javascript
const timestampProcessor = ({ requestContext, responseContext, dataToProcess, originalDataToProcess }) => {
if (typeof dataToProcess !== 'object' || dataToProcess === null) {
return dataToProcess; // Return unchanged for non-objects
}
return {
...dataToProcess,
__metadata: {
processedAt: new Date().toISOString(),
sourceUrl: requestContext.url,
responseStatus: responseContext.status
}
};
};
```
### Security Filtering
```javascript
const securityProcessor = ({ requestContext, responseContext, dataToProcess, originalDataToProcess }) => {
// Only filter external API responses
if (!requestContext.url.includes('external-api')) {
return dataToProcess; // Return unchanged
}
if (typeof dataToProcess !== 'object' || dataToProcess === null) {
return dataToProcess; // Return unchanged for non-objects
}
// Remove sensitive fields
const sensitiveFields = ['password', 'secret', 'apiKey', 'token'];
const cleanedData = { ...dataToProcess };
sensitiveFields.forEach(field => {
if (cleanedData.hasOwnProperty(field)) {
delete cleanedData[field];
console.log(`🔒 Removed sensitive field: ${field}`);
}
});
return cleanedData;
};
```
### Data Format Transformation
```javascript
const dateFormatProcessor = ({ requestContext, responseContext, dataToProcess, originalDataToProcess }) => {
if (typeof dataToProcess !== 'object' || dataToProcess === null) {
return dataToProcess; // Return unchanged for non-objects
}
const processedData = { ...dataToProcess };
// Convert DD/MM/YYYY dates to YYYY-MM-DD
Object.keys(processedData).forEach(key => {
if (typeof processedData[key] === 'string' && /^\d{2}\/\d{2}\/\d{4}$/.test(processedData[key])) {
const [day, month, year] = processedData[key].split('/');
processedData[key] = `${year}-${month}-${day}`;
}
});
return processedData;
};
```
### Conditional Processing
```javascript
const userDataProcessor = ({ requestContext, responseContext, dataToProcess, originalDataToProcess }) => {
// Only process successful user API responses
if (!requestContext.url.includes('/api/users') || responseContext.status !== 200) {
return dataToProcess; // Return unchanged
}
if (typeof dataToProcess !== 'object' || dataToProcess === null) {
return dataToProcess; // Return unchanged for non-objects
}
// Add user-specific processing
return {
...dataToProcess,
fullName: `${dataToProcess.firstName} ${dataToProcess.lastName}`,
isActive: dataToProcess.status === 'active',
permissions: dataToProcess.role === 'admin' ? ['read', 'write', 'delete'] : ['read']
};
};
```
## RjBuild vs Data Processing
The system automatically detects whether the response is a complete RjBuild or just data:
- **Complete RjBuild**: Processors only modify the `data` section
- **Data only**: Processors modify the entire response
This happens automatically based on the `updateOnlyData` parameter in `fetchData`/`submitData`.
## Execution Context
### With `fetchData`/`submitData`
```yaml
- type: button
content: "Load User Data"
actions:
- what: fetchData
on: click
url: "/api/users/123"
refreshAppOnResponse: true
updateOnlyData: false # The response is expected to be a complete RjBuild.
# When the response is a complete RjBuild, the processors will receive the data only.
```
### With `additionalDataSources`
```yaml
additionalDataSource:
- src: "/api/config"
path: "~~.config"
# Processors will automatically process this data
```
## Best Practices
### Always Return Data
Data Processors **must always return data**. To skip processing, return the original data:
```javascript
const myProcessor = ({ requestContext, responseContext, dataToProcess, originalDataToProcess }) => {
// Skip processing for certain conditions
if (!requestContext.url.includes('/api/')) {
return dataToProcess; // Return unchanged
}
if (responseContext.status >= 400) {
return dataToProcess; // Return unchanged
}
if (typeof dataToProcess !== 'object') {
return dataToProcess; // Return unchanged
}
// Process and return transformed data
return { ...dataToProcess, processed: true };
};
```
### Immutable Updates
Always clone data before modifying:
```javascript
const processedData = { ...dataToProcess }; // Shallow clone
// Or for deep cloning, use JSON.parse(JSON.stringify(...)):
const processedData = JSON.parse(JSON.stringify(dataToProcess));
// Or with lodash:
const processedData = _.cloneDeep(dataToProcess);
```
### Error Handling
The system automatically catches errors, but you can add your own:
```javascript
const safeProcessor = ({ requestContext, responseContext, dataToProcess, originalDataToProcess }) => {
try {
// Your processing logic
return processedData;
} catch (error) {
console.error('Processing failed:', error);
return dataToProcess; // Return original data on error
}
};
```
### Descriptive IDs and Ordering
Use clear names and logical ordering:
```javascript
const plugins = {
dataProcessor: {
"01-timestamp": { callback: timestampProcessor, order: 0 },
"02-security": { callback: securityProcessor, order: 10 },
"03-formatting": { callback: formatProcessor, order: 20 }
}
};
```
## Debugging
### Logging
Add console logs to understand the flow:
```javascript
const debugProcessor = ({ requestContext, responseContext, dataToProcess, originalDataToProcess }) => {
console.log('🔍 Processing:', requestContext.url);
console.log('📊 Status:', responseContext.status);
console.log('📥 Data keys:', Object.keys(dataToProcess));
// Your processing logic
const processedData = { ...dataToProcess, debug: true };
console.log('📤 Processed keys:', Object.keys(processedData));
return processedData;
};
```
### Browser DevTools
The system automatically logs processor errors to the console, making debugging straightforward.
## Complete Example
Here's a comprehensive example showing multiple processors working together:
```javascript
import { mergeComponentCollections } from "@ea-lab/reactive-json";
// Utility processors
const timestampProcessor = ({ requestContext, responseContext, dataToProcess, originalDataToProcess }) => {
if (typeof dataToProcess !== 'object' || dataToProcess === null) {
return dataToProcess;
}
return {
...dataToProcess,
__processed: {
timestamp: new Date().toISOString(),
url: requestContext.url,
status: responseContext.status
}
};
};
const securityProcessor = ({ requestContext, responseContext, dataToProcess, originalDataToProcess }) => {
if (!requestContext.url.includes('external')) {
return dataToProcess;
}
if (typeof dataToProcess !== 'object' || dataToProcess === null) {
return dataToProcess;
}
const cleaned = { ...dataToProcess };
['password', 'secret', 'token'].forEach(field => delete cleaned[field]);
return cleaned;
};
const userDataProcessor = ({ requestContext, responseContext, dataToProcess, originalDataToProcess }) => {
if (!requestContext.url.includes('/users')) {
return dataToProcess;
}
if (typeof dataToProcess !== 'object' || dataToProcess === null) {
return dataToProcess;
}
return {
...dataToProcess,
displayName: `${dataToProcess.firstName} ${dataToProcess.lastName}`,
isOnline: dataToProcess.lastSeen &&
(Date.now() - new Date(dataToProcess.lastSeen).getTime()) < 300000 // 5 minutes
};
};
const myPlugins = {
element: {
// Your components
},
dataProcessor: {
"timestamp": { callback: timestampProcessor, order: 0 },
"security": { callback: securityProcessor, order: 5 },
"user-enhancement": { callback: userDataProcessor, order: 10 }
}
};
export const MyApp = (props) => {
const plugins = mergeComponentCollections([myPlugins]);
return <ReactiveJsonRoot {...props} plugins={plugins} />;
};
```