npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ui5-excel-import

v1.1.0

Published

Reusable Excel Import Library for SAP UI5 - Production-ready with error handling, validation, and comprehensive testing

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/excelImport

Option 2: npm (Coming Soon)

npm install ui5-excel-import

Basic 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 with map() 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 provided
  • Column configuration is required - Column config missing
  • File must be an Excel file - Invalid file format
  • Excel sheet is empty - No data in sheet
  • Upload URL must be a non-empty string - Invalid URL
  • Request 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 API

Integration 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 report

Test 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 parsing
  • test/ColumnDetector.test.js - Column mapping
  • test/BatchProcessor.test.js - Batch creation
  • test/ApiClient.test.js - HTTP client
  • test/ExcelImportFacade.test.js - Main API
  • test/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 & test

Requirements

  • 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 docs

Browser 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 timeout parameter
  • 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

  1. Batch Size - Test different sizes for your API:

    // Smaller batches = more requests but less memory
    batchSize: 25   // For large files
    batchSize: 100  // For small files
  2. Timeout Configuration:

    // Adjustable per API performance
    await Api.post(url, data, 60000);  // 60 second timeout
  3. File 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:

  1. Fork the repository
  2. Create feature branch
  3. Run tests: npm run build
  4. Submit pull request

Related Documentation