Skip to content

Commit

Permalink
feat: implement column group & context resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
ChronicStone committed Dec 4, 2023
1 parent 4e3eff7 commit 4820d7f
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 53 deletions.
Binary file modified example.xlsx
Binary file not shown.
87 changes: 70 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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<T, CellKeyPaths | ((data: T) => CellValue), string, TransformMap>[] = []
private columns: Array<Column<T, CellKeyPaths | ((data: T) => CellValue), string, TransformMap> | ColumnGroup<T, string, CellKeyPaths, string, TransformMap, any>> = []
private transformers: TransformMap = {} as TransformMap

public static create<T extends GenericObject, KeyPath extends string = NestedPaths<T>>(): ExcelSchemaBuilder<T, KeyPath> {
Expand All @@ -21,30 +22,65 @@ export class ExcelSchemaBuilder<

public withTransformers<Transformers extends TransformersMap>(transformers: Transformers): ExcelSchemaBuilder<T, CellKeyPaths, UsedKeys, TransformMap & Transformers> {
this.transformers = transformers as TransformMap & Transformers
return this as unknown as ExcelSchemaBuilder<T, CellKeyPaths, UsedKeys, TransformMap & Transformers>
return this as unknown as ExcelSchemaBuilder<T, CellKeyPaths, UsedKeys, TransformMap & Transformers, ContextMap>
}

public column<
K extends string,
FieldValue extends CellKeyPaths | ((data: T) => CellValue),
>(
columnKey: Not<K, UsedKeys>,
column: Omit<Column<T, FieldValue, K, TransformMap>, 'columnKey'>,
): ExcelSchemaBuilder<T, CellKeyPaths, UsedKeys | K, TransformMap> {
column: Omit<Column<T, FieldValue, K, TransformMap>, 'columnKey' | 'type'>,
): ExcelSchemaBuilder<T, CellKeyPaths, UsedKeys | K, TransformMap, ContextMap> {
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<T, CellKeyPaths, UsedKeys | K, TransformMap>
this.columns.push({ type: 'column', columnKey, ...column } as any)
return this as unknown as ExcelSchemaBuilder<T, CellKeyPaths, UsedKeys | K, TransformMap, ContextMap>
}

public group<
K extends `group:${string}`,
Context,
>(
key: Not<K, UsedKeys>,
handler: (builder: ExcelSchemaBuilder<T, CellKeyPaths, UsedKeys, TransformMap>, 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<T, CellKeyPaths>()
.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<T, CellKeyPaths, UsedKeys>
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
>
}
}

Expand All @@ -55,7 +91,11 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {
return new ExcelBuilder()
}

public sheet<Key extends string, T extends GenericObject, Schema extends ExcelSchema<T, any, string>>(
public sheet<
Key extends string,
T extends GenericObject,
Schema extends ExcelSchema<T, any, string>,
>(
key: Not<Key, UsedSheetKeys>,
sheet: Omit<Sheet<T, Schema>, 'sheetKey'>,
): ExcelBuilder<UsedSheetKeys | Key> {
Expand All @@ -70,7 +110,21 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {
const _sheets = this.sheets.map(sheet => ({
sheet: sheet.sheetKey,
columns: sheet.schema
.map((column): Column<any, any, any, any> | Column<any, any, any, any>[] => {
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<any, any, any, any>[]
}
})
.flat()
.filter((column) => {
if (!column)
return false
if (!sheet.select || Object.keys(sheet.select).length === 0)
return true

Expand Down Expand Up @@ -115,7 +169,7 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {
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' })
Expand Down Expand Up @@ -167,6 +221,5 @@ export class ExcelBuilder<UsedSheetKeys extends string = never> {

// eslint-disable-next-line node/prefer-global/buffer
return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) as Buffer
// for()
}
}
65 changes: 40 additions & 25 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable ts/ban-types */
import type { CellStyle } from 'xlsx-js-style'
import type { ExcelSchemaBuilder } from '.'

export type GenericObject = Record<string | number | symbol, any>

Expand All @@ -13,20 +15,6 @@ export type NestedPaths<T> = T extends Array<infer U>
}[keyof T & (string | number)]
: never

export type NestedPathsForType<T, P> = T extends Array<infer U>
? 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], P>}`
: T[K] extends object
? `${K}.${NestedPathsForType<T[K], P>}`
: never
: never;
}[keyof T & (string | number)]
: never

export type Not<T, U> = T extends U ? never : T

export type TypeFromPath<T extends GenericObject, Path extends string> =
Expand Down Expand Up @@ -67,6 +55,7 @@ export type Column<
ColKey extends string,
TransformMap extends TransformersMap,
> = {
type: 'column'
label?: string
columnKey: ColKey
key: FieldValue
Expand All @@ -79,27 +68,53 @@ export type Column<
: { transform: TypedTransformersMap<TransformMap, ExtractColumnValue<T, FieldValue>> | ((value: ExtractColumnValue<T, FieldValue>) => CellValue) }
)

// export type DynamicColumns<
// T extends GenericObject,
// FieldValue extends string | ((data: T) => CellValue),
// ColKey extends `dynamic:${string}`,
// TransformMap extends TransformersMap,
// IteratorData,
// > = (data: IteratorData) => Column<T, FieldValue, ColKey, TransformMap>[]
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<string, any> = {},
> {
type: 'group'
columnKey: ColKey
builder: () => ExcelSchemaBuilder<T, KeyPaths, UsedKeys, TransformMap>
handler: GroupHandler<T, KeyPaths, UsedKeys, TransformMap, Context>
}

export type GroupHandler<
T extends GenericObject,
CellKeyPaths extends string,
UsedKeys extends string,
TransformMap extends TransformersMap,
Context,
> = (
builder: ExcelSchemaBuilder<T, CellKeyPaths, UsedKeys, TransformMap>,
context: Context,
) => void

export type ExcelSchema<
T extends GenericObject,
KeyPaths extends string,
Key extends string,
> = Array<Column<T, KeyPaths, Key, any>>
ContextMap extends { [key: string]: any } = {},
> = Array<Column<T, KeyPaths, Key, any> | ColumnGroup<T, Key, KeyPaths, string, any, any, ContextMap>>

export type SchemaColumnKeys<
T extends ExcelSchema<any, any, string>,
> = T extends Array<Column<any, any, infer K, any>> ? K : never
> = T extends Array<Column<any, any, infer K, any> | ColumnGroup<any, infer K, any, any, any, any>> ? K : never

export interface Sheet<T extends GenericObject, Schema extends ExcelSchema<T, any, string>> {
export type Sheet<
T extends GenericObject,
Schema extends ExcelSchema<T, any, string, any>,
> = {
sheetKey: string
schema: Schema
data: T[]
select?: { [K in SchemaColumnKeys<Schema>]?: boolean }
}
context?: {}
} & (Schema extends ExcelSchema<T, any, any, infer Ctx>
? keyof Ctx extends never ? {} : { context: Ctx }
: {})
48 changes: 37 additions & 11 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,19 @@ 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(),
lastName: faker.person.lastName(),
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) },
Expand Down Expand Up @@ -73,28 +75,52 @@ 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<User>()
.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,
select: {
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()

Expand Down

0 comments on commit 4820d7f

Please sign in to comment.