diff --git a/example.xlsx b/example.xlsx index 984f28f..fb97455 100644 Binary files a/example.xlsx and b/example.xlsx differ diff --git a/src/index.ts b/src/index.ts index 6f6da21..ac3c852 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,19 @@ +/* eslint-disable ts/ban-types */ import xlsx, { type IColumn, type IJsonSheet, getWorksheetColumnWidths } from 'json-as-xlsx' import type { CellStyle } from 'xlsx-js-style' import XLSX from 'xlsx-js-style' import { deepmerge } from 'deepmerge-ts' -import type { CellValue, Column, ExcelSchema, GenericObject, NestedPaths, Not, Sheet, TransformersMap, ValueTransformer } from './types' +import type { CellValue, Column, ColumnGroup, ExcelSchema, GenericObject, NestedPaths, Not, Sheet, TransformersMap, ValueTransformer } from './types' import { formatKey, getPropertyFromPath, getSheetCellKey } from './utils' export class ExcelSchemaBuilder< T extends GenericObject, CellKeyPaths extends string, UsedKeys extends string = never, - // eslint-disable-next-line ts/ban-types TransformMap extends TransformersMap = {}, + ContextMap extends { [key: string]: any } = {}, > { - private columns: Column CellValue), string, TransformMap>[] = [] + private columns: Array CellValue), string, TransformMap> | ColumnGroup> = [] private transformers: TransformMap = {} as TransformMap public static create>(): ExcelSchemaBuilder { @@ -21,7 +22,7 @@ export class ExcelSchemaBuilder< public withTransformers(transformers: Transformers): ExcelSchemaBuilder { this.transformers = transformers as TransformMap & Transformers - return this as unknown as ExcelSchemaBuilder + return this as unknown as ExcelSchemaBuilder } public column< @@ -29,22 +30,57 @@ export class ExcelSchemaBuilder< FieldValue extends CellKeyPaths | ((data: T) => CellValue), >( columnKey: Not, - column: Omit, 'columnKey'>, - ): ExcelSchemaBuilder { + column: Omit, 'columnKey' | 'type'>, + ): ExcelSchemaBuilder { if (this.columns.some(c => c.columnKey === columnKey)) throw new Error(`Column with key '${columnKey}' already exists.`) - this.columns.push({ columnKey, ...column } as any) - return this as unknown as ExcelSchemaBuilder + this.columns.push({ type: 'column', columnKey, ...column } as any) + return this as unknown as ExcelSchemaBuilder + } + + public group< + K extends `group:${string}`, + Context, + >( + key: Not, + handler: (builder: ExcelSchemaBuilder, context: Context) => void, + ): ExcelSchemaBuilder< + T, + CellKeyPaths, + UsedKeys | K, + TransformMap, + ContextMap & { [key in K]: Context } + > { + if (this.columns.some(c => c.columnKey === key)) + throw new Error(`Column with key '${key}' already exists.`) + + const builder = () => ExcelSchemaBuilder.create() + .withTransformers(this.transformers) + + this.columns.push({ + type: 'group', + columnKey: key, + builder, + handler, + } as any) + return this } public build() { - return this.columns.map(column => ({ - ...column, - transform: typeof column.transform === 'string' - ? this.transformers[column.transform] - : column.transform, - })) as ExcelSchema + return this.columns.map(column => column.type === 'column' + ? ({ + ...column, + transform: typeof column.transform === 'string' + ? this.transformers[column.transform] + : column.transform, + }) + : column) as ExcelSchema< + T, + CellKeyPaths, + UsedKeys, + ContextMap + > } } @@ -55,7 +91,11 @@ export class ExcelBuilder { return new ExcelBuilder() } - public sheet>( + public sheet< + Key extends string, + T extends GenericObject, + Schema extends ExcelSchema, + >( key: Not, sheet: Omit, 'sheetKey'>, ): ExcelBuilder { @@ -70,7 +110,21 @@ export class ExcelBuilder { const _sheets = this.sheets.map(sheet => ({ sheet: sheet.sheetKey, columns: sheet.schema + .map((column): Column | Column[] => { + if (column.type === 'column') { + return column + } + else { + const builder = column.builder() + column.handler(builder, ((sheet.context ?? {}) as any)[column.columnKey]) + const columns = builder.build() + return columns as Column[] + } + }) + .flat() .filter((column) => { + if (!column) + return false if (!sheet.select || Object.keys(sheet.select).length === 0) return true @@ -115,7 +169,7 @@ export class ExcelBuilder { type: 'buffer', bookType: 'xlsx', }, - // eslint-disable-next-line node/prefer-global/buffer + // eslint-disable-next-line node/prefer-global/buffer }) as Buffer const workbook = XLSX.read(fileBody, { type: 'buffer' }) @@ -167,6 +221,5 @@ export class ExcelBuilder { // eslint-disable-next-line node/prefer-global/buffer return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) as Buffer - // for() } } diff --git a/src/types.ts b/src/types.ts index 6966f5f..36fd970 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,6 @@ +/* eslint-disable ts/ban-types */ import type { CellStyle } from 'xlsx-js-style' +import type { ExcelSchemaBuilder } from '.' export type GenericObject = Record @@ -13,20 +15,6 @@ export type NestedPaths = T extends Array }[keyof T & (string | number)] : never -export type NestedPathsForType = T extends Array - ? U extends object ? never : never - : T extends object - ? { - [K in keyof T & (string | number)]: K extends string - ? T[K] extends P - ? `${K}` | `${K}.${NestedPathsForType}` - : T[K] extends object - ? `${K}.${NestedPathsForType}` - : never - : never; - }[keyof T & (string | number)] - : never - export type Not = T extends U ? never : T export type TypeFromPath = @@ -67,6 +55,7 @@ export type Column< ColKey extends string, TransformMap extends TransformersMap, > = { + type: 'column' label?: string columnKey: ColKey key: FieldValue @@ -79,27 +68,53 @@ export type Column< : { transform: TypedTransformersMap> | ((value: ExtractColumnValue) => CellValue) } ) -// export type DynamicColumns< -// T extends GenericObject, -// FieldValue extends string | ((data: T) => CellValue), -// ColKey extends `dynamic:${string}`, -// TransformMap extends TransformersMap, -// IteratorData, -// > = (data: IteratorData) => Column[] +export interface ColumnGroup< + T extends GenericObject, + ColKey extends string, + KeyPaths extends string, + UsedKeys extends string, + TransformMap extends TransformersMap, + Context, + // eslint-disable-next-line unused-imports/no-unused-vars + ContextMap extends Record = {}, +> { + type: 'group' + columnKey: ColKey + builder: () => ExcelSchemaBuilder + handler: GroupHandler +} + +export type GroupHandler< + T extends GenericObject, + CellKeyPaths extends string, + UsedKeys extends string, + TransformMap extends TransformersMap, + Context, +> = ( + builder: ExcelSchemaBuilder, + context: Context, +) => void export type ExcelSchema< T extends GenericObject, KeyPaths extends string, Key extends string, -> = Array> + ContextMap extends { [key: string]: any } = {}, +> = Array | ColumnGroup> export type SchemaColumnKeys< T extends ExcelSchema, -> = T extends Array> ? K : never +> = T extends Array | ColumnGroup> ? K : never -export interface Sheet> { +export type Sheet< + T extends GenericObject, + Schema extends ExcelSchema, +> = { sheetKey: string schema: Schema data: T[] select?: { [K in SchemaColumnKeys]?: boolean } -} + context?: {} +} & (Schema extends ExcelSchema + ? keyof Ctx extends never ? {} : { context: Ctx } + : {}) diff --git a/test/index.test.ts b/test/index.test.ts index 4434cd7..3c438b7 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -35,6 +35,11 @@ const transformers = { describe('should', () => { it('exported', () => { + const organizations: Organization[] = Array.from({ length: 10 }, (_, id) => ({ + id, + name: faker.company.name(), + })) + const users: User[] = Array.from({ length: 100 }, (_, id) => ({ id, firstName: faker.person.firstName(), @@ -42,10 +47,7 @@ describe('should', () => { email: faker.internet.email(), roles: ['admin', 'user', 'manager', 'guest'].filter(() => Math.random() > 0.5), // RANDOM NUMBER OF ORGANIZATIONS - organizations: Array.from({ length: Math.floor(Math.random() * 5) }, (_, id) => ({ - id, - name: faker.company.name(), - })), + organizations: organizations.filter(() => Math.random() > 0.5), results: { general: { overall: Math.floor(Math.random() * 10) }, technical: { overall: Math.floor(Math.random() * 10) }, @@ -73,11 +75,35 @@ describe('should', () => { .column('technicalScore', { key: 'results.technical.overall' }) .column('interviewScore', { key: 'results.interview.overall', default: 'N/A' }) .column('createdAt', { key: 'createdAt', format: 'd mmm yyyy' }) + .group('group:org', (builder, context: Organization[]) => { + for (const org of context) { + builder + .column(org.id.toString(), { + label: org.name, + key: 'organizations', + transform: orgs => orgs.some(o => o.id === org.id) ? 'YES' : 'NO', + }) + } + }) + .column('test', { key: 'id' }) + .build() + + const schema = ExcelSchemaBuilder + .create() + .withTransformers(transformers) + .column('id', { key: 'id' }) .build() const buffer = ExcelBuilder .create() - .sheet('sheet1', { data: users, schema: assessmentExport }) + .sheet('sheet1', { + data: users, + schema: assessmentExport, + context: { + 'group:org': organizations, + + }, + }) .sheet('sheet2', { data: users, schema: assessmentExport, @@ -85,16 +111,16 @@ describe('should', () => { firstName: true, lastName: true, email: true, + // 'group:org': true, + }, + context: { + 'group:org': organizations, + }, }) .sheet('sheet3', { data: users, - schema: assessmentExport, - select: { - firstName: false, - lastName: false, - email: false, - }, + schema, }) .build()