firestore-snapshot-utils
Version:
[](https://github.com/ericvera/firestore-snapshot-utils/blob/master/LICENSE) [ • 5.23 kB
JavaScript
import { diff } from 'jest-diff';
import { createHash } from 'node:crypto';
import { extractTimestamps } from './extractTimestamps.js';
import { normalizeData } from './normalizeData.js';
const noop = (a) => a;
const getDiff = (a, b) => {
const text = diff(a ?? {}, b ?? {}, {
omitAnnotationLines: true,
// Disable color markers generated for console logging
aColor: noop,
bColor: noop,
commonColor: noop,
changeColor: noop,
patchColor: noop,
contextLines: 0,
});
if (text?.includes('no visual difference') === true) {
return;
}
return text?.toString();
};
const generateHash = (data) => {
const hashData = JSON.stringify(data);
return createHash('md5')
.update(hashData)
.digest('base64')
.replaceAll(/[/\\+=]/g, 'a');
};
/**
* Creates a normalized ID for a document path based on the content of the
* document whose path is received (e.g. parent path for a subcCollection item).
*/
const getNormalizedIDForPath = (path, docs) => {
const doc = docs.find((d) => d.ref.path === path);
if (!doc) {
throw new Error(`Document not found for path: ${path}`);
}
const currentData = doc.data();
// NOTE: This will only normalize the timestamps within the current document.
// and not across all documents. So if there is a single timestamp it will
// just be normalized to /Timestamp 0000/ rather than a timestamp matching
// across the snapshot.
// Extract and sort timestamps from this document
const timestampValues = extractTimestamps(currentData);
const sortedTimestamps = Array.from(timestampValues).sort();
// Replace timestamps and buffers with normalized values
const normalizedData = normalizeData(currentData, sortedTimestamps);
return generateHash(normalizedData);
};
/**
* Returns a normalized path for a document, replacing the last segment with
* '[ID]' and normalizing the parent path if it is a subcollection.
*/
const getDocPath = (doc, allDocs) => {
const pathSegments = doc.ref.path.split('/');
// Handle subcollections (length 4)
if (pathSegments.length === 4) {
// Since we've checked length is 4, we know these elements exist
const parentPath = pathSegments.slice(0, 2).join('/');
pathSegments[1] = getNormalizedIDForPath(parentPath, allDocs);
}
else if (pathSegments.length > 4) {
throw new Error('Path segments longer than 4 not supported yet.');
}
// Always replace the last segment with [ID]
pathSegments[pathSegments.length - 1] = '[ID]';
return pathSegments.join('/');
};
// Proposed base class
class BaseDocumentSnapshot {
doc;
normalizedData;
normalizedPath;
constructor(doc, allDocs, normalizedData) {
this.doc = doc;
this.normalizedData = normalizedData;
this.normalizedPath = getDocPath(doc, allDocs);
}
}
export class AddedDocumentSnapshot extends BaseDocumentSnapshot {
addedDoc;
normalizedAddedData;
constructor(addedDoc, normalizedAddedData, allDocs) {
super(addedDoc, allDocs, normalizedAddedData);
this.addedDoc = addedDoc;
this.normalizedAddedData = normalizedAddedData;
}
/**
* Returns a string with the differences between two objects or undefined if
* there are no differences.
* Timestamps are normalized to a string representation before comparison.
*/
getDiff() {
return getDiff(undefined, this.normalizedAddedData);
}
}
export class RemovedDocumentSnapshot extends BaseDocumentSnapshot {
normalizedRemovedData;
constructor(removedDoc, normalizedRemovedData, allDocs) {
super(removedDoc, allDocs, normalizedRemovedData);
this.normalizedRemovedData = normalizedRemovedData;
}
/**
* Returns a string with the differences between two objects or undefined if
* there are no differences.
* Timestamps are normalized to a string representation before comparison.
*/
getDiff() {
return getDiff(this.normalizedRemovedData, undefined);
}
}
export class ModifiedDocumentSnapshot extends BaseDocumentSnapshot {
beforeDoc;
afterDoc;
normalizedBeforeData;
normalizedAfterData;
constructor(beforeDoc, afterDoc, normalizeBeforeData, normalizeAfterData, allDocs) {
super(afterDoc, allDocs, normalizeAfterData);
this.beforeDoc = beforeDoc;
this.afterDoc = afterDoc;
this.normalizedBeforeData = normalizeBeforeData;
this.normalizedAfterData = normalizeAfterData;
}
/**
* Returns a string with the differences between two objects or undefined if
* there are no differences.
* Timestamps are normalized to a string representation before comparison.
*/
getDiff() {
return getDiff(this.normalizedBeforeData, this.normalizedAfterData);
}
}
export class UnmodifiedDocumentSnapshot extends BaseDocumentSnapshot {
/**
* Returns a string with the differences between two objects or undefined if
* there are no differences.
* Timestamps are normalized to a string representation before comparison.
*/
getDiff() {
return;
}
}