Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fluffy-pillows-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@flatfile/plugin-export-workbook': major
---

Added options to adjust the origin and column headers
29 changes: 29 additions & 0 deletions plugins/export-workbook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,36 @@ Automatically download the file after exporting

The `debug` parameter lets you toggle on/off helpful debugging messages for development purposes.

#### `sheetOptions` - `Record<string, ExportSheetOptions>` - (optional)

A map of sheet slug to `ExportSheetOptions` instance providing sheet specific export options:

- `skipColumnHeaders` - `boolean` - (optional) - allows omitting column headers
- `origin` - `number` | `SheetAddress` - (optional) - allows offsetting the start of a sheet. The parameter is either a row number or an object with `column` and `row`.

Usage:

```typescript
listener.use(
exportWorkbookPlugin({
sheetOptions: {
SomeSheetSlug: {
// Start the sheet at 5
origin: 5,
// Omit column headers
skipColumnHeaders: true,
},
SomeOtherSheetSlug: {
// Start the sheet at row 10 column 2
origin: {row: 10, column: 2},
},
},
})
```

#### `columnNameTransformer` - `ColumnNameTransformerCallback` - (optional)

A callback function allowing changing how column names appear in the workbook. The function accepts two arguments: `columnName` and `sheetSlug` and returns a new column name.

## Usage

Expand Down
3 changes: 2 additions & 1 deletion plugins/export-workbook/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { FlatfileEvent } from '@flatfile/listener'
import type { TickFunction } from '@flatfile/plugin-job-handler'

import { jobHandler } from '@flatfile/plugin-job-handler'
import { PluginOptions, exportRecords } from './plugin'
import { exportRecords } from './plugin'
import type { PluginOptions } from './options'

/**
* Export records plugin for Flatfile.
Expand Down
53 changes: 53 additions & 0 deletions plugins/export-workbook/src/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Flatfile } from '@flatfile/api'

/**
* A sheet address.
*
* @property {number} column - The column component
* @property {number} row - The row component
*/
export interface SheetAddress {
column: number
row: number
}

/**
* Sheet specific options.
*
* @property {number | SheetAddress} origin - The sheet origin
* @property {boolean} skipColumnHeaders - If true, do not include column row in output
*/
export interface ExportSheetOptions {
origin?: number | SheetAddress
skipColumnHeaders?: boolean
}

export type ColumnNameTransformerCallback = (
columnName: string,
sheetSlug: string
) => string

/**
* Plugin config options.
*
* @property {string} jobName - name of the job
* @property {string[]} excludedSheets - list of sheet names to exclude from the exported data.
* @property {string[]} excludeFields - list of field names to exclude from the exported data. This applies to all sheets.
* @property {Flatfile.Filter} recordFilter - filter to apply to the records before exporting.
* @property {boolean} includeRecordIds - include record ids in the exported data.
* @property {boolean} autoDownload - auto download the file after exporting
* @property {boolean} debug - show helpful messages useful for debugging (use intended for development).
* @property {Record<string, ExportSheetOptions>} sheetOptions - map of sheet slug to ExportSheetOptions.
* @property {ColumnNameTransformerCallback} columnNameTransformer - callback to transform column names.
*/
export interface PluginOptions {
readonly jobName?: string
readonly excludedSheets?: string[]
readonly excludeFields?: string[]
readonly recordFilter?: Flatfile.Filter
readonly includeRecordIds?: boolean
readonly autoDownload?: boolean
readonly debug?: boolean
readonly sheetOptions?: Record<string, ExportSheetOptions>
readonly columnNameTransformer?: ColumnNameTransformerCallback
}
88 changes: 41 additions & 47 deletions plugins/export-workbook/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,23 @@ import * as fs from 'fs'
import path from 'path'
import * as R from 'remeda'
import * as XLSX from 'xlsx'
import { sanitize, sanitizeExcelSheetName } from './utils'
import {
createXLSXSheetOptions,
sanitize,
sanitizeExcelSheetName,
} from './utils'
import type { PluginOptions } from './options'

const api = new FlatfileClient()

/**
* Plugin config options.
*
* @property {string} jobName - name of the job
* @property {string[]} excludedSheets - list of sheet names to exclude from the exported data.
* @property {string[]} excludeFields - list of field names to exclude from the exported data. This applies to all sheets.
* @property {Flatfile.Filter} recordFilter - filter to apply to the records before exporting.
* @property {boolean} includeRecordIds - include record ids in the exported data.
* @property {boolean} autoDownload - auto download the file after exporting
* @property {boolean} debug - show helpul messages useful for debugging (use intended for development).
*/
export interface PluginOptions {
readonly jobName?: string
readonly excludedSheets?: string[]
readonly excludeFields?: string[]
readonly recordFilter?: Flatfile.Filter
readonly includeRecordIds?: boolean
readonly autoDownload?: boolean
readonly debug?: boolean
}
import { name as PACKAGE_NAME } from '../package.json'

/**
* Runs extractor and creates an `.xlsx` file with all Flatfile Workbook data.
*
* @param event - Flatfile event
* @param options - plugin config options
* @param tick - a function to update job progress
*/
export const exportRecords = async (
event: FlatfileEvent,
Expand All @@ -61,24 +48,24 @@ export const exportRecords = async (
}, '')
)

logInfo(
'@flatfile/plugin-export-workbook',
`Sheets found in Flatfile workbook: ${meta}`
)
logInfo(PACKAGE_NAME, `Sheets found in Flatfile workbook: ${meta}`)
}

const xlsxWorkbook = XLSX.utils.book_new()

