Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Export with Excel Format #214

Merged
merged 12 commits into from
May 21, 2024
5 changes: 5 additions & 0 deletions .changeset/silly-months-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'contexture-export': minor
---

Enable Excel Export Option
3 changes: 2 additions & 1 deletion packages/export/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"contexture-client": "^2.53.7",
"futil": "^1.76.4",
"lodash": "^4.17.21",
"minimal-csv-formatter": "^1.0.15"
"minimal-csv-formatter": "^1.0.15",
"write-excel-file": "^2.0.1"
},
"packageManager": "yarn@3.3.1"
}
4 changes: 3 additions & 1 deletion packages/export/src/csv.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export default ({
// and key of the record.
// [{ key: string(supports lodash dot notation), label: string, display: function(value, {key, record, transform})}...]
transform,
headers = null, // array of strings to use as headers, array or arrays for multi-line headers
//TODO: support multi-line headers in excel and csv when implemented
headers = null, // array of strings to use as headers
onWrite = _.noop, // function to intercept writing a page of records
}) => {
stream.write(csv(headers || transformLabels(transform)))
Expand Down Expand Up @@ -39,6 +40,7 @@ export default ({
recordsWritten = recordsWritten + _.getOr(1, 'recordCount', r)
await onWrite({ recordsWritten, record: r })
}
await onWrite({ recordsWritten, isStreamDone: true })
await stream.end()
})(),
cancel() {
Expand Down
94 changes: 94 additions & 0 deletions packages/export/src/excel.js
Copy link
Member

Choose a reason for hiding this comment

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

Can you write tests for this file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What type of test would you suggest? the data is binary, so we really cannot build one like is done for CSV's, I thought about it but it felt like there was little value add for doing this based on this constraint.

Copy link
Member

Choose a reason for hiding this comment

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

You can read the stream via https://www.npmjs.com/package/read-excel-file, which is nice as you don't need to write a file in the test, keeping it pure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This seems ok, I am not in love with it because we are using another library to test this one in essence but seems like a start.

Copy link
Member

Choose a reason for hiding this comment

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

It's by the same author so I think that reduces the risk somewhat

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, it just feels like we are testing the library itself instead of anything novel about how it is used, I will add it though as it is at least something. Thanks for the feedback on this.

Copy link
Member

Choose a reason for hiding this comment

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

My thinking is that there is some code that can benefit from testing which is not library related. Another approach is to take a writeData callback that looks like

      const readStream = await writeXlsxFile(excelData, { columns })
      for await (const chunk of readStream) {
        stream.write(chunk)
      }

That way you don't have to read the xlsx file but can still test this function.

Copy link
Member

Choose a reason for hiding this comment

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

Reminder that this still needs to be done.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is in the current PR and the tests is using it as such.

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import _ from 'lodash/fp.js'
import F from 'futil'
import writeXlsxFile from 'write-excel-file/node'

let transformLabels = _.map(_.get('label'))
let maxColumnWidth = 200
let headerBackgroundColor = '#999999'
let indexColumnBackgroundColor = '#bbbbbb'

const convertToExcelCell = (value, index) => {
return {
wrap: true,
value: value ? `${value}` : ``,
...((index === 0 && { backgroundColor: indexColumnBackgroundColor }) || {}),
}
}

export default ({
stream, // writable stream target stream
readStreamData = async (data, options) => await writeXlsxFile(data, options),
iterableData, // iterator for each page of an array of objects
// order list of which indicates the header label,
// display function for the field,
// and key of the record.
// [{ key: string(supports lodash dot notation), label: string, display: function(value, {key, record, transform})}...]
transform,
//TODO: support multi-line headers in excel and csv when implemented
headers = null, // array of strings to use as headers
onWrite = _.noop, // function to intercept writing a page of records
}) => {
const excelData = [
_.map(
(value) => ({
value,
fontWeight: 'bold',
backgroundColor: headerBackgroundColor,
}),
headers || transformLabels(transform)
),
]

let cancel = false
let recordsWritten = 0
let columns = _.map(
(column) => ({ width: column.value.length }),
excelData[0]
)

return {
promise: (async () => {
for await (let r of iterableData) {
if (cancel) break
let row = F.mapIndexed(
(data, index) =>
convertToExcelCell(
data.display(_.get(data.key, r), {
key: data.key,
record: r,
transform,
}),
index
),
transform
)
columns = F.mapIndexed(
(value, index) => ({
width: Math.min(
Math.max(value.width, row[index].value.length),
maxColumnWidth
),
}),
columns
)
excelData.push(row)
recordsWritten = recordsWritten + _.getOr(1, 'recordCount', r)
await onWrite({ recordsWritten })
}

const readStream = await readStreamData(excelData, {
columns,
stickyRowsCount: 1,
})
for await (const chunk of readStream) {
stream.write(chunk)
}

await onWrite({ recordsWritten, isStreamDone: true })
await stream.end()
})(),
cancel() {
cancel = true
},
}
}
107 changes: 107 additions & 0 deletions packages/export/src/excel.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { PassThrough } from 'stream'
import _ from 'lodash/fp.js'
import excel from './excel.js'

let iterableData = [
{ name: 'record1', value: 1, nestedValue: { value: 'a' } },
{ name: 'record2', value: 2, nestedValue: { value: 'b' } },
{ name: 'record3', value: 3, nestedValue: { value: 'c' } },
]

let headers = ['NAME', 'Value', 'Nested Value']

let transform = [
{ key: 'name', label: 'THE,NAME', display: _.capitalize },
{ key: 'value', label: 'Value', display: _.identity },
{
key: 'nestedValue.value',
label: 'Value RecordName Key TransformLength',
display: (value, { key, record, transform }) =>
`${value} ${record.name} ${key} ${transform.length}`,
},
]

let expectExcelData = [
[
{ backgroundColor: '#999999', fontWeight: 'bold', value: 'NAME' },
{ backgroundColor: '#999999', fontWeight: 'bold', value: 'Value' },
{
backgroundColor: '#999999',
fontWeight: 'bold',
value: 'Nested Value',
},
],
[
{ backgroundColor: '#bbbbbb', value: 'Record1', wrap: true },
{ value: '1', wrap: true },
{ value: 'a record1 nestedValue.value 3', wrap: true },
],
[
{ backgroundColor: '#bbbbbb', value: 'Record2', wrap: true },
{ value: '2', wrap: true },
{ value: 'b record2 nestedValue.value 3', wrap: true },
],
[
{ backgroundColor: '#bbbbbb', value: 'Record3', wrap: true },
{ value: '3', wrap: true },
{ value: 'c record3 nestedValue.value 3', wrap: true },
],
]

describe('Excel export tests', () => {
it('Excel data transformation test', async () => {
const readStream = new PassThrough()
const destinationStream = new PassThrough()
readStream.end('fake data')

let finalizeData = new Promise((res, rej) => {
destinationStream.on('data', () => {})
destinationStream.on('end', () => res())
destinationStream.on('error', rej)
})

let resultData

let writeData = async (data) => {
resultData = data
return readStream
}

await excel({
stream: destinationStream,
readStreamData: writeData,
iterableData,
transform,
headers,
})
await finalizeData
expect(resultData).toStrictEqual(expectExcelData)
})
it('Write stream data test', async () => {
const readStream = new PassThrough()
const destinationStream = new PassThrough()
readStream.end(`Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`)

let destinationData = new Promise((res, rej) => {
let data = ''
destinationStream.on('data', (chunk) => {
data += chunk.toString()
})
destinationStream.on('end', () => res(data))
destinationStream.on('error', rej)
})

await excel({
stream: destinationStream,
readStreamData: () => readStream,
iterableData,
transform,
headers,
})

expect(await destinationData)
.toStrictEqual(`Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`)
})
})
3 changes: 2 additions & 1 deletion packages/export/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import csv from './csv.js'
import excel from './excel.js'
import * as nodes from './nodes/index.js'

export { nodes, csv }
export { nodes, csv, excel }
export * from './utils.js'
Loading
Loading