-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgenerateSingleRecordPdf.js
132 lines (111 loc) · 5.54 KB
/
generateSingleRecordPdf.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
const PdfKit = require('pdfkit-table') // pdfkit-table extends pdfkit
const fs = require('fs-extra')
const { uploadFromDiskToS3, downloadFileToDisk } = require('./helpers')
const C = require('./constants')
const generateAndSavePdfToDisk = async (record, tableSchema, filePathConfig) => {
const doc = new PdfKit()
doc.pipe(fs.createWriteStream(filePathConfig.onDiskFullPath))
await pdfContent(doc, record, tableSchema, filePathConfig)
doc.end()
return filePathConfig.onDiskFullPath
}
const pdfContent = async (doc, record, tableSchema, filePathConfig) => {
// Use table schema to create mappings
const { primaryFieldId, fields: allFields, view } = tableSchema
const visibleFields = allFields.filter(f => view.visibleFieldIds.includes(f.id))
const fieldsByFieldId = Object.fromEntries(visibleFields.map(f => [f.id, f]))
// Determine non-attachment fields' names (which will be displayed in a table of field name-value pairs)
const fieldsIdsToDisplayInTable = Object.entries(fieldsByFieldId).map(f => f[1].type !== 'multipleAttachments' ? f[0] : false).filter(Boolean)
// Determine attachment fields' names (which we will have their contents replicated to S3 and previewed in the PDF if possible)
const attachmentFieldIdsToReplicateAndPreview = Object.entries(fieldsByFieldId).map(f => f[1].type === 'multipleAttachments' ? f[0] : false).filter(Boolean)
resetTextStyle(doc)
styledText(doc, `${record.fields[primaryFieldId]}`, { bold: true, fontSize: 20, fillColor: C.DEFAULT_TEXT_ACCENT_COLOR })
styledText(doc, 'View record in Airtable', { link: `https://airtable.com/${filePathConfig.baseTableRecordAsPath}`, fillColor: 'blue', fontSize: 12 })
doc.moveDown(2)
// Format fields object into an array of arrays and display table
const fieldsAsKeyValueArrays = fieldsIdsToDisplayInTable
.map(fid => [fieldsByFieldId[fid].name, record.fields[fid]])
.filter(a => typeof (a[1]) !== 'object') // do not render objects though
.filter(a => a[0] !== fieldsByFieldId[primaryFieldId].name) // or the primary field
styledText(doc, 'Note: Only fields with non-empty string or numeric values are included in the table below', { fontSize: 12, moveDown: 1 })
doc.table({
// title: 'Fields',
// subtitle: 'Note: Only fields with non-empty string or numeric values are included in the table below',
headers: [
{ label: 'Name', width: 125 },
{ label: 'Value', width: 300 }
],
rows: fieldsAsKeyValueArrays
}, {
prepareHeader: () => doc.font(C.DEFAULT_FONT_BOLD).fontSize(12),
prepareRow: (row, indexColumn, indexRow, rectRow, rectCell) => {
doc.font(C.DEFAULT_FONT_REGULAR).fontSize(12)
if (indexRow % 2 !== 0) { // Shadow every other row
doc.addBackground(rectRow, 'grey', 0.05)
}
}
})
for (const fieldId of attachmentFieldIdsToReplicateAndPreview) {
doc.addPage()
styledText(doc, fieldsByFieldId[fieldId].name, { bold: true })
if (record.fields[fieldId]) {
const imagesEnriched = await processArrayOfAttachments(record.fields[fieldId], filePathConfig)
displayAndLinkToAttachment(doc, imagesEnriched)
} else {
styledText(doc, 'No attachments found', { fontSize: C.DEFAULT_FONT_SIZE * 0.8 })
}
}
return doc
}
const resetTextStyle = (doc) => {
doc.fontSize(C.DEFAULT_FONT_SIZE).font(C.DEFAULT_FONT_REGULAR).fillColor(C.DEFAULT_TEXT_COLOR)
}
const styledText = (doc, text, options = {}) => {
options = { bold: false, fillColor: C.DEFAULT_TEXT_COLOR, fontSize: C.DEFAULT_FONT_SIZE, moveDown: 0, link: null, ...options }
const font = options.bold ? C.DEFAULT_FONT_BOLD : C.DEFAULT_FONT_REGULAR
doc
.font(font)
.fillColor(options.fillColor)
.fontSize(options.fontSize)
.text(text, { link: options.link })
.moveDown(options.moveDown)
}
const processArrayOfAttachments = async (attachments, filePathConfig) => {
// For each attachment...
const attachmentsEnriched = await Promise.all(attachments.map(async (i) => {
// Determine its new filename and paths
const filenameWithId = `${i.id}__${i.filename}`
const onDiskFullPath = `${filePathConfig.onDiskDirectoryPath}/${filenameWithId}`
const onS3DirectoryPath = filePathConfig.baseTableRecordAsPath
// Download the file to disk (Airtable provides a temporary URL)
await downloadFileToDisk(i.url, onDiskFullPath)
// If the file is preview-eligible, convert it to a base64 string (which is necessary to embed a preview in the PDF)
let base64String
if (C.IMAGE_TYPES_TO_PREVIEW.includes(i.type)) {
base64String = await fs.readFile(onDiskFullPath).then(buf => `data:${i.type};base64,` + buf.toString('base64'))
}
// Upload the file to S3 as publicly available
const uploadFileResult = await uploadFromDiskToS3(onDiskFullPath, onS3DirectoryPath, filenameWithId, i.type, 'public-read')
return {
...i,
nonExpiringS3Url: uploadFileResult.Location,
...(base64String && { base64String })
}
}))
return attachmentsEnriched
}
const displayAndLinkToAttachment = (doc, enrichedAttachments) => {
for (const attachment of enrichedAttachments) {
// If available, embed the base64 image preview
if (attachment.base64String) {
doc.image(attachment.base64String, { height: 250, link: attachment.nonExpiringS3Url })
doc.moveDown(0.5)
}
// Link to public file on S3
const cannotBeEmbeddedText = attachment.base64String ? '' : '(No preview)'
styledText(doc, `${attachment.filename} ${cannotBeEmbeddedText}`, { link: attachment.nonExpiringS3Url, fillColor: 'blue', moveDown: 2 })
}
}
module.exports = {
generateAndSavePdfToDisk
}