for (const [sheetIndex, sheet] of sheets.entries()) {
if (options.excludedSheets?.includes(sheet.config.slug)) {
if (options.debug) {
logInfo(
'@flatfile/plugin-export-workbook',
`Skipping sheet: ${sheet.name}`
)
logInfo(PACKAGE_NAME, `Skipping sheet: ${sheet.name}`)
}
continue
}

const columnNameTransformer = options.columnNameTransformer
? (name: string) =>
options.columnNameTransformer(name, sheet.config.slug)
: (name: string) => name

try {
let results = await processRecords<Record<string, any>[]>(
sheet.id,
Expand All @@ -95,7 +82,7 @@ export const exportRecords = async (
}) => {
const rowValue = R.pipe(
Object.keys(row),
R.reduce((acc, colName) => {
R.reduce((acc, colName: string) => {
if (options.excludeFields?.includes(colName)) {
return acc
}
Expand All @@ -120,7 +107,9 @@ export const exportRecords = async (

return {
...acc,
[colName]: formatCell(row[colName]),
[columnNameTransformer(colName)]: formatCell(
row[colName]
),
}
}, {})
)
Expand All @@ -144,13 +133,24 @@ export const exportRecords = async (
v: '',
c: [],
}

results = [
sheet.config.fields.map((field) => ({ [field.key]: emptyCell })),
[
Object.fromEntries(
sheet.config.fields.map((field) => [
columnNameTransformer(field.key),
emptyCell,
])
),
],
]
}
const rows = results.flat()

const worksheet = XLSX.utils.json_to_sheet(rows)
const worksheet = XLSX.utils.json_to_sheet(
rows,
createXLSXSheetOptions(options.sheetOptions?.[sheet.config.slug])
)

XLSX.utils.book_append_sheet(
xlsxWorkbook,
Expand All @@ -163,7 +163,7 @@ export const exportRecords = async (
)
} catch (_) {
logError(
'@flatfile/plugin-export-workbook',
PACKAGE_NAME,
`Failed to fetch records for sheet with id: ${sheet.id}`
)

Expand All @@ -180,10 +180,7 @@ export const exportRecords = async (

if (xlsxWorkbook.SheetNames.length === 0) {
if (options.debug) {
logError(
'@flatfile/plugin-export-workbook',
'No data to write to Excel file'
)
logError(PACKAGE_NAME, 'No data to write to Excel file')
}

throw new Error('No data to write to Excel file.')
Expand All @@ -196,13 +193,10 @@ export const exportRecords = async (
await tick(80, 'Excel file written to disk')

if (options.debug) {
logInfo('@flatfile/plugin-export-workbook', 'File written to disk')
logInfo(PACKAGE_NAME, 'File written to disk')
}
} catch (_) {
logError(
'@flatfile/plugin-export-workbook',
'Failed to write file to disk'
)
logError(PACKAGE_NAME, 'Failed to write file to disk')

throw new Error('Failed writing the Excel file to disk.')
}
Expand All @@ -226,18 +220,18 @@ export const exportRecords = async (

if (options.debug) {
logInfo(
'@flatfile/plugin-export-workbook',
PACKAGE_NAME,
`Excel document uploaded. View file at https://spaces.flatfile.com/space/${spaceId}/files?mode=export`
)
}
} catch (_) {
logError('@flatfile/plugin-export-workbook', 'Failed to upload file')
logError(PACKAGE_NAME, 'Failed to upload file')

throw new Error('Failed uploading Excel file to Flatfile.')
}

if (options.debug) {
logInfo('@flatfile/plugin-export-workbook', 'Done')
logInfo(PACKAGE_NAME, 'Done')
}

return options.autoDownload
Expand Down Expand Up @@ -267,7 +261,7 @@ export const exportRecords = async (
},
}
} catch (error) {
logError('@flatfile/plugin-export-workbook', error)
logError(PACKAGE_NAME, error)

throw new Error((error as Error).message)
}
Expand Down
23 changes: 23 additions & 0 deletions plugins/export-workbook/src/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createXLSXSheetOptions } from './utils'
import type { ExportSheetOptions } from './options'
import type { JSON2SheetOpts } from 'xlsx'

describe('createXLSXSheetOptions', () => {
it.each([
[null, {}],
[undefined, {}],
[{}, {}],
[{ skipColumnHeaders: true }, { skipHeader: true }],
[{ skipColumnHeaders: false }, {}],
[
{ origin: 123, skipColumnHeaders: true },
{ origin: 123, skipHeader: true },
],
[{ origin: { row: 1, column: 2 } }, { origin: { r: 1, c: 2 } }],
])(
'createXLSXSheetOptions %o',
(options: ExportSheetOptions, expected: JSON2SheetOpts) => {
expect(createXLSXSheetOptions(options)).toEqual(expected)
}
)
})
33 changes: 33 additions & 0 deletions plugins/export-workbook/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { ExportSheetOptions } from './options'
import type { JSON2SheetOpts } from 'xlsx'

export function sanitize(fileName: string): string {
// List of invalid characters that are commonly not allowed in file names
const invalidChars = /[\/\?%\*:|"<>]/g
Expand Down Expand Up @@ -55,3 +58,33 @@ export const genCyclicPattern = (length: number = 104): Array<string> => {

return alphaPattern
}

/**
* Convert sheetOptions to JSON2SheetOpts.
*
* @param sheetOptions Sheet options
*/
export function createXLSXSheetOptions(
sheetOptions?: ExportSheetOptions
): JSON2SheetOpts {
const options: JSON2SheetOpts = {}

if (sheetOptions?.origin) {
if (typeof sheetOptions.origin === 'number') {
options.origin = sheetOptions.origin
} else if (
'column' in sheetOptions.origin &&
'row' in sheetOptions.origin
) {
options.origin = {
c: sheetOptions.origin.column,
r: sheetOptions.origin.row,
}
}
}

if (sheetOptions?.skipColumnHeaders) {
options.skipHeader = true
}
return options
}