Skip to content

feat: export decorators (@Exportable, @ExportColumn, @ExportIgnore)#15

Merged
khatabwedaa merged 3 commits into
mainfrom
feat/export-decorators
Mar 25, 2026
Merged

feat: export decorators (@Exportable, @ExportColumn, @ExportIgnore)#15
khatabwedaa merged 3 commits into
mainfrom
feat/export-decorators

Conversation

@khatabwedaa
Copy link
Copy Markdown
Contributor

Summary

  • Adds @Exportable(), @ExportColumn(), and @ExportIgnore() decorators so users can define exports directly on entities/DTOs instead of writing separate export classes
  • Adds 4 new ExcelService methods: downloadFromEntity(), downloadFromEntityAsStream(), storeFromEntity(), rawFromEntity()
  • Full class inheritance support — child classes inherit, override, or ignore parent columns
  • README updated to use the decorator API as the primary example; concern-based API documented as secondary/advanced

What changed

Area Details
src/decorators/ 7 new files — constants, interfaces, 3 decorators, metadata builder, barrel
src/excel.service.ts 4 new *FromEntity methods delegating to existing concern-based pipeline
src/index.ts Re-exports for decorators and types
README.md Decorator API is now the hero snippet and Quick Start; full docs section added
CHANGELOG.md New "Export Decorators" section under v0.2.0
test/decorators.spec.ts 24 tests — 100% coverage on decorator files

Test plan

  • 154 tests passing (130 existing + 24 new)
  • 100% coverage on all decorator source files
  • TypeScript compiles cleanly (npx tsc --noEmit)
  • End-to-end tests verify XLSX and CSV output from decorated entities
  • Inheritance tested (parent columns inherited, child override/ignore)

Closes #6

🤖 Generated with Claude Code

Allows users to decorate entities/DTOs directly instead of writing
separate export classes. Includes inheritance support, per-column
formatting/mapping, and 4 new ExcelService methods. README updated
to use decorator API as the primary example.

Closes #6

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 25, 2026 04:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a decorator-based API for defining exports directly on entities/DTOs, plus ExcelService convenience methods that convert decorated classes into the existing concern-compatible export pipeline.

Changes:

  • Introduces @Exportable, @ExportColumn, @ExportIgnore, and buildExportFromEntity() to build concern-compatible export objects from decorator metadata.
  • Adds ExcelService.downloadFromEntity(), downloadFromEntityAsStream(), storeFromEntity(), and rawFromEntity() helpers.
  • Updates public exports and documentation (README + CHANGELOG) and adds a new decorator-focused test suite.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/decorators.spec.ts Adds unit + end-to-end style tests validating decorator metadata → export output behavior (XLSX/CSV, inheritance, ignore).
src/index.ts Re-exports decorators and their option types from the package entrypoint.
src/excel.service.ts Adds decorator-based convenience methods delegating to existing service methods.
src/decorators/interfaces.ts Defines decorator option types for class-level and column-level configuration.
src/decorators/index.ts Barrel exports for decorators + builder + types.
src/decorators/exportable.decorator.ts Implements @Exportable() metadata storage.
src/decorators/export-column.decorator.ts Implements @ExportColumn() metadata storage.
src/decorators/export-ignore.decorator.ts Implements @ExportIgnore() metadata storage.
src/decorators/constants.ts Defines metadata keys for decorator storage.
src/decorators/build-export-from-entity.ts Builds a concern-compatible export object by resolving columns/options (incl. inheritance).
README.md Promotes decorator API as primary usage and documents options/inheritance/service methods.
CHANGELOG.md Documents new decorator API and new ExcelService methods.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +129 to +135
const exportableOpts: ExportableOptions | undefined =
Reflect.getMetadata(EXPORTABLE_META, entityClass);

