A framework-agnostic TypeScript library for tracking, replaying, and managing object changes with event-sourcing patterns
npm install @mbmj/change-trackingA framework-agnostic TypeScript library for tracking, replaying, and managing object changes using event-sourcing patterns.
- Event Sourcing - Immutable change records that can be replayed to reconstruct state
- Type-Safe - Strongly typed APIs with full TypeScript support
- Framework-Agnostic - Works with any TypeScript/JavaScript project
- Zero Runtime Dependencies - No external dependencies required
- Diff Detection - Identify inserts, updates, and deletions between datasets
``bash`
npm install @mbmj/change-tracking
`typescript
import { DocumentChangeParser, ChangeBuilder } from "@mbmj/change-tracking";
// Original object
const user = { name: "John", age: 25 };
// Create changes
const changes = [
ChangeBuilder.updateStringChange("name", "Jane"),
ChangeBuilder.updateNumberChange("age", 30),
];
// Apply changes (creates a new object)
const updatedUser = DocumentChangeParser.getCopyWithChanges(user, changes);
// Result: { name: 'Jane', age: 30 }
// Original is unchanged
console.log(user); // { name: 'John', age: 25 }
`
`typescript
import { ChangeBuilder } from "@mbmj/change-tracking";
const change = new ChangeBuilder()
.createString("name", "John")
.createNumber("age", 25)
.createObject("address", { city: "Paris", country: "France" })
.build({ createdBy: "user123" });
`
`typescript
import { DocumentChangeParser, ChangeBuilder } from "@mbmj/change-tracking";
const data = {
user: {
profile: {
name: "John",
settings: { theme: "light" },
},
},
};
const changes = [
ChangeBuilder.updateStringChange("user.profile.name", "Jane"),
ChangeBuilder.updateStringChange("user.profile.settings.theme", "dark"),
];
const updated = DocumentChangeParser.getCopyWithChanges(data, changes);
`
Use @ notation for array mutations:
`typescript
import {
DocumentChangeParser,
ChangeType,
PropertyType,
} from "@mbmj/change-tracking";
const data = { items: ["a", "b", "c"] };
// Insert at index 1
const insertChange = {
changes: [
{
type: ChangeType.CREATE,
propertyPath: "items@1",
newValue: "x",
newValueType: PropertyType.STRING,
},
],
};
DocumentChangeParser.applyChange(data, insertChange);
// Result: { items: ['a', 'x', 'b', 'c'] }
// Delete at index 1
const deleteChange = {
changes: [
{
type: ChangeType.DELETE,
propertyPath: "items@1",
},
],
};
DocumentChangeParser.applyChange(data, deleteChange);
// Result: { items: ['a', 'b', 'c'] }
`
Compare datasets to identify changes:
`typescript
import { DiffIdentifier } from "@mbmj/change-tracking";
interface Product {
id: string;
name: string;
price: number;
isDeleted?: boolean;
}
const existingProducts: Product[] = [
{ id: "1", name: "Widget", price: 10 },
{ id: "2", name: "Gadget", price: 20 },
];
const importedProducts = new Map
["Widget", { id: "1", name: "Widget", price: 15 }], // Updated price
["NewItem", { id: "3", name: "NewItem", price: 30 }], // New item
]);
const result = DiffIdentifier.identifyChanges(
importedProducts,
existingProducts,
{
idField: "id",
nameField: "name",
isDeletedField: "isDeleted",
},
);
console.log(result.inserts); // [{ id: '3', name: 'NewItem', price: 30 }]
console.log(result.updates); // [{ oldItem: {..., price: 10}, newItem: {..., price: 15} }]
`
#### ChangeType
`typescript`
enum ChangeType {
CREATE = "CREATE",
UPDATE = "UPDATE",
DELETE = "DELETE",
}
#### PropertyType
`typescript`
enum PropertyType {
STRING = "string",
NUMBER = "number",
OBJECT = "object",
}
#### ChangeDescriptor
`typescript`
interface ChangeDescriptor {
type: ChangeType;
propertyPath: string;
newValue?: string;
newValueType?: PropertyType;
}
#### DocumentChange
`typescript`
interface DocumentChange {
id?: string;
changes: ChangeDescriptor[];
createdBy?: string;
createdAt?: Date;
}
| Method | Description |
| ---------------------------------- | ------------------------------------------- |
| getCopyWithChanges(obj, changes) | Creates a deep copy and applies all changes |applyChanges(obj, changes)
| | Applies changes in place (mutates) |getCopyWithChange(obj, change)
| | Creates a deep copy and applies one change |applyChange(obj, change)
| | Applies one change in place |isValidPropertyPath(path)
| | Validates a property path |
#### Fluent API
`typescript`
new ChangeBuilder()
.createString(path, value)
.updateString(path, value)
.createNumber(path, value)
.updateNumber(path, value)
.createObject(path, value)
.updateObject(path, value)
.delete(path)
.build(options?)
.reset()
#### Static Methods
`typescript`
ChangeBuilder.createStringChange(path, value);
ChangeBuilder.updateStringChange(path, value);
ChangeBuilder.createNumberChange(path, value);
ChangeBuilder.updateNumberChange(path, value);
ChangeBuilder.createObjectChange(path, value);
ChangeBuilder.updateObjectChange(path, value);
ChangeBuilder.deleteChange(path);
| Method | Description |
| ----------------------------------------------------------------- | --------------------------- |
| identifyChanges(newItems, existingItems, config) | Finds inserts and updates |identifyInactive(existingItems, processedIds, config, options?)
| | Finds deleted/missing items |
Low-level utilities for nested object manipulation:
| Method | Description |
| --------------------------------------- | --------------------------- |
| setPropertyValue(obj, path, value) | Sets a nested property |getPropertyValue(obj, path, default?)
| | Gets a nested property |deleteProperty(obj, path)
| | Deletes a property |insertAtIndex(obj, path, value)
| | Inserts into array at index |removeAtIndex(obj, path)
| | Removes from array at index |setValueAtIndex(obj, path, value)
| | Updates array element |
| Syntax | Description | Example |
| -------- | ------------------------------ | ------------------- |
| name | Simple property | user |a.b.c
| | Nested property | user.profile.name |arr[0]
| | Array access | items[0].name |arr@0
| | Array mutation (insert/remove) | items@0 |arr@-1
| | Append to array | items@-1 |
The library provides typed errors:
`typescript
import {
InvalidPathError,
ConflictError,
ChangeTrackingError,
} from "@mbmj/change-tracking";
try {
DocumentChangeParser.applyChange(obj, change, { validatePaths: true });
} catch (error) {
if (error instanceof InvalidPathError) {
console.error("Invalid path:", error.details?.path);
}
}
`
`bash`
npm test
npm run test:coverage
`bash`
npm run build
Outputs:
- dist/index.js - CommonJSdist/index.mjs
- - ES Modulesdist/index.d.ts` - Type declarations
-
MIT