Skip to content

Commit c4a1886

Browse files
nyamathshaikclaude
andcommitted
feat: Add native Date object support to fast-json-patch
This commit adds proper handling for JavaScript Date objects in JSON Patch operations. ## Changes ### 1. Enhanced `_deepClone` function (src/helpers.ts) - Added special handling for Date objects to preserve them as Date instances - Changed from JSON.stringify/parse approach to recursive cloning - Date objects are now cloned using `new Date(obj.getTime())` to maintain type and value - Arrays and objects are recursively cloned to handle nested Date objects ### 2. Enhanced `_generate` function (src/duplex.ts) - Added Date-specific comparison logic - Date objects are now compared by value (using `getTime()`) instead of by reference - When two Date objects have different timestamps, a replace patch is generated - Preserves Date objects in the generated patches (not converted to ISO strings) ### 3. Added comprehensive test suite (test/spec/dateSpec.mjs) - Tests for cloning Date objects - Tests for comparing objects with Date properties - Tests for nested Date objects in arrays and objects - Real-world scenario test simulating calendar event workflows ## Benefits - **Type Preservation**: Date objects remain as Date instances throughout patch operations - **Correct Comparisons**: Dates are compared by value, not reference - **Backward Compatible**: All existing tests pass (215 specs, 0 failures) - **Real-world Use Case**: Solves issues when working with API responses containing dates ## Use Case This fix addresses the common scenario where workflow contexts contain Date objects (e.g., calendar events, timestamps, scheduled tasks) and need to generate accurate diffs without converting dates to strings. Example: ```javascript const oldContext = { events: [] }; const newContext = { events: [{ startTime: new Date('2025-11-15T10:00:00Z'), endTime: new Date('2025-11-15T11:00:00Z') }] }; const patches = compare(oldContext, newContext); // Patches now contain actual Date objects, not ISO strings ``` ## Testing All tests pass: - New Date-specific tests: 11 specs, 0 failures - Existing core tests: 215 specs, 0 failures Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9d313ac commit c4a1886

8 files changed

Lines changed: 11116 additions & 10838 deletions

File tree

commonjs/duplex.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,19 @@ function _generate(mirror, obj, patches, path, invertible) {
132132
if (helpers_js_1.hasOwnProperty(obj, key) && !(obj[key] === undefined && oldVal !== undefined && Array.isArray(obj) === false)) {
133133
var newVal = obj[key];
134134
if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null && Array.isArray(oldVal) === Array.isArray(newVal)) {
135-
_generate(oldVal, newVal, patches, path + "/" + helpers_js_1.escapePathComponent(key), invertible);
135+
// Special handling for Date objects: compare by value, not reference
136+
if (oldVal instanceof Date && newVal instanceof Date) {
137+
if (oldVal.getTime() !== newVal.getTime()) {
138+
changed = true;
139+
if (invertible) {
140+
patches.push({ op: "test", path: path + "/" + helpers_js_1.escapePathComponent(key), value: helpers_js_1._deepClone(oldVal) });
141+
}
142+
patches.push({ op: "replace", path: path + "/" + helpers_js_1.escapePathComponent(key), value: helpers_js_1._deepClone(newVal) });
143+
}
144+
}
145+
else {
146+
_generate(oldVal, newVal, patches, path + "/" + helpers_js_1.escapePathComponent(key), invertible);
147+
}
136148
}
137149
else {
138150
if (oldVal !== newVal) {

commonjs/helpers.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,25 @@ exports._objectKeys = _objectKeys;
5252
function _deepClone(obj) {
5353
switch (typeof obj) {
5454
case "object":
55-
return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5
55+
if (obj === null) {
56+
return null;
57+
}
58+
// Handle Date objects specially to preserve them as Date instances
59+
if (obj instanceof Date) {
60+
return new Date(obj.getTime());
61+
}
62+
// Handle Arrays
63+
if (Array.isArray(obj)) {
64+
return obj.map(function (item) { return _deepClone(item); });
65+
}
66+
// Handle plain objects - recursively clone each property
67+
var cloned = {};
68+
for (var key in obj) {
69+
if (hasOwnProperty(obj, key)) {
70+
cloned[key] = _deepClone(obj[key]);
71+
}
72+
}
73+
return cloned;
5674
case "undefined":
5775
return null; //this is how JSON.stringify behaves for array items
5876
default:

module/duplex.mjs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,19 @@ function _generate(mirror, obj, patches, path, invertible) {
128128
if (hasOwnProperty(obj, key) && !(obj[key] === undefined && oldVal !== undefined && Array.isArray(obj) === false)) {
129129
var newVal = obj[key];
130130
if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null && Array.isArray(oldVal) === Array.isArray(newVal)) {
131-
_generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key), invertible);
131+
// Special handling for Date objects: compare by value, not reference
132+
if (oldVal instanceof Date && newVal instanceof Date) {
133+
if (oldVal.getTime() !== newVal.getTime()) {
134+
changed = true;
135+
if (invertible) {
136+
patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) });
137+
}
138+
patches.push({ op: "replace", path: path + "/" + escapePathComponent(key), value: _deepClone(newVal) });
139+
}
140+
}
141+
else {
142+
_generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key), invertible);
143+
}
132144
}
133145
else {
134146
if (oldVal !== newVal) {

module/helpers.mjs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,25 @@ export function _objectKeys(obj) {
4949
export function _deepClone(obj) {
5050
switch (typeof obj) {
5151
case "object":
52-
return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5
52+
if (obj === null) {
53+
return null;
54+
}
55+
// Handle Date objects specially to preserve them as Date instances
56+
if (obj instanceof Date) {
57+
return new Date(obj.getTime());
58+
}
59+
// Handle Arrays
60+
if (Array.isArray(obj)) {
61+
return obj.map(function (item) { return _deepClone(item); });
62+
}
63+
// Handle plain objects - recursively clone each property
64+
var cloned = {};
65+
for (var key in obj) {
66+
if (hasOwnProperty(obj, key)) {
67+
cloned[key] = _deepClone(obj[key]);
68+
}
69+
}
70+
return cloned;
5371
case "undefined":
5472
return null; //this is how JSON.stringify behaves for array items
5573
default:

0 commit comments

Comments
 (0)