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
JavaScript
"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