diff --git a/.changeset/fluffy-pillows-call.md b/.changeset/fluffy-pillows-call.md new file mode 100644 index 000000000..ff928f3fa --- /dev/null +++ b/.changeset/fluffy-pillows-call.md @@ -0,0 +1,5 @@ +--- +'@flatfile/plugin-export-workbook': major +--- + +Added options to adjust the origin and column headers diff --git a/plugins/export-workbook/README.md b/plugins/export-workbook/README.md index 455fe3c7c..9d199c125 100644 --- a/plugins/export-workbook/README.md +++ b/plugins/export-workbook/README.md @@ -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` - (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 diff --git a/plugins/export-workbook/src/index.ts b/plugins/export-workbook/src/index.ts index 143b6608b..185156299 100644 --- a/plugins/export-workbook/src/index.ts +++ b/plugins/export-workbook/src/index.ts @@ -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. diff --git a/plugins/export-workbook/src/options.ts b/plugins/export-workbook/src/options.ts new file mode 100644 index 000000000..dea72fbca --- /dev/null +++ b/plugins/export-workbook/src/options.ts @@ -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} 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 + readonly columnNameTransformer?: ColumnNameTransformerCallback +} diff --git a/plugins/export-workbook/src/plugin.ts b/plugins/export-workbook/src/plugin.ts index 81966178e..f8563e6b1 100644 --- a/plugins/export-workbook/src/plugin.ts +++ b/plugins/export-workbook/src/plugin.ts @@ -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, @@ -61,10 +48,7 @@ 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() @@ -72,13 +56,16 @@ export const exportRecords = async ( 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[]>( sheet.id, @@ -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 } @@ -120,7 +107,9 @@ export const exportRecords = async ( return { ...acc, - [colName]: formatCell(row[colName]), + [columnNameTransformer(colName)]: formatCell( + row[colName] + ), } }, {}) ) @@ -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, @@ -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}` ) @@ -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.') @@ -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.') } @@ -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 @@ -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) } diff --git a/plugins/export-workbook/src/utils.spec.ts b/plugins/export-workbook/src/utils.spec.ts new file mode 100644 index 000000000..62fa41af1 --- /dev/null +++ b/plugins/export-workbook/src/utils.spec.ts @@ -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) + } + ) +}) diff --git a/plugins/export-workbook/src/utils.ts b/plugins/export-workbook/src/utils.ts index f6833cb44..4f8970284 100644 --- a/plugins/export-workbook/src/utils.ts +++ b/plugins/export-workbook/src/utils.ts @@ -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 @@ -55,3 +58,33 @@ export const genCyclicPattern = (length: number = 104): Array => { 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 +}