UNPKG

tabular-data-differ

Version:

A very efficient library for diffing two sorted streams of tabular data, such as CSV files.

1,379 lines 110 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs_1 = __importDefault(require("fs")); const globals_1 = require("@jest/globals"); const differ_1 = require("./differ"); const formats_1 = require("./formats"); const streams_1 = require("./streams"); class FakeFormatWriter { constructor() { this.diffs = []; } open() { return Promise.resolve(); } writeHeader(header) { this.header = header; return Promise.resolve(); } writeDiff(rowDiff) { this.diffs.push(rowDiff); return Promise.resolve(); } writeFooter(footer) { this.footer = footer; return Promise.resolve(); } close() { return Promise.resolve(); } } async function diffStrings(options) { const writer = new FakeFormatWriter(); await (0, differ_1.diff)({ ...options, oldSource: { format: 'csv', stream: new streams_1.ArrayInputStream(options.oldLines) }, newSource: { format: 'csv', stream: new streams_1.ArrayInputStream(options.newLines), }, }).to({ destination: { format: 'custom', writer, }, keepSameRows: options.keepSameRows, changeLimit: options.changeLimit, }); return writer; } function readAllText(path) { return fs_1.default.readFileSync(path).toString(); } (0, globals_1.describe)('differ', () => { beforeAll(() => { if (!fs_1.default.existsSync('./output')) { fs_1.default.mkdirSync('./output'); } if (!fs_1.default.existsSync('./output/files')) { fs_1.default.mkdirSync('./output/files'); } }); (0, globals_1.describe)('validation errors', () => { (0, globals_1.test)('should reject unknown source format', async () => { await (0, globals_1.expect)(async () => { await (0, differ_1.diff)({ oldSource: { format: 'foobar', stream: './tests/a.csv', }, newSource: { format: 'csv', stream: './tests/b.csv', }, keys: ['id'], }).to('null'); }).rejects.toThrowError(`Unknown source format 'foobar'`); }); (0, globals_1.test)('should reject unknown destination format', async () => { await (0, globals_1.expect)(async () => { await (0, differ_1.diff)({ oldSource: { format: 'csv', stream: './tests/a.csv', }, newSource: { format: 'csv', stream: './tests/b.csv', }, keys: ['id'], }).to({ destination: { format: 'foo', stream: 'console', }, }); }).rejects.toThrowError(`Unknown destination format 'foo'`); }); (0, globals_1.test)('should detect invalid ordering in ascending mode', async () => { await (0, globals_1.expect)(() => diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '3,dave,44', '2,rachel,22', ], keys: ['ID'], })).rejects.toThrowError(`Expected rows to be ordered by \"ID ASC\" in new source but received: previous=3,dave,44 current=2,rachel,22`); }); (0, globals_1.test)('should detect invalid ordering in descending mode', async () => { await (0, globals_1.expect)(() => diffStrings({ oldLines: [ 'ID,NAME,AGE', '3,dave,44', '2,rachel,22', '1,john,33', ], newLines: [ 'ID,NAME,AGE', '3,dave,44', '1,john,33', '2,rachel,22', ], keys: [{ name: 'ID', order: 'DESC', }], })).rejects.toThrowError(new differ_1.UnorderedStreamsError(`Expected rows to be ordered by "ID DESC" in new source but received: previous=1,john,33 current=2,rachel,22`)); }); (0, globals_1.test)('should detect primary key violation in old source', async () => { await (0, globals_1.expect)(() => diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', '3,dave bis,444', '4,noemie,11', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], keys: ['ID'], })).rejects.toThrowError(new differ_1.UniqueKeyViolationError(`Expected rows to be unique by "ID" in old source but received: previous=3,dave,44 current=3,dave bis,444 Note that you can resolve this conflict automatically using the duplicateKeyHandling option.`)); }); (0, globals_1.test)('should detect primary key violation in new source', async () => { await (0, globals_1.expect)(() => diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', '3,dave bis,444', '4,noemie,11', ], keys: ['ID'], })).rejects.toThrowError(new differ_1.UniqueKeyViolationError(`Expected rows to be unique by "ID" in new source but received: previous=3,dave,44 current=3,dave bis,444 Note that you can resolve this conflict automatically using the duplicateKeyHandling option.`)); }); (0, globals_1.test)('should detect duplicate keys and return the first row', async () => { const writer = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', '3,dave bis,444', '4,noemie,11', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], keys: ['ID'], duplicateKeyHandling: 'keepFirstRow', keepSameRows: true, }); (0, globals_1.expect)(writer.diffs).toEqual([ { delta: 0, status: 'same', oldRow: ['1', 'john', '33'], newRow: ['1', 'john', '33'] }, { delta: 0, status: 'same', oldRow: ['2', 'rachel', '22'], newRow: ['2', 'rachel', '22'] }, { delta: 0, status: 'same', oldRow: ['3', 'dave', '44'], newRow: ['3', 'dave', '44'] }, { delta: -1, status: 'deleted', oldRow: ['4', 'noemie', '11'] } ]); }); (0, globals_1.test)('should detect duplicate keys and return the last row', async () => { const writer = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', '3,dave bis,444', '4,noemie,11', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], keys: ['ID'], duplicateKeyHandling: 'keepLastRow', keepSameRows: true, }); (0, globals_1.expect)(writer.diffs).toEqual([ { delta: 0, status: 'same', oldRow: ['1', 'john', '33'], newRow: ['1', 'john', '33'] }, { delta: 0, status: 'same', oldRow: ['2', 'rachel', '22'], newRow: ['2', 'rachel', '22'] }, { delta: 0, status: 'modified', oldRow: ['3', 'dave bis', '444'], newRow: ['3', 'dave', '44'] }, { delta: -1, status: 'deleted', oldRow: ['4', 'noemie', '11'] } ]); }); (0, globals_1.test)('should detect duplicate keys and call aggregate function', async () => { let duplicateRows = []; const duplicateKeyHandler = (rows) => { if (duplicateRows.length === 0) { duplicateRows = rows; } return rows[rows.length - 1]; }; const writer = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', '3,dave bis,444', '4,noemie,11', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], keys: ['ID'], duplicateKeyHandling: duplicateKeyHandler, keepSameRows: true, }); (0, globals_1.expect)(writer.diffs).toEqual([ { delta: 0, status: 'same', oldRow: ['1', 'john', '33'], newRow: ['1', 'john', '33'] }, { delta: 0, status: 'same', oldRow: ['2', 'rachel', '22'], newRow: ['2', 'rachel', '22'] }, { delta: 0, status: 'modified', oldRow: ['3', 'dave bis', '444'], newRow: ['3', 'dave', '44'] }, { delta: -1, status: 'deleted', oldRow: ['4', 'noemie', '11'] } ]); (0, globals_1.expect)(duplicateRows).toEqual([ ['3', 'dave', '44'], ['3', 'dave bis', '444'] ]); }); (0, globals_1.test)('should detect duplicate keys and call aggregate function, with buffer overflow', async () => { const dups = []; for (let i = 0; i < 100; i++) { dups.push(`3,dave bis${i},444`); } let duplicateRows = []; const duplicateKeyHandler = (rows) => { if (duplicateRows.length === 0) { duplicateRows = rows; } return rows[rows.length - 1]; }; const writer = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ...dups, '4,noemie,11', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], keys: ['ID'], duplicateKeyHandling: duplicateKeyHandler, duplicateRowBufferOverflow: true, duplicateRowBufferSize: 10, keepSameRows: true, }); (0, globals_1.expect)(writer.diffs).toEqual([ { delta: 0, status: 'same', oldRow: ['1', 'john', '33'], newRow: ['1', 'john', '33'] }, { delta: 0, status: 'same', oldRow: ['2', 'rachel', '22'], newRow: ['2', 'rachel', '22'] }, { delta: 0, status: 'modified', oldRow: ['3', 'dave bis99', '444'], newRow: ['3', 'dave', '44'] }, { delta: -1, status: 'deleted', oldRow: ['4', 'noemie', '11'] } ]); (0, globals_1.expect)(duplicateRows).toEqual([ ['3', 'dave bis90', '444'], ['3', 'dave bis91', '444'], ['3', 'dave bis92', '444'], ['3', 'dave bis93', '444'], ['3', 'dave bis94', '444'], ['3', 'dave bis95', '444'], ['3', 'dave bis96', '444'], ['3', 'dave bis97', '444'], ['3', 'dave bis98', '444'], ['3', 'dave bis99', '444'] ]); }); (0, globals_1.test)('should detect duplicate keys and throw an error when the buffer exceeds the limit', async () => { const dups = []; for (let i = 0; i < 10; i++) { dups.push('3,dave bis,444'); } (0, globals_1.expect)(diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ...dups, '4,noemie,11', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], keys: ['ID'], duplicateKeyHandling: (rows) => rows[0], duplicateRowBufferSize: 5, keepSameRows: true, })).rejects.toThrowError('Too many duplicate rows'); }); (0, globals_1.test)('should be able to execute twice', async () => { const differ = (0, differ_1.diff)({ oldSource: './tests/a.csv', newSource: './tests/b.csv', keys: ['id'], }); const stats1 = await differ.to('./output/files/output.csv'); const output1 = readAllText('./output/files/output.csv'); const stats2 = await differ.to('./output/files/output.csv'); const output2 = readAllText('./output/files/output.csv'); (0, globals_1.expect)(stats1.totalChanges).toBe(6); (0, globals_1.expect)(stats2).toEqual(stats1); (0, globals_1.expect)(output2).toEqual(output1); }); (0, globals_1.test)('should not open output file twice', async () => { const f = new streams_1.FileOutputStream('./output/files/output.csv'); await f.open(); try { await (0, globals_1.expect)(async () => await f.open()).rejects.toThrowError('file \"./output/files/output.csv\" is already open'); } finally { await f.close(); } }); (0, globals_1.test)('should have columns in old source', async () => { await (0, globals_1.expect)(() => diffStrings({ oldLines: [], newLines: [ 'ID,NAME,AGE', ], keys: ['ID'], })).rejects.toThrowError('Expected to find columns in old source'); }); (0, globals_1.test)('should have columns in new source', async () => { await (0, globals_1.expect)(() => diffStrings({ oldLines: [ 'ID,NAME,AGE', ], newLines: [], keys: ['ID'], })).rejects.toThrowError('Expected to find columns in new source'); }); (0, globals_1.test)('should find keys in old columns', async () => { await (0, globals_1.expect)(() => diffStrings({ oldLines: [ 'CODE,NAME,AGE', ], newLines: [ 'ID,NAME,AGE', ], keys: ['ID'], })).rejects.toThrowError(`Could not find key 'ID' in old stream`); }); (0, globals_1.test)('should find keys in new columns', async () => { await (0, globals_1.expect)(() => diffStrings({ oldLines: [ 'ID,NAME,AGE', 'a1,a,33', ], newLines: [ 'CODE,NAME,AGE', 'a1,a,33', ], keys: ['ID'], })).rejects.toThrowError(`Could not find key 'ID' in new stream`); }); (0, globals_1.test)('should not allow calling diffs() twice', async () => { const ctx = await (0, differ_1.diff)({ oldSource: './tests/a.csv', newSource: './tests/b.csv', keys: ['id'], }).start(); const diffs = []; for await (const rowDiff of ctx.diffs()) { diffs.push(rowDiff); } (0, globals_1.expect)(diffs.length).toBe(11); (0, globals_1.expect)(ctx.isOpen).toBeFalsy(); await (0, globals_1.expect)(async () => { for await (const rowDiff of ctx.diffs()) { } }).rejects.toThrowError('Cannot get diffs on closed streams. You should call "Differ.start()" again.'); }); (0, globals_1.test)('should not allow calling to() twice', async () => { const ctx = await (0, differ_1.diff)({ oldSource: './tests/a.csv', newSource: './tests/b.csv', keys: ['id'], }).start(); (0, globals_1.expect)(ctx.isOpen).toBeTruthy(); const stats = await ctx.to('null'); (0, globals_1.expect)(stats.totalComparisons).toBe(11); (0, globals_1.expect)(ctx.isOpen).toBeFalsy(); await (0, globals_1.expect)(async () => { await ctx.to('null'); }).rejects.toThrowError('Cannot get diffs on closed streams. You should call "Differ.start()" again.'); }); (0, globals_1.test)('should allow calling start() twice', async () => { const differ = (0, differ_1.diff)({ oldSource: './tests/a.csv', newSource: './tests/b.csv', keys: ['id'], }); const ctx = await differ.start(); (0, globals_1.expect)(ctx.isOpen).toBeTruthy(); const diffs = []; for await (const rowDiff of ctx.diffs()) { diffs.push(rowDiff); } (0, globals_1.expect)(diffs.length).toBe(11); (0, globals_1.expect)(ctx.isOpen).toBeFalsy(); const ctx2 = await differ.start(); (0, globals_1.expect)(ctx2.isOpen).toBeTruthy(); (0, globals_1.expect)(ctx2).not.toBe(ctx); const diffs2 = []; for await (const rowDiff of ctx2.diffs()) { diffs2.push(rowDiff); } (0, globals_1.expect)(ctx2.isOpen).toBeFalsy(); (0, globals_1.expect)(diffs2.length).toBe(11); (0, globals_1.expect)(diffs2).toEqual(diffs); }); }); (0, globals_1.describe)('changes', () => { (0, globals_1.test)('both files are empty', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', ], newLines: [ 'ID,NAME,AGE', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 0, totalChanges: 0, added: 0, modified: 0, deleted: 0, same: 0, changePercent: 0, }); }); (0, globals_1.test)('old is empty', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([ { status: 'added', delta: 1, oldRow: undefined, newRow: ['1', 'john', '33'], }, { status: 'added', delta: 1, oldRow: undefined, newRow: ['2', 'rachel', '22'], }, ]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 2, totalChanges: 2, added: 2, modified: 0, deleted: 0, same: 0, changePercent: 100, }); }); (0, globals_1.test)('new is empty', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', ], newLines: [ 'ID,NAME,AGE', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([ { status: 'deleted', delta: -1, oldRow: ['1', 'john', '33'], newRow: undefined, }, { status: 'deleted', delta: -1, oldRow: ['2', 'rachel', '22'], newRow: undefined, }, ]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 2, totalChanges: 2, added: 0, modified: 0, deleted: 2, same: 0, changePercent: 100, }); }); (0, globals_1.test)('same and do not keep same rows', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 0, added: 0, modified: 0, deleted: 0, same: 3, changePercent: 0, }); }); (0, globals_1.test)('same and keep same rows', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], keys: ['ID'], keepSameRows: true, }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([ { status: 'same', delta: 0, oldRow: ['1', 'john', '33'], newRow: ['1', 'john', '33'], }, { status: 'same', delta: 0, oldRow: ['2', 'rachel', '22'], newRow: ['2', 'rachel', '22'], }, { status: 'same', delta: 0, oldRow: ['3', 'dave', '44'], newRow: ['3', 'dave', '44'], }, ]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 0, added: 0, modified: 0, deleted: 0, same: 3, changePercent: 0, }); }); (0, globals_1.test)('same with reordered columns', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,AGE,NAME', '1,33,john', '2,22,rachel', '3,44,dave', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'AGE', 'NAME']); (0, globals_1.expect)(res.diffs).toEqual([]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 0, added: 0, modified: 0, deleted: 0, same: 3, changePercent: 0, }); }); (0, globals_1.test)('same with excluded columns', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,20', '3,dave,44', ], keys: ['ID'], excludedColumns: ['AGE'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME']); (0, globals_1.expect)(res.diffs).toEqual([]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 0, added: 0, modified: 0, deleted: 0, same: 3, changePercent: 0, }); }); (0, globals_1.test)('same with included columns', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,20', '3,dave,44', ], keys: ['ID'], includedColumns: ['ID', 'NAME'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME']); (0, globals_1.expect)(res.diffs).toEqual([]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 0, added: 0, modified: 0, deleted: 0, same: 3, changePercent: 0, }); }); (0, globals_1.test)('1 modified', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,20', '3,dave,44', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([{ status: 'modified', delta: 0, oldRow: ['2', 'rachel', '22'], newRow: ['2', 'rachel', '20'], }]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 1, added: 0, modified: 1, deleted: 0, same: 2, changePercent: 33.33, }); }); (0, globals_1.test)('all modified', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,30', '2,rachel,20', '3,dave,40', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([ { status: 'modified', delta: 0, oldRow: ['1', 'john', '30'], newRow: ['1', 'john', '33'], }, { status: 'modified', delta: 0, oldRow: ['2', 'rachel', '20'], newRow: ['2', 'rachel', '22'], }, { status: 'modified', delta: 0, oldRow: ['3', 'dave', '40'], newRow: ['3', 'dave', '44'], }, ]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 3, added: 0, modified: 3, deleted: 0, same: 0, changePercent: 100, }); }); (0, globals_1.test)('1 modified with reordered columns', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,AGE,NAME', '1,33,john', '2,20,rachel', '3,44,dave', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'AGE', 'NAME']); (0, globals_1.expect)(res.diffs).toEqual([{ status: 'modified', delta: 0, oldRow: ['2', '22', 'rachel'], newRow: ['2', '20', 'rachel'], }]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 1, added: 0, modified: 1, deleted: 0, same: 2, changePercent: 33.33, }); }); (0, globals_1.test)('1 modified with excluded columns', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rach,22', '3,dave,44', ], keys: ['ID'], excludedColumns: ['AGE'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME']); (0, globals_1.expect)(res.diffs).toEqual([{ status: 'modified', delta: 0, oldRow: ['2', 'rachel'], newRow: ['2', 'rach'], }]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 1, added: 0, modified: 1, deleted: 0, same: 2, changePercent: 33.33, }); }); (0, globals_1.test)('1 added with excluded columns', async () => { // this test will also help boost code coverage in normalizeOldRow const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', ], keys: ['ID'], excludedColumns: ['AGE'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME']); (0, globals_1.expect)(res.diffs).toEqual([ { delta: 1, status: 'added', newRow: ['2', 'rachel'] } ]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 2, totalChanges: 1, added: 1, modified: 0, deleted: 0, same: 1, changePercent: 50, }); }); (0, globals_1.test)('1 deleted with excluded columns', async () => { // this test will also help boost code coverage in normalizeNewRow const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', ], newLines: [ 'ID,NAME,AGE', '1,john,33', ], keys: ['ID'], excludedColumns: ['AGE'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME']); (0, globals_1.expect)(res.diffs).toEqual([ { delta: -1, status: 'deleted', oldRow: ['2', 'rachel'] } ]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 2, totalChanges: 1, added: 0, modified: 0, deleted: 1, same: 1, changePercent: 50, }); }); (0, globals_1.test)('1 modified with included columns', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rach,22', '3,dave,44', ], keys: ['ID'], includedColumns: ['ID', 'NAME'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME']); (0, globals_1.expect)(res.diffs).toEqual([{ status: 'modified', delta: 0, oldRow: ['2', 'rachel'], newRow: ['2', 'rach'], }]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 1, added: 0, modified: 1, deleted: 0, same: 2, changePercent: 33.33, }); }); (0, globals_1.test)('No modification but adding a new column should force the rows to be modified', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE,NEW_COL', '1,john,33,new1', '2,rachel,22,new2', '3,dave,44,new3', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE', 'NEW_COL']); (0, globals_1.expect)(res.diffs).toEqual([ { delta: 0, status: 'modified', oldRow: ['1', 'john', '33', ''], newRow: ['1', 'john', '33', 'new1'] }, { delta: 0, status: 'modified', oldRow: ['2', 'rachel', '22', ''], newRow: ['2', 'rachel', '22', 'new2'] }, { delta: 0, status: 'modified', oldRow: ['3', 'dave', '44', ''], newRow: ['3', 'dave', '44', 'new3'] } ]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 3, added: 0, modified: 3, deleted: 0, same: 0, changePercent: 100, }); }); (0, globals_1.test)('No modification but removing an old column should be transparent', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE,REMOVED_COL', '1,john,33,rem1', '2,rachel,22,rem2', '3,dave,44,rem3', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 0, added: 0, modified: 0, deleted: 0, same: 3, changePercent: 0, }); }); (0, globals_1.test)('1 deleted', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '3,dave,44', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([{ status: 'deleted', delta: -1, oldRow: ['2', 'rachel', '22'], newRow: undefined, }]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 1, added: 0, modified: 0, deleted: 1, same: 2, changePercent: 33.33, }); }); (0, globals_1.test)('1 added', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,20', '3,dave,44', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([{ status: 'added', delta: 1, oldRow: undefined, newRow: ['2', 'rachel', '20'], }]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 3, totalChanges: 1, added: 1, modified: 0, deleted: 0, same: 2, changePercent: 33.33, }); }); (0, globals_1.test)('only new rows and previous rows have been deleted', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '4,paula,11', '5,jane,66', ], keys: ['ID'], }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([ { status: 'deleted', delta: -1, oldRow: ['1', 'john', '33'], newRow: undefined, }, { status: 'deleted', delta: -1, oldRow: ['2', 'rachel', '22'], newRow: undefined, }, { status: 'deleted', delta: -1, oldRow: ['3', 'dave', '44'], newRow: undefined, }, { status: 'added', delta: 1, oldRow: undefined, newRow: ['4', 'paula', '11'], }, { status: 'added', delta: 1, oldRow: undefined, newRow: ['5', 'jane', '66'], }, ]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 5, totalChanges: 5, added: 2, modified: 0, deleted: 3, same: 0, changePercent: 100, }); }); (0, globals_1.test)('same, modified, added and deleted', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,22', '3,dave,44', ], newLines: [ 'ID,NAME,AGE', '1,john,33', '2,rachel,20', '4,paula,11', ], keys: ['ID'], keepSameRows: true, }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([ { status: 'same', delta: 0, oldRow: ['1', 'john', '33'], newRow: ['1', 'john', '33'], }, { status: 'modified', delta: 0, oldRow: ['2', 'rachel', '22'], newRow: ['2', 'rachel', '20'], }, { status: 'deleted', delta: -1, oldRow: ['3', 'dave', '44'], newRow: undefined, }, { status: 'added', delta: 1, oldRow: undefined, newRow: ['4', 'paula', '11'], }, ]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 4, totalChanges: 3, added: 1, modified: 1, deleted: 1, same: 1, changePercent: 75, }); }); (0, globals_1.test)('same, modified, added and deleted with a case insensitive primary key', async () => { const caseInsensitiveCompare = function (a, b) { if (typeof a === 'string' && typeof b === 'string') { return (0, formats_1.stringComparer)(a.toLowerCase(), b.toLowerCase()); } return (0, formats_1.cellComparer)(a, b); }; const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', 'a1,john,33', 'a2,rachel,22', 'a3,dave,44', ], newLines: [ 'ID,NAME,AGE', 'A1,john,33', 'A2,rachel,20', 'A4,paula,11', ], keys: [ { name: 'ID', comparer: caseInsensitiveCompare, } ], keepSameRows: true, }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([ { status: 'same', delta: 0, oldRow: ['a1', 'john', '33'], newRow: ['A1', 'john', '33'], }, { status: 'modified', delta: 0, oldRow: ['a2', 'rachel', '22'], newRow: ['A2', 'rachel', '20'], }, { status: 'deleted', delta: -1, oldRow: ['a3', 'dave', '44'], newRow: undefined, }, { status: 'added', delta: 1, oldRow: undefined, newRow: ['A4', 'paula', '11'], }, ]); (0, globals_1.expect)(res.footer?.stats).toEqual({ totalComparisons: 4, totalChanges: 3, added: 1, modified: 1, deleted: 1, same: 1, changePercent: 75, }); }); (0, globals_1.test)('same, modified, added and deleted, in descending order', async () => { const res = await diffStrings({ oldLines: [ 'ID,NAME,AGE', '3,dave,44', '2,rachel,22', '1,john,33', ], newLines: [ 'ID,NAME,AGE', '4,paula,11', '2,rachel,20', '1,john,33', ], keys: [{ name: 'ID', order: 'DESC', }], keepSameRows: true, }); (0, globals_1.expect)(res.header?.columns).toEqual(['ID', 'NAME', 'AGE']); (0, globals_1.expect)(res.diffs).toEqual([ { status: 'added