ui5-excel-import
v1.1.0
Published
Reusable Excel Import Library for SAP UI5 - Production-ready with error handling, validation, and comprehensive testing
Maintainers
Readme
UI5 Excel Import Library
Production-ready, reusable Excel import library for SAP UI5 applications.
Status: ✅ Fully tested and validated | 🛡️ Comprehensive error handling | 📊 80%+ code coverage
Features
- ✅ Robust Excel Parsing - Full validation and error handling
- ✅ Column Mapping - Flexible alias-based column detection
- ✅ Batch Processing - Efficient handling of large datasets
- ✅ Error Handling - Comprehensive error messages and validation
- ✅ Type Safe - Full JSDoc documentation
- ✅ Well Tested - 80%+ code coverage with Jest
- ✅ Zero External Dependencies - XLSX library bundled
Quick Start
Installation
Option 1: Git Submodule (Current)
git submodule add https://github.com/wusxo24/ui5-excel-import webapp/lib/excelImportOption 2: npm (Coming Soon)
npm install ui5-excel-importBasic Usage
sap.ui.define([
"your/lib/excelImport/ExcelImportFacade"
], function (ExcelImport) {
"use strict";
// Parse Excel file
const result = await ExcelImport.parseOnly({
file: excelFile,
columns: {
companyCode: ["Company Code", "Bukrs"],
account: ["Account", "HKONT"],
amount: ["Amount", "WRBTR"],
currency: ["Currency", "WAERS"]
}
});
if (result.errors.length > 0) {
console.error("Parse errors:", result.errors);
} else {
console.log(`Processed ${result.rowsProcessed} rows`);
console.log(result.data);
}
});Advanced: Parse and Post
// Complete workflow: parse, transform, and post data
const responses = await ExcelImport.parseAndPost({
file: excelFile,
columns: ColumnConfig,
mapper: PayloadMapper,
uploadUrl: "/api/import",
fileName: "employees.xlsx",
testMode: false,
batchSize: 50
});
responses.forEach((response, index) => {
console.log(`Batch ${index + 1}:`, response);
});API Reference
ExcelImportFacade
parseOnly(options)
Parses Excel file and maps columns without posting data.
Parameters:
options.file(File): Excel file to parse [required]options.columns(Object): Column mapping configuration [required]
Returns:
{
data: Array<Object>, // Parsed and mapped rows
errors: Array<String>, // Error messages
rowsProcessed: Number // Total rows processed
}Example:
const result = await ExcelImport.parseOnly({
file: fileInput.files[0],
columns: {
companyCode: ["Company Code", "Bukrs"],
account: ["Account", "HKONT"]
}
});parseAndPost(options)
Parses Excel, transforms data, and posts in batches.
Parameters:
options.file(File): Excel file [required]options.columns(Object): Column mapping [required]options.mapper(Object): Object withmap()function [required]options.uploadUrl(String): API endpoint [required]options.fileName(String): File name for request [required]options.testMode(Boolean): Test mode flag (optional, default: false)options.batchSize(Number): Items per batch (optional, default: 50)
Returns: Promise<Array<Object>> - API responses for each batch
Example:
const responses = await ExcelImport.parseAndPost({
file: excelFile,
columns: config,
mapper: {
map: (data) => data.map(row => ({
bukrs: row.companyCode,
hkont: row.account,
wrbtr: row.amount
}))
},
uploadUrl: "/api/import",
fileName: "data.xlsx",
batchSize: 100
});Configuration
Column Mapping (ColumnConfig)
Maps Excel columns to internal field names using aliases.
// template/ColumnConfig.js
sap.ui.define([], function () {
return {
// Field name: [possible column headers]
companyCode: ["Company Code", "Bukrs", "Company"],
account: ["Account", "HKONT", "Acct"],
amount: ["Amount", "WRBTR", "Sum"],
currency: ["Currency", "WAERS", "Curr"]
};
});Data Mapper (PayloadMapper)
Transforms mapped rows to API payload format.
// template/PayloadMapper.js
sap.ui.define([], function () {
return {
map: function(data) {
return data.map(row => ({
bukrs: row.companyCode,
hkont: row.account,
wrbtr: row.amount,
waers: row.currency
}));
}
};
});Error Handling
The library provides comprehensive error messages:
try {
const result = await ExcelImport.parseOnly({
file: null,
columns: {}
});
} catch (error) {
// Error: "File is required for parseOnly operation"
// Error: "Column configuration cannot be empty"
console.error(error.message);
}Common errors:
File is required- No file providedColumn configuration is required- Column config missingFile must be an Excel file- Invalid file formatExcel sheet is empty- No data in sheetUpload URL must be a non-empty string- Invalid URLRequest timeout after 30000ms- API timeout
Architecture
Module Breakdown
| Module | Purpose | |--------|---------| | ExcelImportFacade | Main public API (parseOnly, parseAndPost, postOnly) | | ExcelReader | Reads and parses Excel files with validation | | ColumnDetector | Maps Excel columns based on configuration | | BatchProcessor | Splits data into batches for processing | | ApiClient | HTTP POST client with timeout handling |
Module Dependencies
ExcelImportFacade
├── ExcelReader (reads Excel files)
├── ColumnDetector (detects columns)
├── BatchProcessor (creates batches)
└── ApiClient (posts data)Backend Integration
Architecture Pattern
This library handles generic Excel import logic. Domain-specific integration (validation, transformation, API format) belongs in your application.
Library (Reusable) Your Application (Specific)
───────────────────────────── ─────────────────────────────
ExcelImportFacade YourController.js
├── ExcelReader YourValidator.js (validate fields)
├── ColumnDetector YourMapper.js (transform data)
├── BatchProcessor YourAdapter.js (format for API)
└── ApiClient ────────────────→ Your Backend APIIntegration Example: SAP Finance Document Posting
Below is a real-world example of integrating with an ABAP HTTP service that expects specific JSON structure.
Step 1: Company-Specific Validator
// your/helper/FinanceValidator.js
sap.ui.define([], function () {
"use strict";
const _validateDate = (dateStr) => {
// Validate DD/MM/YYYY format and convert to YYYYMMDD
const regex = /^(\d{2})\/(\d{2})\/(\d{4})$/;
const match = dateStr.match(regex);
if (!match) return null;
return `${match[3]}${match[2]}${match[1]}`;
};
return {
/**
* Validate and transform a single row
* @param {Object} row - Row from Excel
* @param {Number} rowIndex - Row number for error reporting
* @returns {Object} Validated/transformed row
* @throws {Error} If validation fails
*/
validateRow: function(row, rowIndex) {
// Required field checks
if (!row.documentdate) {
throw new Error(`Row ${rowIndex}: Document Date is required`);
}
if (!row.account) {
throw new Error(`Row ${rowIndex}: Account is required`);
}
// Format validation
const documentdate = _validateDate(row.documentdate);
if (!documentdate) {
throw new Error(`Row ${rowIndex}: Invalid date format (use DD/MM/YYYY)`);
}
// Business logic: Currency handling
if (row.currency === "VND") {
// For VND: copy document currency to local currency
if (!row.amountinlocalcurrency && row.amountindoumentcurrency) {
row.amountinlocalcurrency = row.amountindoumentcurrency;
}
} else {
// For foreign currency: auto-calculate local currency
if (row.exchangerate && row.amountindoumentcurrency) {
row.amountinlocalcurrency = Math.round(
row.amountindoumentcurrency * row.exchangerate
);
}
}
return {
...row,
documentdate: documentdate,
postingdate: _validateDate(row.postingdate)
};
},
/**
* Validate entire dataset
* @param {Array} rows - All parsed rows
* @returns {Object} { data: validated rows, errors: error messages }
*/
validate: function(rows) {
const validatedData = [];
const errors = [];
rows.forEach((row, index) => {
try {
validatedData.push(this.validateRow(row, index + 3)); // Row numbering starts at 3
} catch (error) {
errors.push({
type: "Error",
message: error.message,
group: `Row ${index + 3}`
});
}
});
return {
data: validatedData,
errors: errors,
hasErrors: errors.length > 0
};
}
};
});Step 2: Backend Request Mapper
// your/helper/AbapPayloadMapper.js
sap.ui.define([], function () {
"use strict";
return {
/**
* Transform validated rows to ABAP service format
* Maps camelCase (UI5) → field names expected by ABAP
*
* @param {Array} rows - Validated rows from FinanceValidator
* @param {Object} metadata - File metadata { filename, isUpdate, testMode }
* @returns {Object} Request payload for ABAP service
*/
mapToAbapFormat: function(rows, metadata) {
// Group rows by document ID
const groupedByDocId = new Map();
rows.forEach(row => {
const docId = row.id_doc || row.idDoc;
if (!groupedByDocId.has(docId)) {
groupedByDocId.set(docId, []);
}
groupedByDocId.get(docId).push(row);
});
// Build documents array
const documents = [];
groupedByDocId.forEach((items, idDoc) => {
const headerRow = items[0];
documents.push({
filename: metadata.filename,
idDoc: idDoc,
companycode: headerRow.companycode,
documentdate: headerRow.documentdate, // YYYYMMDD
postingdate: headerRow.postingdate, // YYYYMMDD
documenttype: headerRow.documenttype,
currency: headerRow.currency,
headertext: headerRow.headertext,
toItem: items.map((item, idx) => ({
idline: `${idx + 1}`,
postingkey: item.postingkey,
account: item.account,
amountinlocalcurrency: item.amountinlocalcurrency,
transactioncurrency: item.currency,
amountindoumentcurrency: item.amountindoumentcurrency,
assignment: item.assignment || "",
costcenter: item.costcenter || "",
itemtext: item.itemtext || ""
}))
});
});
// Return request matching ABAP ts_post_request
return {
isupdate: metadata.isUpdate ? true : false,
testmode: metadata.testMode ? true : false,
filename: metadata.filename,
doc: documents
};
}
};
});Step 3: Integration in Controller
// your/controller/FinanceImport.controller.js
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/m/MessageBox",
"your/lib/excelImport/ExcelImportFacade",
"your/template/ColumnConfig",
"your/helper/FinanceValidator",
"your/helper/AbapPayloadMapper",
"your/helper/ResultsDialog"
], function (Controller, MessageBox, ExcelImport, ColumnConfig, Validator, Mapper, ResultsDialog) {
"use strict";
const API_URL = "/sap/bc/http/sap/ZFI_API_UPLOAD_FIDOC";
const HTTP_TIMEOUT = 60000; // 60 seconds
return Controller.extend("your.app.controller.FinanceImport", {
onImportPress: async function () {
const file = document.getElementById("fileInput").files[0];
if (!file) return;
this._showBusy("Processing Excel...");
try {
// Step 1: Parse Excel using library (generic)
const parseResult = await ExcelImport.parseOnly({
file: file,
columns: ColumnConfig
});
// Step 2: Validate using company rules (specific)
const validationResult = Validator.validate(parseResult.data);
if (validationResult.hasErrors) {
ResultsDialog.show(this, validationResult.errors);
return;
}
// Step 3: Test mode first
this._closeBusy();
const testResponse = await this._postToBackend(
validationResult.data,
file.name,
true // testMode
);
if (testResponse.hasErrors) {
ResultsDialog.show(this, testResponse.messages);
return;
}
// Step 4: If test passed, ask user to confirm
MessageBox.confirm("Test mode successful. Post to production?", {
onClose: async (action) => {
if (action === MessageBox.Action.OK) {
this._showBusy("Posting to production...");
const prodResponse = await this._postToBackend(
validationResult.data,
file.name,
false // production
);
ResultsDialog.show(this, prodResponse.messages);
}
}
});
} catch (error) {
MessageBox.error(`Import failed: ${error.message}`);
} finally {
this._closeBusy();
}
},
_postToBackend: async function(rows, filename, testMode) {
this._showBusy(testMode ? "Running test mode..." : "Posting to backend...");
try {
// Transform to ABAP format
const payload = Mapper.mapToAbapFormat(rows, {
filename: filename,
isUpdate: false,
testMode: testMode
});
// Post using library's ApiClient
const response = await $.ajax({
url: API_URL,
type: "POST",
contentType: "application/json",
data: JSON.stringify(payload),
timeout: HTTP_TIMEOUT
});
const parsedResponse = JSON.parse(response);
return {
messages: parsedResponse.results || [],
hasErrors: parsedResponse.results?.some(r => r.type === "Error") || false
};
} catch (error) {
throw new Error(`Backend error: ${error.statusText || error.message}`);
}
},
_showBusy: function(msg) {
this.byId("busyDialog").setText(msg).open();
},
_closeBusy: function() {
this.byId("busyDialog").close();
}
});
});Key Integration Principles
| Principle | Implementation |
|-----------|---|
| Library Agnostic | Library returns raw data + errors. Your app interprets them. |
| Transformation External | Keep Validator, Mapper, Adapter in your application folder. |
| Error Handling | Library provides error details. Your Validator adds context. |
| Backend Format | Mapper translates library output to your API's expected format. |
| Reusability | Library unchanged. Switch backends by replacing Mapper/Validator. |
Multi-Backend Example
Use different mappers for different backends:
// Switch backends without changing library or core logic
if (backend === "ABAP") {
payload = AbapPayloadMapper.map(rows, metadata);
apiUrl = "/sap/bc/http/sap/ZFI_API";
} else if (backend === "Node.js REST") {
payload = RestPayloadMapper.map(rows, metadata);
apiUrl = "https://api.company.com/import";
}
await ApiClient.post(apiUrl, payload, 30000);Testing
Run Tests
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage reportTest Coverage
Current Coverage: 119 comprehensive tests
| Module | Tests | Coverage | |--------|-------|----------| | ExcelReader | 18 | File validation, parsing, error handling | | ColumnDetector | 18 | Column mapping, edge cases, validation | | BatchProcessor | 14 | Batch creation, boundary conditions | | ApiClient | 19 | HTTP operations, error scenarios | | ExcelImportFacade | 18 | Complete workflows, validation | | Edge Cases & Integration | 55 | Type validation, error recovery, scenarios |
Test Files
test/ExcelReader.test.js- File validation and parsingtest/ColumnDetector.test.js- Column mappingtest/BatchProcessor.test.js- Batch creationtest/ApiClient.test.js- HTTP clienttest/ExcelImportFacade.test.js- Main APItest/Coverage100.test.js- Edge cases, boundaries, integration scenarios
Test Categories
Happy Path Tests (50%):
- Successful file parsing
- Column mapping with aliases
- Batch processing
- API requests & responses
- Complete workflows
Error Handling Tests (30%):
- Invalid inputs (null, undefined, wrong types)
- Missing required parameters
- Malformed data
- Network errors
- Timeout scenarios
- HTTP error responses
Edge Cases & Boundaries (20%):
- Empty arrays/files
- Single item processing
- Large datasets (1000+ items)
- Case-insensitive operations
- Different MIME types
- Exact vs. remainder batching
Development
Code Quality
npm run lint # ESLint check
npm run lint:fix # Auto-fix issues
npm run build # Full build & testRequirements
- Node.js >= 14.0.0
- npm >= 6.0.0
Project Structure
ui5-excel-import/
├── src/your/lib/excelImport/ # Source modules
├── lib/xlsx/ # XLSX library
├── template/ # Config templates
├── test/ # Test files
├── BUILD_AND_NPM_GUIDE.md # Publishing guide
└── LIBRARY_EVALUATION.md # Architecture docsBrowser Support
- Chrome 89+
- Firefox 86+
- Safari 14+
- Edge 89+
Requirements:
- FileReader API
- Promise support
- Fetch API
- AbortController (for timeouts)
Deprecations
postOnly() - Deprecated
Use parseAndPost() instead for complete workflow:
// ❌ OLD WAY
await ExcelImport.postOnly({
data: parsedData,
mapper: PayloadMapper,
uploadUrl: "/api/import",
fileName: "data.xlsx",
testMode: false
});
// ✅ NEW WAY
await ExcelImport.parseAndPost({
file: excelFile,
columns: ColumnConfig,
mapper: PayloadMapper,
uploadUrl: "/api/import",
fileName: "data.xlsx",
testMode: false
});Documentation
- BUILD_AND_NPM_GUIDE.md - Build, test, and publish guide
- LIBRARY_EVALUATION.md - Architecture and design patterns
- JSDoc Comments - Inline documentation in source code
Examples
Complete Import Workflow
sap.ui.define([
"sap/ui/core/mvc/Controller",
"your/lib/excelImport/ExcelImportFacade",
"your/template/ColumnConfig",
"your/template/PayloadMapper"
], function (Controller, ExcelImport, ColumnConfig, PayloadMapper) {
"use strict";
return Controller.extend("your.app.controller.Import", {
onImportPress: async function () {
try {
// Get file from input
const file = document.getElementById("fileInput").files[0];
// Upload to API
const responses = await ExcelImport.parseAndPost({
file: file,
columns: ColumnConfig,
mapper: PayloadMapper,
uploadUrl: "/odata/import",
fileName: file.name,
testMode: false,
batchSize: 50
});
// Handle responses
const successCount = responses.filter(r => r.success).length;
this.showMessage(`${successCount}/${responses.length} batches processed`);
} catch (error) {
this.showError(error.message);
}
}
});
});Troubleshooting
Excel File Not Recognized
- Verify file has .xlsx extension
- Check MIME type includes "spreadsheet"
- Try exporting as .xlsx from Excel
Column Not Detected
- Check aliases match Excel headers exactly (case-sensitive for aliases check)
- Verify column configuration has array of aliases
- Review warnings in console
Timeout Errors
- Increase
timeoutparameter - Check API server performance
- Verify network connectivity
- Consider reducing batch size
Memory Issues with Large Files
- Use smaller batch size (e.g., 25 instead of 50)
- Process files sequentially
- Monitor browser memory usage
Performance Tips
Batch Size - Test different sizes for your API:
// Smaller batches = more requests but less memory batchSize: 25 // For large files batchSize: 100 // For small filesTimeout Configuration:
// Adjustable per API performance await Api.post(url, data, 60000); // 60 second timeoutFile Size - Recommended max 10,000 rows per file
Version History
v1.0.0 (Current)
- ✅ Comprehensive error handling
- ✅ Full JSDoc documentation
- ✅ 80%+ test coverage
- ✅ Production-ready
- ✨ New
parseAndPost()method - ⚠️ Deprecated
postOnly()method
License
MIT - See LICENSE file
Support & Contributing
Issues: https://github.com/wusxo24/ui5-excel-import/issues
Contributing:
- Fork the repository
- Create feature branch
- Run tests:
npm run build - Submit pull request
