Adds Excel and CSV import functionality#13
Conversation
Introduces comprehensive import capabilities to complement existing export features, including: - Implements concern-based import pattern with ToArray, ToCollection interfaces for flexible data reception - Adds row processing concerns: WithHeadingRow, WithImportMapping, WithColumnMapping for data transformation - Provides validation system with custom rules and optional class-validator DTO support - Enables batch processing with WithBatchInserts for efficient large dataset handling - Includes error handling via SkipsOnError and SkipsEmptyRows concerns - Adds WithStartRow and WithLimit for granular control over data reading Exposes new service methods: import(), importFromBuffer(), toArray(), toCollection() for various use cases Updates documentation with detailed examples and API reference for all import concerns Marks class-validator and class-transformer as optional peer dependencies for DTO validation feature
There was a problem hiding this comment.
Pull request overview
This PR adds import (XLSX/CSV) capabilities to the existing NestJS Excel module, mirroring the concern-based export approach and exposing new ExcelService APIs for reading files/buffers into arrays/collections with optional mapping, validation, batching, and row controls.
Changes:
- Introduces an import pipeline (
readImport+processSheet) supporting headings, column mapping, row mapping, validation (custom rules or class-validator DTO), batching, limits, and start-row behavior. - Exposes new public types (
ImportResult,ImportValidationError,FieldError) and re-exports new import concerns from the package entrypoint. - Updates docs, adds extensive Vitest coverage for import behavior, and adds optional peer dependency support for
class-validator/class-transformer.
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/validate-row.spec.ts | Adds unit tests for rule-based validation and DTO error mapping. |
| test/excel.service.spec.ts | Adds regression tests for template exports (properties + placeholder replacement). |
| test/excel.import.spec.ts | Adds comprehensive end-to-end import tests across all new concerns. |
| src/interfaces/index.ts | Re-exports new import result interfaces. |
| src/interfaces/import-result.interface.ts | Defines ImportResult + validation error types. |
| src/index.ts | Exposes import concerns + import result types from the public API. |
| src/helpers/validate-row.ts | Implements row validation (custom rules + DTO via optional deps). |
| src/helpers/index.ts | Exposes new helper exports used by import/export internals. |
| src/helpers/csv-settings.ts | Extracts reusable CSV settings resolution for read/write. |
| src/excel.writer.ts | Refactors CSV settings resolution to shared helper. |
| src/excel.sheet-reader.ts | Implements core worksheet-to-rows processing and concern pipeline. |
| src/excel.service.ts | Adds import, importFromBuffer, toArray, toCollection methods. |
| src/excel.reader.ts | Adds XLSX/CSV reader that loads a workbook from file/buffer. |
| src/concerns/with-validation.interface.ts | Introduces WithValidation concern types for import validation. |
| src/concerns/with-start-row.interface.ts | Adds WithStartRow concern for skipping initial rows. |
| src/concerns/with-limit.interface.ts | Adds WithLimit concern for limiting imported rows. |
| src/concerns/with-import-mapping.interface.ts | Adds WithImportMapping concern for transforming rows. |
| src/concerns/with-heading-row.interface.ts | Adds WithHeadingRow concern for header-derived object keys. |
| src/concerns/with-column-mapping.interface.ts | Adds WithColumnMapping concern for keying by column ref/index. |
| src/concerns/with-batch-inserts.interface.ts | Adds WithBatchInserts concern for batch delivery. |
| src/concerns/to-collection.interface.ts | Adds ToCollection receiver concern. |
| src/concerns/to-array.interface.ts | Adds ToArray receiver concern. |
| src/concerns/skips-on-error.interface.ts | Adds SkipsOnError marker concern for validation behavior. |
| src/concerns/skips-empty-rows.interface.ts | Adds SkipsEmptyRows marker concern for filtering blanks. |
| src/concerns/index.ts | Aggregates and exports all new import concerns. |
| package.json | Adds optional peer deps + dev deps for DTO validation support; updates metadata. |
| pnpm-lock.yaml | Locks added dev dependencies (class-validator, class-transformer, etc.). |
| README.md | Documents the new import API, concerns, and service methods. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let classValidator: any; | ||
| let classTransformer: any; | ||
|
|
||
| try { | ||
| classValidator = await import("class-validator"); | ||
| classTransformer = await import("class-transformer"); | ||
| } catch { | ||
| throw new Error( | ||
| "WithValidation with DTO requires class-validator and class-transformer. " + | ||
| "Install them: pnpm add class-validator class-transformer", | ||
| ); | ||
| } | ||
|
|
||
| const instance = classTransformer.plainToInstance(dto, row); | ||
| const errors: any[] = classValidator.validateSync(instance); | ||
|
|
||
| return mapDtoErrors(errors, rowNumber); |
There was a problem hiding this comment.
validateWithDto() dynamically imports class-validator/class-transformer for every row validated. Even though Node caches modules, repeatedly awaiting dynamic imports inside the per-row loop adds avoidable overhead for large imports. Consider caching the imported modules at module scope (or a lazy singleton) so each process only resolves them once per import run.
| if (isWithBatchInserts(importable)) { | ||
| const size = importable.batchSize(); | ||
| for (let i = 0; i < validRows.length; i += size) { | ||
| const batch = validRows.slice(i, i + size); | ||
| await importable.handleBatch(batch); | ||
| } |
There was a problem hiding this comment.
batchSize() is used as the increment in the batching loop. If an implementation returns 0 or a negative number, this will result in an infinite loop (or never executing the loop correctly). Add a guard that throws a clear error when batchSize() is not a positive integer.
| if (isToArray(importable)) { | ||
| // If rows are objects, convert back to arrays for ToArray | ||
| const arrayRows = useObjects | ||
| ? validRows.map((obj) => Object.values(obj)) |
There was a problem hiding this comment.
When useObjects is true, ToArray currently converts objects back to arrays using Object.values(obj). This can produce unstable/incorrect column ordering (e.g., if WithColumnMapping keys are not defined in the desired order, or if WithImportMapping returns objects with a different insertion order). Consider building the output arrays using a deterministic key order (headings order or sorted by mapped column index) so ToArray always reflects spreadsheet column order.
| if (isToArray(importable)) { | |
| // If rows are objects, convert back to arrays for ToArray | |
| const arrayRows = useObjects | |
| ? validRows.map((obj) => Object.values(obj)) | |
| // Helper to convert a row to an array using a deterministic key order. | |
| const toDeterministicArrayRow = (row: any): any[] => { | |
| if (Array.isArray(row)) { | |
| return row; | |
| } | |
| const keys = Object.keys(row); | |
| // If all keys are numeric strings, sort numerically (column-like indices). | |
| const allNumeric = keys.every((k) => /^\d+$/.test(k)); | |
| if (allNumeric) { | |
| return keys | |
| .sort((a, b) => Number(a) - Number(b)) | |
| .map((k) => row[k]); | |
| } | |
| // Fallback: sort keys lexicographically for deterministic ordering. | |
| return keys.sort().map((k) => row[k]); | |
| }; | |
| if (isToArray(importable)) { | |
| // If rows are objects, convert back to arrays for ToArray using | |
| // a deterministic key order so column ordering is stable. | |
| const arrayRows = useObjects | |
| ? validRows.map((row) => toDeterministicArrayRow(row)) |
| import { IsString, IsEmail } from "class-validator"; | ||
|
|
||
| class UserDto { | ||
| @IsString() @IsNotEmpty() name: string; | ||
| @IsEmail() email: string; |
There was a problem hiding this comment.
In the README DTO example, UserDto declares name: string; / email: string; without initialization or definite assignment. With common TS settings used in Nest projects (e.g., strictPropertyInitialization), this snippet won’t type-check. Use definite assignment (name!: string) or a constructor to make the example compile cleanly.
| import { IsString, IsEmail } from "class-validator"; | |
| class UserDto { | |
| @IsString() @IsNotEmpty() name: string; | |
| @IsEmail() email: string; | |
| import { IsString, IsEmail, IsNotEmpty } from "class-validator"; | |
| class UserDto { | |
| @IsString() @IsNotEmpty() name!: string; | |
| @IsEmail() email!: string; |
Fixes issue where object keys were returned in arbitrary order when converting back to arrays, causing column misalignment with the original spreadsheet structure. Maintains consistent key ordering by: - Sorting column mappings by their index position - Preserving heading order when available - Applying this order when converting objects back to arrays Adds validation for batch size to prevent invalid configurations (zero or negative values). Optimizes DTO validation by caching imported dependencies to avoid repeated dynamic imports. Updates documentation examples to include proper TypeScript definite assignment assertions and missing imports.
Introduces comprehensive import capabilities to complement existing export features, including:
Exposes new service methods: import(), importFromBuffer(), toArray(), toCollection() for various use cases
Updates documentation with detailed examples and API reference for all import concerns
Marks class-validator and class-transformer as optional peer dependencies for DTO validation feature
Resolve #1