if (!exportableOpts) {
throw new Error(
`Class "${entityClass.name}" is not decorated with @Exportable().`,
);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildExportFromEntity uses Reflect.getMetadata(EXPORTABLE_META, entityClass), which will also return metadata inherited from a parent class. That means a child class without @Exportable() will still be treated as exportable, contradicting the error message and the README examples where each class is explicitly decorated. Consider using Reflect.getOwnMetadata (or explicitly documenting/handling inherited @Exportable options) so behavior matches the intended API.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +46
const merged = new Map<string, ExportColumnOptions>();
const ignored = new Set<string>();

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collectColumns maintains an ignored Set and updates it, but the set is never used to influence the returned ResolvedColumn[] (the behavior is fully determined by merged). This extra state makes the override/ignore logic harder to reason about; consider removing ignored (or using it in a final filter if that was the intent).

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +14
return (target: Object, propertyKey: string | symbol) => {
const key = String(propertyKey);
const ctor = target.constructor;

const existing: Map<string, ExportColumnOptions> =
Reflect.getOwnMetadata(EXPORT_COLUMNS_META, ctor) ?? new Map();

existing.set(key, opts ?? {});
Reflect.defineMetadata(EXPORT_COLUMNS_META, existing, ctor);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExportColumn derives the owning class via target.constructor, which breaks if the decorator is applied to a static property (for static properties, target is the constructor function and target.constructor is Function). Consider using const ctor = typeof target === "function" ? target : target.constructor; so metadata is attached to the correct class in both cases.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +13
return (target: Object, propertyKey: string | symbol) => {
const key = String(propertyKey);
const ctor = target.constructor;

const existing: Set<string> =
Reflect.getOwnMetadata(EXPORT_IGNORE_META, ctor) ?? new Set();

existing.add(key);
Reflect.defineMetadata(EXPORT_IGNORE_META, existing, ctor);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExportIgnore derives the owning class via target.constructor, which breaks if the decorator is applied to a static property (for static properties, target is the constructor function and target.constructor is Function). Consider using const ctor = typeof target === "function" ? target : target.constructor; so the ignore metadata is always attached to the correct class.

Copilot uses AI. Check for mistakes.
Comment thread src/excel.service.ts
Comment on lines +108 to +154
async downloadFromEntity<T>(
entityClass: new (...args: any[]) => T,
data: T[],
filename: string,
writerType?: ExcelType,
): Promise<ExcelDownloadResult> {
const exportable = buildExportFromEntity(entityClass, data);
return this.download(exportable, filename, writerType);
}

/**
* Export a decorated entity class as a NestJS StreamableFile.
*/
async downloadFromEntityAsStream<T>(
entityClass: new (...args: any[]) => T,
data: T[],
filename: string,
writerType?: ExcelType,
): Promise<StreamableFile> {
const exportable = buildExportFromEntity(entityClass, data);
return this.downloadAsStream(exportable, filename, writerType);
}

/**
* Export a decorated entity class to a local file.
*/
async storeFromEntity<T>(
entityClass: new (...args: any[]) => T,
data: T[],
filePath: string,
writerType?: ExcelType,
): Promise<void> {
const exportable = buildExportFromEntity(entityClass, data);
return this.store(exportable, filePath, writerType);
}

/**
* Export a decorated entity class and return the raw buffer.
*/
async rawFromEntity<T>(
entityClass: new (...args: any[]) => T,
data: T[],
writerType: ExcelType,
): Promise<Buffer> {
const exportable = buildExportFromEntity(entityClass, data);
return this.raw(exportable, writerType);
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New decorator-based convenience methods (downloadFromEntity*, storeFromEntity, rawFromEntity) are not covered by the existing test/excel.service.spec.ts suite (no references found). Since this file has extensive service-level tests, it would be good to add a few cases asserting these methods delegate correctly (type resolution via filename for download/store, and required writerType for raw).

Copilot uses AI. Check for mistakes.
khatabwedaa and others added 2 commits March 25, 2026 07:54
- Use getOwnMetadata for @Exportable check so child classes must
  explicitly opt in rather than silently inheriting
- Remove dead `ignored` Set from collectColumns — merged.delete()
  already handles exclusion
- Add service-level tests for all *FromEntity methods (download,
  stream, store, raw) and inheritance guard test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
v0.2.0 is already published; decorator feature will ship in the next
release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@khatabwedaa khatabwedaa merged commit 0feb796 into main Mar 25, 2026
2 checks passed
@khatabwedaa khatabwedaa deleted the feat/export-decorators branch March 25, 2026 04:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Export Decorators — @Exportable() on entities and DTOs

2 participants