UNPKG

@report-toolkit/transformers

Version:

See docs at [https://ibm.github.io/report-toolkit](https://ibm.github.io/report-toolkit)

999 lines (864 loc) 26.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var common = require('@report-toolkit/common'); var json2csv = require('json2csv'); var stripAnsi = _interopDefault(require('strip-ansi')); var stringify = _interopDefault(require('fast-safe-stringify')); var hashjs = _interopDefault(require('hash.js')); var CLITable3 = _interopDefault(require('cli-table3')); var wrapAnsi = _interopDefault(require('wrap-ansi')); /** * CSV transformer. Accepts an object or {@link Report}, and outputs CSV. * @module @report-toolkit/transformers.csv */ const { filter, concatMapTo, finalize, fromEvent, map, takeUntil, tap } = common.observable; /** * @type {TransformerMeta} */ const meta = { description: 'Comma-separated values', id: 'csv', input: ['object', 'report'], output: 'string' }; /** * CSV transformer; accepts whatever json2csv can handle * @see https://npm.im/json2csv * @param {CSVTransformOptions} [parserOpts] * @type {TransformFunction<string|object,CSVTransformResult>} */ const transform = (parserOpts = {}) => observable => { // XXX: the parser wants to add a newline to everything we push to it for // some reason. likely "user error" const parser = new json2csv.AsyncParser({ ...parserOpts, eol: '', flatten: true }, { objectMode: true }); return observable.pipe(finalize(() => { parser.input.push(null); }), tap(row => { parser.input.push(row); }), concatMapTo(fromEvent(parser.processor, 'data')), takeUntil(fromEvent(parser.processor, 'end')), filter(Boolean), map(common._.pipe(common._.trim, stripAnsi))); }; /** * This is a single CSV-formatted row * @typedef {string} CSVTransformResult */ /** * Options for AsyncParser * @typedef {object} CSVTransformOptions */ /** * @typedef {import('@report-toolkit/common').Report} Report * @typedef {import('./transformer.js').TransformerMeta} TransformerMeta * @typedef {import('./transformer.js').TransformerField} TransformerField */ /** * @template T,U * @typedef {import('./transformer.js').TransformFunction<T,U>} TransformFunction */ var csv = /*#__PURE__*/Object.freeze({ __proto__: null, meta: meta, transform: transform }); /** * A general-purpose filtering Transformer, which allows whitelisting or * blacklisting of properties from output. * @module @report-toolkit/transformers.filter */ const { map: map$1 } = common.observable; const debug = common.createDebugPipe('transformer', 'filter'); /** * @type {TransformerMeta} */ const meta$1 = { description: 'Filters properties', id: 'filter', input: ['report'], output: 'object' }; /** * @param {FilterTransformerOptions} opts * @type {TransformFunction<object,object>} */ const transform$1 = ({ include = [], exclude = [] } = {}) => { /** * @type {Function[]} */ const filterFns = [common._.identity]; if (include.length) { filterFns.push(common._.pick(include)); } if (exclude.length) { filterFns.push(common._.omit(exclude)); } const filterFn = common._.pipe.apply(null, filterFns); return observable => observable.pipe(map$1(filterFn), debug(data => [`filtered data: %O`, data])); }; /** * @typedef {import('@report-toolkit/common').Report} Report * @typedef {import('./transformer.js').TransformerMeta} TransformerMeta * @typedef {{include?: string[], exclude?: string[]}} FilterTransformerOptions */ /** * @template T,U * @typedef {import('./transformer.js').TransformFunction<T,U>} TransformFunction */ var filter$1 = /*#__PURE__*/Object.freeze({ __proto__: null, meta: meta$1, transform: transform$1 }); /** * JSON transformer; outputs JSON. * @module @report-toolkit/transformers.json */ const { map: map$2, toArray, pipeIf, mergeAll } = common.observable; const debug$1 = common.createDebugPipe('transformers', 'json'); /** * @type {TransformerMeta} */ const meta$2 = { defaults: /** * @type {Partial<JSONTransformOptions>} */ { pretty: false }, description: 'JSON', id: 'json', input: ['string', 'object', 'number', 'report'], output: 'string' }; /** * Emits a single JSON blob. * @param {Partial<JSONTransformOptions>} [opts] * @type {TransformFunction<object,string>} */ const transform$2 = ({ pretty = false } = {}) => observable => observable.pipe(toArray(), pipeIf(result => result.length === 1, mergeAll()), debug$1(values => [`transforming to JSON with pretty = ${pretty}`, values]), map$2(pretty ? values => stringify(values, null, 2) : values => stringify(values))); /** * @typedef {object} JSONTransformOptions * @property {TransformerField[]} fields * @property {boolean} pretty */ /** * @typedef {import('@report-toolkit/common').Report} Report * @typedef {import('./transformer.js').TransformerMeta} TransformerMeta * @typedef {import('./transformer.js').TransformerField} TransformerField */ /** * @template T,U * @typedef {import('./transformer.js').TransformFunction<T,U>} TransformFunction */ var json = /*#__PURE__*/Object.freeze({ __proto__: null, meta: meta$2, transform: transform$2 }); /** * A transformer which outputs newline-delimited output. Can be used to output * newline-delimited JSON ("ndjson"). * @module @report-toolkit/transformers.newline */ const { concatMap, of } = common.observable; /** * @type {TransformerMeta} */ const meta$3 = { defaults: /** * @type {NewlineTransformOptions} */ { json: true }, description: 'Newline-delimited output', id: 'newline', input: ['string', 'object', 'number'], output: 'string' }; /** * Newline parser; given whatever, output a string ending with newline (or `newline` of your choice) * @param {Partial<NewlineTransformOptions>} [opts] * @type {TransformFunction<any,string>} */ const transform$3 = ({ json = meta$3.defaults.json } = {}) => observable => observable.pipe(concatMap(value => of(json || common._.isObject(value) ? stringify(value) : String(value)))); /** * @typedef {object} NewlineTransformOptions * @property {boolean} json - If true, force-stringify the value. Objects will always be stringified */ /** * @typedef {import('@report-toolkit/common').Report} Report * @typedef {import('./transformer.js').TransformerMeta} TransformerMeta * @typedef {import('./transformer.js').TransformerField} TransformerField */ /** * @template T,U * @typedef {import('./transformer.js').TransformFunction<T,U>} TransformFunction */ var newline = /*#__PURE__*/Object.freeze({ __proto__: null, meta: meta$3, transform: transform$3 }); /** * Transformer which redacts secrets from a {@link Report}. This transformer is * _always_ run unless the user explicitly disables it. * @module @report-toolkit/transformers.redact */ const { map: map$3 } = common.observable; /** * @type {TransformerMeta} */ const meta$4 = { description: 'Redact secrets from a report', id: 'redact', input: ['report'], output: 'report' }; /** * @type {TransformFunction<Report,Report>} */ const transform$4 = (opts = {}) => observable => observable.pipe(map$3(report => common.redact(report, opts))); /** * @typedef {import('@report-toolkit/common').Report} Report * @typedef {import('./transformer.js').TransformerMeta} TransformerMeta */ /** * @template T,U * @typedef {import('./transformer.js').TransformFunction<T,U>} TransformFunction */ var redact = /*#__PURE__*/Object.freeze({ __proto__: null, meta: meta$4, transform: transform$4 }); /** * @module @report-toolkit/transformers.stack-hash */ const { map: map$4 } = common.observable; /** * @type {TransformerMeta} */ const meta$5 = { defaults: /** @type {StackHashTransformOptions} */ { fields: [{ label: 'SHA1', value: 'sha1' }, { label: 'Time', value: 'dumpEventTime' }, { label: 'File', value: 'filepath' }, { label: 'Error', value: 'message' }, { label: 'Stack', value: 'stack' }], strip: /[0-9]+/g }, description: 'Generate unique hash for JS stack trace', id: 'stack-hash', input: ['report'], output: 'object' }; /** * Given a report, generate a SHA1 hash of the stack trace. Useful when * determining whether a stack trace is new or already known. * @param {Partial<StackHashTransformOptions>} [opts] - Options * @type {TransformFunction<Report,StackHashTransformResult>} */ const transform$5 = ({ strip } = {}) => observable => observable.pipe(map$4(report => { // @ts-ignore const { dumpEventTime } = report.header; const { filepath } = report; // @ts-ignore const { message, stack } = report.javascriptStack; const strippedMessage = common._.isFunction(strip) ? /** @type {((arg: string) => string)} */ strip(message) : message.replace(strip, ''); return { dumpEventTime, filepath, message: strippedMessage, sha1: hashjs.sha1().update(`${strippedMessage}${stack.join(',')}`).digest('hex'), stack }; })); /** * @typedef {{dumpEventTime:string,filename?:string,sha1:string,message:string}} StackHashTransformResult * @typedef {{fields: TransformerField[], strip: RegExp|function(string):string|string}} StackHashTransformOptions */ /** * @typedef {import('@report-toolkit/common').Report} Report * @typedef {import('./transformer.js').TransformerMeta} TransformerMeta * @typedef {import('./transformer.js').TransformerField} TransformerField */ /** * @template T,U * @typedef {import('./transformer.js').TransformFunction<T,U>} TransformFunction */ var stackHash = /*#__PURE__*/Object.freeze({ __proto__: null, meta: meta$5, transform: transform$5 }); var version = "0.6.1"; /** * @module @report-toolkit/transformers.table */ const debug$2 = common.createDebugPipe('transformers', 'table'); const { concatMap: concatMap$1, from, map: map$5, pipeIf: pipeIf$1, reduce } = common.observable; const DEFAULT_TABLE_OPTS = { chars: { bottom: '', 'bottom-left': '', 'bottom-mid': '', 'bottom-right': '', left: '', 'left-mid': '', mid: '─', 'mid-mid': '', middle: '', right: '', 'right-mid': '', top: '', 'top-left': '', 'top-mid': '', 'top-right': '' }, /** * @type {number[]} */ colWidthPcts: [], /** * @type {Field[]} */ fields: [], style: { /** * @type {object[]} */ head: [] }, wordWrap: true }; const fieldWidthPcts = common._.pipe(common._.map('widthPct'), common._.map(Number)); /** * @param {(...args: any[]) => string | string} v */ const constantValue = v => common._.isFunction(v) ? v : common._.constant(v); /** * @param {(arg: CLITable3.Table) => string | string} header * @param {CLITable3.Table} value * @returns {string} */ const withHeader = (header, value) => { header = constantValue(header); return common.colors.grey('[') + common.colors.cyan().bold('report-toolkit') + ' ' + common.colors.cyan(`v${version}`) + common.colors.grey('] ') + common.colors.magenta(header(value)) + ` `; }; /** * @param {(arg: CLITable3.Table) => string | string} footer * @param {CLITable3.Table} value * @returns {string} */ const withFooter = (footer, value) => { /** * @type {(arg: CLITable3.Table) => string} */ footer = constantValue(footer); return ` ${footer(value)}`; }; const safeSum = common._.reduce((sum, pct) => common._.isNaN(pct) ? sum : sum + pct, 0); /** * This little nasty accepts a list of field objects with `widthPct` * props and a `colWidths` array. It prefers the `colWidths` array. * It calculates column widths _as a percentage_ of maxWidth. * @todo "Infinity" should be a problem * @param {Field[]} fields * @param {number[]} colWidths * @param {number} maxWidth * @returns {number[]} */ const normalizeColWidthPcts = (fields, colWidths, maxWidth) => { if (!common._.isEmpty(colWidths)) { const maxPct = 100 - safeSum(colWidths) / maxWidth * 100; const fieldsCount = common._.size(fields) - common._.size(common._.filter(common._.isNumber, colWidths)); return common._.map(width => typeof width === 'number' ? Math.floor(width / maxWidth * 100) : Math.floor(maxPct / fieldsCount), [...colWidths, ...new Array(fields.length - colWidths.length).fill(null)]); } const colWidthPcts = fieldWidthPcts(fields); if (common._.some(common._.isNaN, colWidthPcts)) { const maxPct = 100 - safeSum(colWidthPcts); if (maxPct < 100) { const fieldsCount = common._.size(fields) - common._.size(common._.filter(common._.isNumber, colWidthPcts)); return common._.map(pct => common._.isNaN(pct) ? Math.floor(maxPct / fieldsCount) : pct, colWidthPcts); } else { const fieldsCount = common._.size(fields); return new Array(fieldsCount).fill(Math.floor(100 / fieldsCount)); } } return colWidthPcts; }; /** * * @param {Field[]} [fields] - Field settings * @param {number[]} [colWidths] - Fixed column widths * @param {number} [maxWidth] - Maximum column width * @returns {number[]} */ const calculateColumnWidths = (fields = [], colWidths = [], maxWidth = 80) => common._.map( // normalize column widths to total max width pct => Math.floor(pct / 100 * maxWidth), // normalize column widths based on explicit colWidths option normalizeColWidthPcts(fields, colWidths, maxWidth)); /** * @param {Field[]} fields - Field whose table headers need formatting * @returns {string[]} Formatted headers */ const formatTableHeaders = common._.pipe(common._.map('label'), common._.map(v => common.colors.underline(v))); /** * * @param {Object} [opts] - Options * @returns {CLITable3.Table} */ const createTable = (opts = {}) => { opts = common._.defaultsDeep(DEFAULT_TABLE_OPTS, opts); const { fields, maxWidth, truncate, colWidths } = opts; if (truncate) { opts.colWidths = calculateColumnWidths(fields, colWidths, maxWidth); } return new CLITable3({ // "truncate" is used by CLITable3 for the truncation symbol. ...common._.omit('truncate', opts), head: formatTableHeaders(fields), truncate: '…' }); }; const colValuesByFields = common._.curry( /** * @param {Field[]} fields * @param {object} row * @returns {string[]} */ (fields, row) => common._.map(common._.invokeArgs('value', [row]), fields)); /** * @type {TransformerMeta} */ const meta$6 = { description: 'Tabular output', id: 'table', input: ['object'], output: 'string' }; /** * @returns {TransformFunction<object,string>} */ const transform$6 = (opts = {}) => { const table = createTable(opts); const { fields, outputFooter, outputHeader, wrap } = opts; const colValues = colValuesByFields(fields); const padding = table.options.style['padding-left'] - table.options.style['padding-right']; return observable => observable.pipe(debug$2( /** * @param {object} value */ // @ts-ignore value => [`received data %O`, value]), map$5(colValues), pipeIf$1(wrap, map$5( // this force-wraps the column text common._.map( /** * @param {string} col * @param {string | number} idx */ (col, idx) => wrapAnsi(col, table.options.colWidths[idx] - padding, { hard: true, wordWrap: false })))), reduce((table, row) => { // `push` must be used because Table subclasses Array, but // doesn't implement concat, so we'd just get a plain Array back... table.push(row); return table; }, table), concatMap$1(table => { const isTableEmpty = common._.isEmpty(table); /** * @type {Array<string|CLITable3.Table>} */ const output = isTableEmpty ? [] : [table]; if (!isTableEmpty) { if (outputHeader) { output.unshift(withHeader(outputHeader, table)); } if (outputFooter) { output.push(withFooter(outputFooter, table)); } } return from(output); }), map$5(String)); }; /** * @typedef {import('@report-toolkit/common').Report} Report * @typedef {import('./transformer.js').TransformerField} Field * @typedef {import('./transformer.js').TransformerMeta} TransformerMeta */ /** * @template T,U * @typedef {import('./transformer.js').TransformFunction<T,U>} TransformFunction */ var table = /*#__PURE__*/Object.freeze({ __proto__: null, meta: meta$6, transform: transform$6 }); /** * @module @report-toolkit/transformers */ const { RTKERR_INVALID_TRANSFORMER_PIPE, createRTkError } = common.error; const FIELD_COLORS = Object.freeze(['cyan', 'magenta', 'blue', 'green']); const debug$3 = common.createDebugger('transformers', 'transformer'); /** * @type {Partial<TransformerMeta>} */ const DEFAULT_TRANSFORMER_META = Object.freeze({ alias: [], input: ['report'] }); const configMap = new WeakMap(); /** * Represents a Transformer having a transform() function and * metadata. * @template T,U */ class Transformer { /** * Sets defaults and instance props * @param {TransformFunction<T,U>} transform * @param {TransformerMeta} meta - Transformer metdata * @param {object} [config] - Transformer config */ constructor(transform, meta, config = {}) { this._meta = common._.defaultsDeep(DEFAULT_TRANSFORMER_META, meta); this._transform = transform; if (config.fields) { config.fields = Transformer.normalizeFields(config.fields); } configMap.set(this, config); debug$3(`created Transform with id "%s" and config %O`, this._meta.id, config); } get id() { return this._meta.id; } get input() { return this._meta.input; } get output() { return this._meta.output; } get defaults() { return this._meta.defaults; } /** * Pipe one Transformer to another * @param {Transformer<any,any>} transformer * @returns {Transformer<any,any>} */ pipe(transformer) { if (!this.canPipeTo(transformer)) { throw createRTkError(RTKERR_INVALID_TRANSFORMER_PIPE, `Transformer "${this.id}" cannot pipe to transformer "${transformer.id}"`); } return transformer.pipeFrom(this); } transform() { let defaults = [this.defaults]; if (this._source && configMap.has(this._source)) { const sourceOpts = configMap.get(this._source); defaults = common._.defaults({ fields: sourceOpts.fields }, defaults); } return this._transform(common._.defaultsDeep(defaults, configMap.get(this))); } /** * * @param {Transformer<any,any>} transformer * @returns {Transformer<any,any>} */ pipeFrom(transformer) { this._source = transformer; return this; } /** * Returns `true` if this Transformer can pipe to another * @param {Transformer<any,any>} transformer - Transformer to compare */ canPipeTo(transformer) { return common._.includes(this.output, transformer.input); } canBeginWith(type) { return common._.includes(type, this.input); } canEndWith(type) { return this.output === type; } /** * Creates a Transformer * @template T,U * @param {TransformFunction<T,U>} transform - Transformer function * @param {TransformerMeta} meta - Transformer meta * @param {object} [config] - Transformer config * @returns {Transformer<T,U>} */ static create(transform, meta, config = {}) { return new Transformer(transform, meta, config); } } Transformer.normalizeFields = common._.pipe(common._.toPairs, common._.map( /** * @param {[number, TransformerField]} value */ ([idx, field]) => { // a field can have a string `color`, no `color`, or a function which accepts a `row` and returns a string. // likewise, it can have a `value` function which accepts a `row` and returns a value, or just a string, which // corresponds to a property of the `row` object. const fieldColor = field.color || FIELD_COLORS[idx % FIELD_COLORS.length]; const colorFn = common._.isFunction(fieldColor) ? (row, value) => { // the function might not return a color const result = /** @type {((arg0: any) => string)} */ fieldColor(row); const color = common.colors[result] ? result : FIELD_COLORS[idx % FIELD_COLORS.length]; return common.colors[color](value); } : (row, value) => common.colors[ /** @type {string} */ fieldColor](value); const valueFn = common._.isFunction(field.value) ? row => { // yuck const fn = /** * @type {function(typeof row): string} */ field.value; return fn(row); } : common._.get(field.value); return { ...field, value: row => colorFn(row, valueFn(row)) }; })); const createTransformer = Transformer.create; /** * @typedef {"json"|"csv"|"table"} Formatters * @typedef {{label: string, value: string|function(any): string, color?: string|function(any): string}} TransformerField * @typedef {import('@report-toolkit/common').Report} Report */ /** * @typedef {{id: string, description?: string, input?: string[], output: string, alias?: string[], defaults?: any}} TransformerMeta */ /** * @template T,U * @typedef {import('rxjs/internal/types').OperatorFunction<T,U>} OperatorFunction<T,U> */ /** * @template T,U * @typedef {(opts?: object)=>OperatorFunction<T,U>} TransformFunction */ const builtinTransformers = [csv, filter$1, json, newline, redact, stackHash, table]; const transformerModules = common._.fromPairs(common._.map(transformer => [transformer.meta.id, transformer], builtinTransformers)); const DEFAULT_TRANSFORMER = 'table'; const { RTKERR_INVALID_TRANSFORMER_HEAD, RTKERR_UNKNOWN_TRANSFORMER, createRTkError: createRTkError$1 } = common.error; const { concatMap: concatMap$2, map: map$6, mergeAll: mergeAll$1, pipeIf: pipeIf$2, share, switchMap, tap: tap$1, toArray: toArray$1, throwRTkError } = common.observable; const debug$4 = common.createDebugPipe('transformers'); /** * @type {Readonly<string[]>} */ const builtinTransformerIds = Object.freeze(common._.map('meta.id', builtinTransformers)); /** * @param {string} id - Transformer ID * @param {object} [config] - Configuration */ const loadTransformer = (id, config = {}) => { if (!isKnownTransformer(id)) { throw createRTkError$1(RTKERR_UNKNOWN_TRANSFORMER, `Unknown transformer "${id}"`); } const { meta, transform } = transformerModules[id]; return createTransformer( /** @type {TransformFunction<any,any>} */ transform, meta, config); }; /** * @param {string} id */ const isKnownTransformer = id => Boolean(transformerModules[id]); /** * @returns {OperatorFunction<TransformerConfig,Transformer<any,any>>} */ const toTransformer = () => observable => observable.pipe(pipeIf$2(({ id }) => !isKnownTransformer(id), switchMap(({ id }) => throwRTkError(RTKERR_UNKNOWN_TRANSFORMER, `Unknown transformer "${id}"`))), map$6(({ id, config }) => loadTransformer(id, config))); /** * * @param {Partial<TransformOptions>} [opts] * @returns {OperatorFunction<Transformer<any,any>,Transformer<any,any>>} */ const validateTransformerChain = ({ beginWith = 'report', endWith = 'string', defaultTransformer = DEFAULT_TRANSFORMER, defaultTransformerConfig = {} } = {}) => observable => observable.pipe(toArray$1(), debug$4(transformers => [`validating chain of transformers: %O`, common._.map('id', transformers)]), tap$1( /** * @param {Transformer[]} transformers */ transformers => { let idx = 0; let transformer = transformers[idx]; if (!transformer.canBeginWith(beginWith)) { // TODO: list valid transformers (using URL?) throw createRTkError$1(RTKERR_INVALID_TRANSFORMER_HEAD, `The first transformer ("${transformer.id}") must accept a "${beginWith}"`); } if (!transformers[transformers.length - 1].canEndWith(endWith)) { transformers.push(loadTransformer(defaultTransformer, defaultTransformerConfig)); } let nextTransformer = transformers[++idx]; while (nextTransformer) { transformer = transformer.pipe(nextTransformer); nextTransformer = transformers[++idx]; } return transformers; }), debug$4(transformers => `transformer pipe {${beginWith}} => ${common._.map('id', transformers).join(' => ')} => {${endWith}} OK`), mergeAll$1()); /** * @param {Observable<any>} source */ const runTransformer = source => /** * @param {Observable<Transformer<any,any>>} observable */ observable => observable.pipe(toArray$1(), debug$4(transformers => `running transform(s): ${common._.map('id', transformers).join(' => ')}`), concatMap$2(transformers => // @ts-ignore source.pipe(share(), ...common._.map(transformer => transformer.transform(), transformers)))); /** * Returns a list of transformers which can accept data of type `sourceType` * @todo memoize * @todo constants for source types * @param {string} sourceType */ const compatibleTransformers = sourceType => common._.keys(common._.fromPairs(common._.filter(([, transformerModule]) => common._.includes(sourceType, transformerModule.meta.input), common._.toPairs(transformerModules)))); /** * @typedef {import('./transformer.js').TransformerMeta} TransformerMeta * @typedef {import('@report-toolkit/common').Report} Report */ /** * @template T,U * @typedef {import('./transformer.js').TransformFunction<T,U>} TransformFunction */ /** * @template T * @typedef {import('rxjs').Observable<T>} Observable */ /** * @template T,U * @typedef {import('rxjs').OperatorFunction<T,U>} OperatorFunction */ /** * @typedef {{id: string, config: object}} TransformerConfig */ /** * for {@link validateTransformerChain} * @typedef {object} TransformOptions * @property {string} beginWith - Begin transformer chain with this type * @property {string} endWith - End transformer chain with this type * @property {string} defaultTransformer - Default transformer * @property {object} defaultTransformerConfig - Default transformer config */ exports.Transformer = Transformer; exports.builtinTransformerIds = builtinTransformerIds; exports.compatibleTransformers = compatibleTransformers; exports.isKnownTransformer = isKnownTransformer; exports.loadTransformer = loadTransformer; exports.runTransformer = runTransformer; exports.toTransformer = toTransformer; exports.validateTransformerChain = validateTransformerChain; //# sourceMappingURL=report-toolkit-transformers.cjs.js.map