Skip to content

Commit dfdd02b

Browse files
fix: Ensure consistent JSON key order by sorting after all modifications (#249) 4acc744
1 parent a5cbe9d commit dfdd02b

File tree

2 files changed

+71
-23
lines changed

2 files changed

+71
-23
lines changed

src/localStorage.js

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,33 @@ const TOS_RESTRICTED_ATTRIBUTES = [
1717
"vehicleWaypoints",
1818
];
1919

20+
/**
21+
* Recursively sorts the keys of an object or objects within an array,
22+
* ensuring a consistent order for display and comparison.
23+
* @param {*} data The object or array to sort.
24+
* @returns {*} The sorted object or array.
25+
*/
26+
export function sortObjectKeysRecursively(data) {
27+
const _sort = (obj) => {
28+
if (obj === null || typeof obj !== "object") {
29+
return obj;
30+
}
31+
32+
if (Array.isArray(obj)) {
33+
return obj.map(_sort);
34+
}
35+
36+
return Object.keys(obj)
37+
.sort()
38+
.reduce((sorted, key) => {
39+
sorted[key] = _sort(obj[key]);
40+
return sorted;
41+
}, {});
42+
};
43+
44+
return _sort(data);
45+
}
46+
2047
async function openDB() {
2148
return new Promise((resolve, reject) => {
2249
const request = indexedDB.open(DB_NAME, 1);
@@ -32,8 +59,10 @@ export async function uploadFile(file, index) {
3259
console.log(`Importing file: ${file.name}`);
3360
let parsedData;
3461
if (file.name.endsWith(".zip")) {
62+
log("uploadFile: Processing ZIP file.");
3563
parsedData = await processZipFile(file);
3664
} else if (file.name.endsWith(".json")) {
65+
log("uploadFile: Processing JSON file.");
3766
parsedData = await processJsonFile(file);
3867
} else {
3968
throw new Error("Unsupported file format. Please upload a ZIP or JSON file.");
@@ -128,19 +157,6 @@ async function processJsonFile(file) {
128157
export function parseJsonContent(content) {
129158
log("Parsing JSON content");
130159

131-
const sortObjectKeys = (obj) => {
132-
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
133-
return obj;
134-
}
135-
136-
return Object.keys(obj)
137-
.sort()
138-
.reduce((sorted, key) => {
139-
sorted[key] = sortObjectKeys(obj[key]);
140-
return sorted;
141-
}, {});
142-
};
143-
144160
const processJsonObject = (obj) => {
145161
if (obj === null || typeof obj !== "object") return obj;
146162
if (Array.isArray(obj)) return obj.map(processJsonObject);
@@ -186,14 +202,14 @@ export function parseJsonContent(content) {
186202
const parsed = JSON.parse(content);
187203
const processedData = processJsonObject(parsed);
188204
log("Processed JSON data: removed underscores, flattened value objects, and pruned null/undefined fields");
189-
return sortObjectKeys(processedData);
205+
return sortObjectKeysRecursively(processedData);
190206
} catch (error) {
191207
log("Initial JSON parsing failed, attempting to wrap in array");
192208
try {
193209
const parsed = JSON.parse(`[${content}]`);
194210
const processedData = processJsonObject(parsed);
195211
log("Processed JSON data in array format");
196-
return sortObjectKeys(processedData);
212+
return sortObjectKeysRecursively(processedData);
197213
} catch (innerError) {
198214
console.error("JSON parsing error:", innerError);
199215
throw new Error(`Invalid JSON content: ${innerError.message}`);
@@ -271,6 +287,12 @@ export function ensureCorrectFormat(data) {
271287
return true;
272288
});
273289

290+
mergedLogs.forEach((row) => {
291+
if (row.jsonPayload) {
292+
row.jsonPayload = sortObjectKeysRecursively(row.jsonPayload);
293+
}
294+
});
295+
274296
// Determine the solution type based on the presence of _delivery_vehicle logs
275297
const isLMFS = mergedLogs.some((row) => row.logName?.includes("_delivery_vehicle"));
276298
const solutionType = isLMFS ? "LMFS" : "ODRD";

src/localStorage.test.js

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
11
// src/localStorage.test.js
2-
32
import fs from "fs";
4-
import { parseJsonContent, removeEmptyObjects, ensureCorrectFormat } from "./localStorage";
3+
import { parseJsonContent, removeEmptyObjects, ensureCorrectFormat, sortObjectKeysRecursively } from "./localStorage";
54

65
// Helper function to load test data
76
function loadTestData(filename) {
87
return JSON.parse(fs.readFileSync(`./datasets/${filename}`));
98
}
109

10+
test("sortObjectKeysRecursively sorts object keys recursively but preserves array order", () => {
11+
const unsorted = {
12+
c: 3,
13+
a: 1,
14+
b: [{ z: "last", x: "first" }, { y: "middle" }],
15+
};
16+
17+
const expected = {
18+
a: 1,
19+
b: [{ x: "first", z: "last" }, { y: "middle" }],
20+
c: 3,
21+
};
22+
23+
const sorted = sortObjectKeysRecursively(unsorted);
24+
25+
// Using JSON.stringify provides a simple and effective way to verify
26+
// both the structure and the key order of the entire object.
27+
expect(JSON.stringify(sorted)).toBe(JSON.stringify(expected));
28+
});
29+
1130
test("parseJsonContent handles valid JSON", () => {
1231
const validJson = JSON.stringify({ test: "data" });
1332
const result = parseJsonContent(validJson);
@@ -31,22 +50,28 @@ test("parseJsonContent handles JSON array", () => {
3150
});
3251

3352
test("parseJsonContent throws error for invalid JSON", () => {
53+
// Temporarily spy on console.error and replace it with a function that does nothing.
54+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
55+
3456
const invalidJson = "{invalid}";
3557
expect(() => parseJsonContent(invalidJson)).toThrow("Invalid JSON content");
58+
59+
consoleErrorSpy.mockRestore();
3660
});
3761

38-
test("parseJsonContent removes underscores from keys", () => {
62+
test("parseJsonContent removes underscores from keys and sorts them", () => {
3963
const snakeCaseJson = JSON.stringify({
4064
snake_case_key: "value",
41-
normal_key: "value2",
65+
another_key: "value2",
4266
});
4367

4468
const result = parseJsonContent(snakeCaseJson);
4569

4670
expect(result).toHaveProperty("snakecasekey", "value");
47-
expect(result).toHaveProperty("normalkey", "value2");
71+
expect(result).toHaveProperty("anotherkey", "value2");
4872
expect(result).not.toHaveProperty("snake_case_key");
49-
expect(result).not.toHaveProperty("normal_key");
73+
expect(result).not.toHaveProperty("another_key");
74+
expect(Object.keys(result)).toEqual(["anotherkey", "snakecasekey"]);
5075
});
5176

5277
test("parseJsonContent removes underscores from deeply nested object keys", () => {
@@ -65,17 +90,18 @@ test("parseJsonContent removes underscores from deeply nested object keys", () =
6590
});
6691

6792
// New tests for value object flattening
68-
test("parseJsonContent flattens objects with a single 'value' property", () => {
93+
test("parseJsonContent flattens value objects and sorts keys", () => {
6994
const valueObjectJson = JSON.stringify({
70-
normalKey: "normal",
7195
valueObject: { value: "flattened" },
96+
normalKey: "normal",
7297
});
7398

7499
const result = parseJsonContent(valueObjectJson);
75100

76101
expect(result.normalKey).toBe("normal");
77102
expect(result.valueObject).toBe("flattened");
78103
expect(typeof result.valueObject).toBe("string");
104+
expect(Object.keys(result)).toEqual(["normalKey", "valueObject"]);
79105
});
80106

81107
test("parseJsonContent flattens nested objects with a single 'value' property", () => {

0 commit comments

Comments
 (0)