UNPKG

ffc-pay-etl-framework

Version:

A framework for creating ETL pipelines in node

142 lines (133 loc) 4.49 kB
const EventEmitter = require('node:events') const { Transform } = require('node:stream') const debug = require('debug')('destination') function isKeyWord (column) { return ['USER'].includes(column.toUpperCase()) } function getMappingForColumn (mapping, column) { if (mapping.length === 0) { return {} } const map = mapping.find(m => m.column === column) return map || {} } function hasReturningColumns (mapping) { return mapping.some(m => m.returning === true) } function getReturningColumns (mapping) { return mapping.filter(m => m.returning === true).map(m => m?.targetColumn) } function writeInsertStatement (columnMapping, table, chunk, schema, ignoredColumns = []) { const filteredColumns = chunk._columns.filter(column => { const mapping = getMappingForColumn(columnMapping, column) const targetColumn = mapping?.targetColumn ? mapping?.targetColumn : mapping?.column return !ignoredColumns.includes(targetColumn) }) let statement = `INSERT INTO ${schema ?? 'public'}."${table}" (${filteredColumns.map(column => { const mapping = getMappingForColumn(columnMapping, column) return mapping?.targetColumn ? `"${mapping.targetColumn}"` : `"${mapping?.column}"` }) .join(',')}) VALUES (${filteredColumns.map(column => { const index = chunk._columns.indexOf(column) const mapping = getMappingForColumn(columnMapping, column) if (mapping?.targetType === 'number' && (Number.isNaN(Number(chunk[index])) || chunk[index] === '')) { debug('Source data is not a number.') return 0 } if (mapping?.targetType === 'varchar' || mapping?.targetType === 'char') { return `'${chunk[index]}'` } if (mapping?.targetType === 'date') { if (!chunk[index]) { return '\'\'' } return `to_timestamp('${chunk[index]}','${mapping?.format}')` } return chunk[index] ? chunk[index] : 'null' })})` if (hasReturningColumns(columnMapping)) { statement = statement + ` RETURNING ${getReturningColumns(columnMapping).join(',')}` } return statement } /** * * @param {Object} options * @param {Object} options.table * @param {Object} options.connectionname * @param {Object} options.mapping * @param {Object} options.includeErrors * @param {String} [options.schema] * @param {Array<String>} [options.ignoredColumns] * @returns Transform */ function postgresDestination (options) { EventEmitter.call(this) const table = options.table const connectionname = options.connectionname const mapping = options.mapping const schema = options.schema const ignoredColumns = options.ignoredColumns ?? [] let lastChunk const transform = new Transform({ objectMode: true, emitClose: true, construct (callback) { // @ts-ignore this.connectionname = connectionname callback() }, write (chunk, _, callback) { let insertStatement // @ts-ignore if (chunk.errors.length === 0 || options.includeErrors) { insertStatement = writeInsertStatement(mapping, table, chunk, schema, ignoredColumns) debug('Insert statement: [%s]', insertStatement) // @ts-ignore this.connection.db.query(insertStatement) .then(result => { debug('result %o', result) chunk._result = result lastChunk = chunk // @ts-ignore this.tasks?.forEach(task => task.write(chunk)) // @ts-ignore callback(null, chunk) }).catch(error => { debug('error %o', error) chunk.errors.push(error) lastChunk = chunk // @ts-ignore callback(error, chunk) }) } else { debug('Chunk has errors %o', chunk) } }, final (callback) { this.emit('result', lastChunk) callback() } }) Object.assign(Transform.prototype, { type: 'PostgresDestination', setConnection: function (connection) { this.connection = connection }.bind(transform), getConnectionName: function () { return this.connection?.name }.bind(transform), setTasks: function (tasks) { this.tasks = tasks }.bind(transform) }) return transform } module.exports = { PostgresDestination: postgresDestination, writeInsertStatement, isKeyWord, getMappingForColumn, hasReturningColumns, getReturningColumns }