@typed-web/form-utils 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/array-path-to-string-path.d.ts +15 -0
- package/dist/lib/array-path-to-string-path.d.ts.map +1 -0
- package/dist/lib/flat-object-to-nested-object.d.ts +21 -0
- package/dist/lib/flat-object-to-nested-object.d.ts.map +1 -0
- package/dist/lib/get-arrays-diff.d.ts +15 -0
- package/dist/lib/get-arrays-diff.d.ts.map +1 -0
- package/dist/lib/nested-object-to-flat-object.d.ts +21 -0
- package/dist/lib/nested-object-to-flat-object.d.ts.map +1 -0
- package/dist/lib/set-value-at-path.d.ts +27 -0
- package/dist/lib/set-value-at-path.d.ts.map +1 -0
- package/dist/lib/string-path-to-array-path.d.ts +18 -0
- package/dist/lib/string-path-to-array-path.d.ts.map +1 -0
- package/dist/lib/types.d.ts +24 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/package.json +47 -0
- package/src/index.ts +15 -0
- package/src/lib/array-path-to-string-path.ts +24 -0
- package/src/lib/flat-object-to-nested-object.ts +32 -0
- package/src/lib/get-arrays-diff.ts +33 -0
- package/src/lib/nested-object-to-flat-object.ts +51 -0
- package/src/lib/set-value-at-path.ts +85 -0
- package/src/lib/string-path-to-array-path.ts +95 -0
- package/src/lib/types.ts +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Łukasz Jagodziński
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @typed-web/form-utils
|
|
2
|
+
|
|
3
|
+
Utilities for form handling: path conversion, object flattening, and array diffing.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @typed-web/form-utils
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Exports
|
|
12
|
+
|
|
13
|
+
### Object Conversion
|
|
14
|
+
- `flatObjectToNestedObject` - Convert flat objects to nested structures
|
|
15
|
+
- `nestedObjectToFlatObject` - Convert nested objects to flat structures
|
|
16
|
+
|
|
17
|
+
### Path Utilities
|
|
18
|
+
- `setValueAtPath` - Set value at path in object
|
|
19
|
+
- `stringPathToArrayPath` - Convert string path to array
|
|
20
|
+
- `arrayPathToStringPath` - Convert array path to string
|
|
21
|
+
|
|
22
|
+
### Array Utilities
|
|
23
|
+
- `getArraysDiff` - Compute differences between arrays
|
|
24
|
+
|
|
25
|
+
### Types
|
|
26
|
+
- `FlatObject`, `NestedObject`, `GetArraysDiffArgs`
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import {
|
|
32
|
+
flatObjectToNestedObject,
|
|
33
|
+
stringPathToArrayPath,
|
|
34
|
+
setValueAtPath,
|
|
35
|
+
} from "@typed-web/form-utils";
|
|
36
|
+
|
|
37
|
+
// Convert flat to nested
|
|
38
|
+
const nested = flatObjectToNestedObject({
|
|
39
|
+
"user.name": "John",
|
|
40
|
+
"addresses[0].city": "NYC",
|
|
41
|
+
});
|
|
42
|
+
// { user: { name: "John" }, addresses: [{ city: "NYC" }] }
|
|
43
|
+
|
|
44
|
+
// Parse path
|
|
45
|
+
const path = stringPathToArrayPath("user.addresses[0].city");
|
|
46
|
+
// ["user", "addresses", 0, "city"]
|
|
47
|
+
|
|
48
|
+
// Set value at path
|
|
49
|
+
const obj = {};
|
|
50
|
+
setValueAtPath(obj, ["user", "name"], "John");
|
|
51
|
+
// { user: { name: "John" } }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { FlatObject, NestedObject } from "./lib/types.ts";
|
|
2
|
+
export { setValueAtPath } from "./lib/set-value-at-path.ts";
|
|
3
|
+
export { stringPathToArrayPath } from "./lib/string-path-to-array-path.ts";
|
|
4
|
+
export { arrayPathToStringPath } from "./lib/array-path-to-string-path.ts";
|
|
5
|
+
export { flatObjectToNestedObject } from "./lib/flat-object-to-nested-object.ts";
|
|
6
|
+
export { nestedObjectToFlatObject } from "./lib/nested-object-to-flat-object.ts";
|
|
7
|
+
export { getArraysDiff } from "./lib/get-arrays-diff.ts";
|
|
8
|
+
export type { GetArraysDiffArgs } from "./lib/get-arrays-diff.ts";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG/D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AACjF,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AAGjF,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/lib/string-path-to-array-path.ts
|
|
2
|
+
var BRACKET_NOTATION_REGEX = /^\[(.+?)\](.*)$/;
|
|
3
|
+
var DOT_NOTATION_REGEX = /^\.?([^\.\[\]]+)(.*)$/;
|
|
4
|
+
var NUMERIC_KEY_REGEX = /^\d+$/;
|
|
5
|
+
function parsePath(currentPath) {
|
|
6
|
+
if (currentPath.length === 0) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const bracketMatch = currentPath.match(BRACKET_NOTATION_REGEX);
|
|
10
|
+
const dotMatch = currentPath.match(DOT_NOTATION_REGEX);
|
|
11
|
+
if (bracketMatch) {
|
|
12
|
+
const [, key = "", rest = ""] = bracketMatch;
|
|
13
|
+
const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;
|
|
14
|
+
const restResult = parsePath(rest);
|
|
15
|
+
if (restResult === null) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return [parsedKey, ...restResult];
|
|
19
|
+
} else if (dotMatch) {
|
|
20
|
+
const [, key = "", rest = ""] = dotMatch;
|
|
21
|
+
const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;
|
|
22
|
+
const restResult = parsePath(rest);
|
|
23
|
+
if (restResult === null) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return [parsedKey, ...restResult];
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function stringPathToArrayPath(path) {
|
|
31
|
+
if (path.length === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const result = parsePath(path);
|
|
35
|
+
if (result === null) {
|
|
36
|
+
return [path];
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/lib/set-value-at-path.ts
|
|
42
|
+
function setValueAtPath(object, path, value) {
|
|
43
|
+
const pathSegments = stringPathToArrayPath(path);
|
|
44
|
+
if (pathSegments.length === 0) {
|
|
45
|
+
throw new Error("Cannot set value on empty path");
|
|
46
|
+
}
|
|
47
|
+
const leadingSegments = pathSegments.slice(0, -1);
|
|
48
|
+
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
49
|
+
if (lastSegment === void 0) {
|
|
50
|
+
throw new Error("Invalid path: last segment is undefined");
|
|
51
|
+
}
|
|
52
|
+
let currentObject = object;
|
|
53
|
+
for (let i = 0; i < leadingSegments.length; i++) {
|
|
54
|
+
const currentSegment = leadingSegments[i];
|
|
55
|
+
if (currentSegment === void 0) {
|
|
56
|
+
throw new Error(`Invalid path: segment at index ${i} is undefined`);
|
|
57
|
+
}
|
|
58
|
+
if (!(currentSegment in currentObject)) {
|
|
59
|
+
const nextSegment = leadingSegments[i + 1] ?? lastSegment;
|
|
60
|
+
currentObject[currentSegment] = typeof nextSegment === "number" ? [] : {};
|
|
61
|
+
}
|
|
62
|
+
const nextValue = currentObject[currentSegment];
|
|
63
|
+
if (typeof nextValue !== "object" || nextValue === null) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Cannot navigate through path: expected object at segment "${currentSegment}", got ${typeof nextValue}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
currentObject = nextValue;
|
|
69
|
+
}
|
|
70
|
+
currentObject[lastSegment] = value;
|
|
71
|
+
return object;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/lib/array-path-to-string-path.ts
|
|
75
|
+
function arrayPathToStringPath(path) {
|
|
76
|
+
return path.reduce((str, item) => {
|
|
77
|
+
if (typeof item === "symbol") {
|
|
78
|
+
return str + (str === "" ? "" : ".") + item.toString();
|
|
79
|
+
}
|
|
80
|
+
if (typeof item === "number" || !Number.isNaN(Number(item))) {
|
|
81
|
+
return str + "[" + item + "]";
|
|
82
|
+
}
|
|
83
|
+
return str + (str === "" ? "" : ".") + item;
|
|
84
|
+
}, "");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/lib/flat-object-to-nested-object.ts
|
|
88
|
+
function flatObjectToNestedObject(flatStructure) {
|
|
89
|
+
const result = {};
|
|
90
|
+
for (const [path, value] of Object.entries(flatStructure)) {
|
|
91
|
+
setValueAtPath(result, path, value);
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/lib/nested-object-to-flat-object.ts
|
|
97
|
+
function nestedObjectToFlatObject(nestedStructure) {
|
|
98
|
+
const result = {};
|
|
99
|
+
function walk(obj, currentPath) {
|
|
100
|
+
if (typeof obj !== "object" || obj === null) {
|
|
101
|
+
result[currentPath] = obj;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(obj)) {
|
|
105
|
+
obj.forEach((item, index) => {
|
|
106
|
+
const newPath = currentPath ? `${currentPath}[${index}]` : `[${index}]`;
|
|
107
|
+
walk(item, newPath);
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
111
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
112
|
+
walk(value, newPath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
walk(nestedStructure, "");
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/lib/get-arrays-diff.ts
|
|
121
|
+
function getArraysDiff(args) {
|
|
122
|
+
const { before, after, key, equals } = args;
|
|
123
|
+
const isEqual = equals;
|
|
124
|
+
const beforeMap = new Map(before.map((b) => [key(b), b]));
|
|
125
|
+
const afterMap = new Map(after.map((a) => [key(a), a]));
|
|
126
|
+
const added = after.filter((a) => !beforeMap.has(key(a)));
|
|
127
|
+
const removed = before.filter((b) => !afterMap.has(key(b)));
|
|
128
|
+
const modified = after.map((a) => {
|
|
129
|
+
const b = beforeMap.get(key(a));
|
|
130
|
+
return b !== void 0 && !isEqual(b, a) ? { before: b, after: a } : void 0;
|
|
131
|
+
}).filter((x) => x !== void 0);
|
|
132
|
+
return { added, removed, modified };
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
arrayPathToStringPath,
|
|
136
|
+
flatObjectToNestedObject,
|
|
137
|
+
getArraysDiff,
|
|
138
|
+
nestedObjectToFlatObject,
|
|
139
|
+
setValueAtPath,
|
|
140
|
+
stringPathToArrayPath
|
|
141
|
+
};
|
|
142
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/string-path-to-array-path.ts", "../src/lib/set-value-at-path.ts", "../src/lib/array-path-to-string-path.ts", "../src/lib/flat-object-to-nested-object.ts", "../src/lib/nested-object-to-flat-object.ts", "../src/lib/get-arrays-diff.ts"],
|
|
4
|
+
"sourcesContent": ["// Regular expression to match bracket notation: [key] followed by rest of path.\nconst BRACKET_NOTATION_REGEX = /^\\[(.+?)\\](.*)$/;\n\n// Regular expression to match dot notation: optional dot followed by property\n// name and rest of path.\nconst DOT_NOTATION_REGEX = /^\\.?([^\\.\\[\\]]+)(.*)$/;\n\n// Regular expression to test if a string contains only digits\n// (for array indices).\nconst NUMERIC_KEY_REGEX = /^\\d+$/;\n\n/**\n * Internal recursive parser for path segments.\n * Returns null if the path contains invalid syntax that cannot be parsed.\n *\n * @param currentPath - The remaining path to parse\n * @returns Array of path segments, or null if parsing fails\n */\nfunction parsePath(currentPath: string): Array<string | number> | null {\n // Base case: empty path\n if (currentPath.length === 0) {\n return [];\n }\n\n // Try to match bracket notation first, then dot notation.\n const bracketMatch = currentPath.match(BRACKET_NOTATION_REGEX);\n const dotMatch = currentPath.match(DOT_NOTATION_REGEX);\n\n if (bracketMatch) {\n const [, key = \"\", rest = \"\"] = bracketMatch;\n // Convert numeric keys to numbers, keep string keys as strings.\n const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;\n\n // Recursively process the rest of the path.\n const restResult = parsePath(rest);\n\n // If rest parsing failed (returned null), propagate failure\n if (restResult === null) {\n return null;\n }\n\n return [parsedKey, ...restResult];\n } else if (dotMatch) {\n const [, key = \"\", rest = \"\"] = dotMatch;\n // Convert numeric keys to numbers, keep string keys as strings.\n const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;\n\n // Recursively process the rest of the path.\n const restResult = parsePath(rest);\n\n // If rest parsing failed (returned null), propagate failure\n if (restResult === null) {\n return null;\n }\n\n return [parsedKey, ...restResult];\n }\n\n // No pattern matched but we still have content - invalid path\n // Return null to signal failure\n return null;\n}\n\n/**\n * Converts a string path to an array of path segments.\n * Supports both bracket notation (e.g., \"[0]\", \"[key]\") and dot notation (e.g., \".prop\", \"prop\").\n * All numeric strings are converted to numbers regardless of notation type.\n * If the path contains invalid syntax, returns the entire path as a single segment.\n *\n * @param path - The string path to convert (e.g., \"user.profile[0].name\")\n * @returns Array of path segments where numeric strings become numbers\n *\n * @example\n * stringPathToArrayPath(\"user.profile[0].name\") // [\"user\", \"profile\", 0, \"name\"]\n * stringPathToArrayPath(\"users.0[name]\") // [\"users\", 0, \"name\"]\n * stringPathToArrayPath(\"[0].title\") // [0, \"title\"]\n * stringPathToArrayPath(\"\") // []\n * stringPathToArrayPath(\"invalid[[path\") // [\"invalid[[path\"] (invalid syntax)\n */\nexport function stringPathToArrayPath(path: string): Array<string | number> {\n // Handle empty path.\n if (path.length === 0) {\n return [];\n }\n\n // Attempt to parse the path\n const result = parsePath(path);\n\n // If parsing failed, return the entire path as a single segment\n if (result === null) {\n return [path];\n }\n\n return result;\n}\n", "import { stringPathToArrayPath } from \"./string-path-to-array-path.ts\";\n\n/**\n * Sets a value at a specific path within an object, creating nested objects/arrays as needed.\n * The path is parsed using dot notation and bracket notation to navigate through the object structure.\n * Missing intermediate objects are automatically created as objects or arrays based on the next segment type.\n *\n * @param object - The target object to modify (will be mutated)\n * @param path - The path string indicating where to set the value (e.g., \"user.profile[0].name\")\n * @param value - The value to set at the specified path\n * @returns The modified input object (same reference, mutated)\n *\n * @example\n * const obj = {};\n * setValueAtPath(obj, \"user.profile.name\", \"John\");\n * // obj becomes { user: { profile: { name: \"John\" } } }\n *\n * @example\n * const obj = {};\n * setValueAtPath(obj, \"users[0].name\", \"Alice\");\n * // obj becomes { users: [{ name: \"Alice\" }] }\n *\n * @example\n * const obj = {};\n * setValueAtPath(obj, \"config[database][host]\", \"localhost\");\n * // obj becomes { config: { database: { host: \"localhost\" } } }\n */\nexport function setValueAtPath<T extends Record<string | number, unknown>>(\n object: T,\n path: string,\n value: unknown,\n): T {\n const pathSegments = stringPathToArrayPath(path);\n\n // Handle empty path - cannot set value on empty path.\n if (pathSegments.length === 0) {\n throw new Error(\"Cannot set value on empty path\");\n }\n\n // Extract leading segments (all but last) and the final segment.\n const leadingSegments = pathSegments.slice(0, -1);\n const lastSegment = pathSegments[pathSegments.length - 1];\n\n // Type guard for lastSegment.\n if (lastSegment === undefined) {\n throw new Error(\"Invalid path: last segment is undefined\");\n }\n\n // Navigate through the object, creating intermediate objects/arrays as needed.\n let currentObject: Record<string | number, unknown> = object;\n\n for (let i = 0; i < leadingSegments.length; i++) {\n const currentSegment = leadingSegments[i];\n\n // Type guard for currentSegment.\n if (currentSegment === undefined) {\n throw new Error(`Invalid path: segment at index ${i} is undefined`);\n }\n\n // If the current property doesn't exist, create it.\n if (!(currentSegment in currentObject)) {\n // Determine the next segment to decide whether to create an object or array.\n const nextSegment = leadingSegments[i + 1] ?? lastSegment;\n // Create array if next segment is a number, otherwise create object.\n currentObject[currentSegment] = typeof nextSegment === \"number\" ? [] : {};\n }\n\n // Move deeper into the structure.\n const nextValue = currentObject[currentSegment];\n\n // Type guard to ensure we have an object-like structure to navigate into.\n if (typeof nextValue !== \"object\" || nextValue === null) {\n throw new Error(\n `Cannot navigate through path: expected object at segment \"${currentSegment}\", got ${typeof nextValue}`,\n );\n }\n\n currentObject = nextValue as Record<string | number, unknown>;\n }\n\n // Set the final value.\n currentObject[lastSegment] = value;\n\n return object;\n}\n", "/**\n * Convert array path to string format.\n * Examples: [\"user\", \"name\"] -> \"user.name\", [\"items\", 0] -> \"items[0]\"\n *\n * @param path - Array of path segments (strings, numbers, or symbols)\n * @returns String path with dot notation for object properties and bracket notation for array indices\n *\n * @example\n * arrayPathToStringPath([\"user\", \"name\"]) // \"user.name\"\n * arrayPathToStringPath([\"items\", 0]) // \"items[0]\"\n * arrayPathToStringPath([\"addresses\", 0, \"city\"]) // \"addresses[0].city\"\n * arrayPathToStringPath([]) // \"\"\n */\nexport function arrayPathToStringPath(path: Array<PropertyKey>): string {\n return path.reduce((str: string, item) => {\n if (typeof item === \"symbol\") {\n return str + (str === \"\" ? \"\" : \".\") + item.toString();\n }\n if (typeof item === \"number\" || !Number.isNaN(Number(item))) {\n return str + \"[\" + item + \"]\";\n }\n return str + (str === \"\" ? \"\" : \".\") + item;\n }, \"\");\n}\n", "import { setValueAtPath } from \"./set-value-at-path.ts\";\nimport type { FlatObject, NestedObject } from \"./types.ts\";\n\n/**\n * Converts a flat structure object to a nested structure object.\n * Uses path strings like \"user.name\" or \"addresses[0].city\" to create nested structure.\n *\n * @param flatStructure - Flat structure object with string paths as keys\n * @returns Nested structure object with hierarchical structure\n *\n * @example\n * const flat = {\n * \"user.name\": \"John\",\n * \"addresses[0].city\": \"San Francisco\"\n * };\n * const nested = flatObjectToNestedObject(flat);\n * // {\n * // user: { name: \"John\" },\n * // addresses: [{ city: \"San Francisco\" }]\n * // }\n */\nexport function flatObjectToNestedObject<T = unknown, V = unknown>(\n flatStructure: FlatObject<V>,\n): NestedObject<T> {\n const result = {} as Record<string | number, unknown>;\n\n for (const [path, value] of Object.entries(flatStructure)) {\n setValueAtPath(result, path, value);\n }\n\n return result as NestedObject<T>;\n}\n", "import type { FlatObject, NestedObject } from \"./types.ts\";\n\n/**\n * Converts a nested structure object to a flat structure object.\n * Recursively walks the nested structure and creates path strings.\n *\n * @param nestedStructure - Nested structure object with hierarchical structure\n * @returns Flat structure object with string paths as keys\n *\n * @example\n * const nested = {\n * user: { name: \"John\" },\n * addresses: [{ city: \"San Francisco\" }]\n * };\n * const flat = nestedObjectToFlatObject(nested);\n * // {\n * // \"user.name\": \"John\",\n * // \"addresses[0].city\": \"San Francisco\"\n * // }\n */\nexport function nestedObjectToFlatObject<T = unknown, V = unknown>(\n nestedStructure: NestedObject<T>,\n): FlatObject<V> {\n const result: FlatObject<V> = {};\n\n function walk(obj: unknown, currentPath: string) {\n if (typeof obj !== \"object\" || obj === null) {\n // Leaf node - this is a value\n result[currentPath] = obj as V;\n return;\n }\n\n if (Array.isArray(obj)) {\n // Handle arrays\n obj.forEach((item, index) => {\n const newPath = currentPath ? `${currentPath}[${index}]` : `[${index}]`;\n walk(item, newPath);\n });\n } else {\n // Handle objects\n for (const [key, value] of Object.entries(obj)) {\n const newPath = currentPath ? `${currentPath}.${key}` : key;\n walk(value, newPath);\n }\n }\n }\n\n walk(nestedStructure, \"\");\n\n return result;\n}\n", "export interface GetArraysDiffArgs<T extends Record<string, unknown>> {\n before: Array<T>;\n after: Array<T>;\n key: (item: T) => PropertyKey;\n equals: (a: T, b: T) => boolean;\n}\n\nexport function getArraysDiff<T extends Record<string, unknown>>(\n args: GetArraysDiffArgs<T>,\n): {\n added: Array<T>;\n removed: Array<T>;\n modified: Array<{ before: T; after: T }>;\n} {\n const { before, after, key, equals } = args;\n const isEqual = equals;\n\n // Build lookup maps/sets for efficient comparisons by key.\n const beforeMap = new Map(before.map((b) => [key(b), b]));\n const afterMap = new Map(after.map((a) => [key(a), a]));\n\n const added = after.filter((a) => !beforeMap.has(key(a)));\n const removed = before.filter((b) => !afterMap.has(key(b)));\n\n const modified = after\n .map((a): { before: T; after: T } | undefined => {\n const b = beforeMap.get(key(a));\n return b !== undefined && !isEqual(b, a) ? { before: b, after: a } : undefined;\n })\n .filter((x): x is { before: T; after: T } => x !== undefined);\n\n return { added, removed, modified };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert array path to string format.
|
|
3
|
+
* Examples: ["user", "name"] -> "user.name", ["items", 0] -> "items[0]"
|
|
4
|
+
*
|
|
5
|
+
* @param path - Array of path segments (strings, numbers, or symbols)
|
|
6
|
+
* @returns String path with dot notation for object properties and bracket notation for array indices
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* arrayPathToStringPath(["user", "name"]) // "user.name"
|
|
10
|
+
* arrayPathToStringPath(["items", 0]) // "items[0]"
|
|
11
|
+
* arrayPathToStringPath(["addresses", 0, "city"]) // "addresses[0].city"
|
|
12
|
+
* arrayPathToStringPath([]) // ""
|
|
13
|
+
*/
|
|
14
|
+
export declare function arrayPathToStringPath(path: Array<PropertyKey>): string;
|
|
15
|
+
//# sourceMappingURL=array-path-to-string-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"array-path-to-string-path.d.ts","sourceRoot":"","sources":["../../src/lib/array-path-to-string-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,MAAM,CAUtE"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FlatObject, NestedObject } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Converts a flat structure object to a nested structure object.
|
|
4
|
+
* Uses path strings like "user.name" or "addresses[0].city" to create nested structure.
|
|
5
|
+
*
|
|
6
|
+
* @param flatStructure - Flat structure object with string paths as keys
|
|
7
|
+
* @returns Nested structure object with hierarchical structure
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const flat = {
|
|
11
|
+
* "user.name": "John",
|
|
12
|
+
* "addresses[0].city": "San Francisco"
|
|
13
|
+
* };
|
|
14
|
+
* const nested = flatObjectToNestedObject(flat);
|
|
15
|
+
* // {
|
|
16
|
+
* // user: { name: "John" },
|
|
17
|
+
* // addresses: [{ city: "San Francisco" }]
|
|
18
|
+
* // }
|
|
19
|
+
*/
|
|
20
|
+
export declare function flatObjectToNestedObject<T = unknown, V = unknown>(flatStructure: FlatObject<V>): NestedObject<T>;
|
|
21
|
+
//# sourceMappingURL=flat-object-to-nested-object.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flat-object-to-nested-object.d.ts","sourceRoot":"","sources":["../../src/lib/flat-object-to-nested-object.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAC/D,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC,GAC3B,YAAY,CAAC,CAAC,CAAC,CAQjB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface GetArraysDiffArgs<T extends Record<string, unknown>> {
|
|
2
|
+
before: Array<T>;
|
|
3
|
+
after: Array<T>;
|
|
4
|
+
key: (item: T) => PropertyKey;
|
|
5
|
+
equals: (a: T, b: T) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function getArraysDiff<T extends Record<string, unknown>>(args: GetArraysDiffArgs<T>): {
|
|
8
|
+
added: Array<T>;
|
|
9
|
+
removed: Array<T>;
|
|
10
|
+
modified: Array<{
|
|
11
|
+
before: T;
|
|
12
|
+
after: T;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=get-arrays-diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-arrays-diff.d.ts","sourceRoot":"","sources":["../../src/lib/get-arrays-diff.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAClE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,WAAW,CAAC;IAC9B,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC;CACjC;AAED,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7D,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GACzB;IACD,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,CAAC,CAAC;QAAC,KAAK,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAC1C,CAmBA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FlatObject, NestedObject } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Converts a nested structure object to a flat structure object.
|
|
4
|
+
* Recursively walks the nested structure and creates path strings.
|
|
5
|
+
*
|
|
6
|
+
* @param nestedStructure - Nested structure object with hierarchical structure
|
|
7
|
+
* @returns Flat structure object with string paths as keys
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const nested = {
|
|
11
|
+
* user: { name: "John" },
|
|
12
|
+
* addresses: [{ city: "San Francisco" }]
|
|
13
|
+
* };
|
|
14
|
+
* const flat = nestedObjectToFlatObject(nested);
|
|
15
|
+
* // {
|
|
16
|
+
* // "user.name": "John",
|
|
17
|
+
* // "addresses[0].city": "San Francisco"
|
|
18
|
+
* // }
|
|
19
|
+
*/
|
|
20
|
+
export declare function nestedObjectToFlatObject<T = unknown, V = unknown>(nestedStructure: NestedObject<T>): FlatObject<V>;
|
|
21
|
+
//# sourceMappingURL=nested-object-to-flat-object.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nested-object-to-flat-object.d.ts","sourceRoot":"","sources":["../../src/lib/nested-object-to-flat-object.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO,EAC/D,eAAe,EAAE,YAAY,CAAC,CAAC,CAAC,GAC/B,UAAU,CAAC,CAAC,CAAC,CA4Bf"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets a value at a specific path within an object, creating nested objects/arrays as needed.
|
|
3
|
+
* The path is parsed using dot notation and bracket notation to navigate through the object structure.
|
|
4
|
+
* Missing intermediate objects are automatically created as objects or arrays based on the next segment type.
|
|
5
|
+
*
|
|
6
|
+
* @param object - The target object to modify (will be mutated)
|
|
7
|
+
* @param path - The path string indicating where to set the value (e.g., "user.profile[0].name")
|
|
8
|
+
* @param value - The value to set at the specified path
|
|
9
|
+
* @returns The modified input object (same reference, mutated)
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const obj = {};
|
|
13
|
+
* setValueAtPath(obj, "user.profile.name", "John");
|
|
14
|
+
* // obj becomes { user: { profile: { name: "John" } } }
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const obj = {};
|
|
18
|
+
* setValueAtPath(obj, "users[0].name", "Alice");
|
|
19
|
+
* // obj becomes { users: [{ name: "Alice" }] }
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const obj = {};
|
|
23
|
+
* setValueAtPath(obj, "config[database][host]", "localhost");
|
|
24
|
+
* // obj becomes { config: { database: { host: "localhost" } } }
|
|
25
|
+
*/
|
|
26
|
+
export declare function setValueAtPath<T extends Record<string | number, unknown>>(object: T, path: string, value: unknown): T;
|
|
27
|
+
//# sourceMappingURL=set-value-at-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set-value-at-path.d.ts","sourceRoot":"","sources":["../../src/lib/set-value-at-path.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EACvE,MAAM,EAAE,CAAC,EACT,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,GACb,CAAC,CAqDH"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a string path to an array of path segments.
|
|
3
|
+
* Supports both bracket notation (e.g., "[0]", "[key]") and dot notation (e.g., ".prop", "prop").
|
|
4
|
+
* All numeric strings are converted to numbers regardless of notation type.
|
|
5
|
+
* If the path contains invalid syntax, returns the entire path as a single segment.
|
|
6
|
+
*
|
|
7
|
+
* @param path - The string path to convert (e.g., "user.profile[0].name")
|
|
8
|
+
* @returns Array of path segments where numeric strings become numbers
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* stringPathToArrayPath("user.profile[0].name") // ["user", "profile", 0, "name"]
|
|
12
|
+
* stringPathToArrayPath("users.0[name]") // ["users", 0, "name"]
|
|
13
|
+
* stringPathToArrayPath("[0].title") // [0, "title"]
|
|
14
|
+
* stringPathToArrayPath("") // []
|
|
15
|
+
* stringPathToArrayPath("invalid[[path") // ["invalid[[path"] (invalid syntax)
|
|
16
|
+
*/
|
|
17
|
+
export declare function stringPathToArrayPath(path: string): Array<string | number>;
|
|
18
|
+
//# sourceMappingURL=string-path-to-array-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"string-path-to-array-path.d.ts","sourceRoot":"","sources":["../../src/lib/string-path-to-array-path.ts"],"names":[],"mappings":"AA+DA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAe1E"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flat structure format - values mapped by string paths
|
|
3
|
+
* @example
|
|
4
|
+
* {
|
|
5
|
+
* "user.name": "John",
|
|
6
|
+
* "addresses[0].city": "San Francisco",
|
|
7
|
+
* "addresses[0].state": "California"
|
|
8
|
+
* }
|
|
9
|
+
*/
|
|
10
|
+
export type FlatObject<V = unknown> = Record<string, V>;
|
|
11
|
+
/**
|
|
12
|
+
* Nested structure format - values in a nested object structure
|
|
13
|
+
* @example
|
|
14
|
+
* {
|
|
15
|
+
* user: { name: "John" },
|
|
16
|
+
* addresses: [
|
|
17
|
+
* { city: "San Francisco", state: "California" }
|
|
18
|
+
* ]
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
export type NestedObject<T = unknown> = T extends object ? {
|
|
22
|
+
[K in keyof T]?: T[K] extends object ? NestedObject<T[K]> : unknown;
|
|
23
|
+
} : unknown;
|
|
24
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AAExD;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,SAAS,MAAM,GACpD;KACG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO;CACpE,GACD,OAAO,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typed-web/form-utils",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Shared utilities for form handling: path parsing, error conversion, and array diffing",
|
|
5
|
+
"author": "Łukasz Jagodziński <luke.jagodzinski@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/typed-web/typed-web.git",
|
|
10
|
+
"directory": "packages/form-utils"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/typed-web/typed-web/tree/main/packages/form-utils#readme",
|
|
13
|
+
"files": [
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md",
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"!src/**/*.test.ts"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.0.10",
|
|
30
|
+
"esbuild": "^0.27.2"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"form",
|
|
34
|
+
"utilities",
|
|
35
|
+
"path-parsing",
|
|
36
|
+
"error-conversion",
|
|
37
|
+
"typescript"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "pnpm run clean && pnpm run build:types && pnpm run build:esm",
|
|
41
|
+
"build:esm": "esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm --platform=neutral --sourcemap",
|
|
42
|
+
"build:types": "tsc --project tsconfig.build.json",
|
|
43
|
+
"clean": "rm -rf dist",
|
|
44
|
+
"test": "node --disable-warning=ExperimentalWarning --test './src/**/*.test.ts'",
|
|
45
|
+
"typecheck": "tsc --noEmit"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type { FlatObject, NestedObject } from "./lib/types.ts";
|
|
3
|
+
|
|
4
|
+
// Path utilities
|
|
5
|
+
export { setValueAtPath } from "./lib/set-value-at-path.ts";
|
|
6
|
+
export { stringPathToArrayPath } from "./lib/string-path-to-array-path.ts";
|
|
7
|
+
export { arrayPathToStringPath } from "./lib/array-path-to-string-path.ts";
|
|
8
|
+
|
|
9
|
+
// Structure conversion
|
|
10
|
+
export { flatObjectToNestedObject } from "./lib/flat-object-to-nested-object.ts";
|
|
11
|
+
export { nestedObjectToFlatObject } from "./lib/nested-object-to-flat-object.ts";
|
|
12
|
+
|
|
13
|
+
// Array utilities
|
|
14
|
+
export { getArraysDiff } from "./lib/get-arrays-diff.ts";
|
|
15
|
+
export type { GetArraysDiffArgs } from "./lib/get-arrays-diff.ts";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert array path to string format.
|
|
3
|
+
* Examples: ["user", "name"] -> "user.name", ["items", 0] -> "items[0]"
|
|
4
|
+
*
|
|
5
|
+
* @param path - Array of path segments (strings, numbers, or symbols)
|
|
6
|
+
* @returns String path with dot notation for object properties and bracket notation for array indices
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* arrayPathToStringPath(["user", "name"]) // "user.name"
|
|
10
|
+
* arrayPathToStringPath(["items", 0]) // "items[0]"
|
|
11
|
+
* arrayPathToStringPath(["addresses", 0, "city"]) // "addresses[0].city"
|
|
12
|
+
* arrayPathToStringPath([]) // ""
|
|
13
|
+
*/
|
|
14
|
+
export function arrayPathToStringPath(path: Array<PropertyKey>): string {
|
|
15
|
+
return path.reduce((str: string, item) => {
|
|
16
|
+
if (typeof item === "symbol") {
|
|
17
|
+
return str + (str === "" ? "" : ".") + item.toString();
|
|
18
|
+
}
|
|
19
|
+
if (typeof item === "number" || !Number.isNaN(Number(item))) {
|
|
20
|
+
return str + "[" + item + "]";
|
|
21
|
+
}
|
|
22
|
+
return str + (str === "" ? "" : ".") + item;
|
|
23
|
+
}, "");
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { setValueAtPath } from "./set-value-at-path.ts";
|
|
2
|
+
import type { FlatObject, NestedObject } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts a flat structure object to a nested structure object.
|
|
6
|
+
* Uses path strings like "user.name" or "addresses[0].city" to create nested structure.
|
|
7
|
+
*
|
|
8
|
+
* @param flatStructure - Flat structure object with string paths as keys
|
|
9
|
+
* @returns Nested structure object with hierarchical structure
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const flat = {
|
|
13
|
+
* "user.name": "John",
|
|
14
|
+
* "addresses[0].city": "San Francisco"
|
|
15
|
+
* };
|
|
16
|
+
* const nested = flatObjectToNestedObject(flat);
|
|
17
|
+
* // {
|
|
18
|
+
* // user: { name: "John" },
|
|
19
|
+
* // addresses: [{ city: "San Francisco" }]
|
|
20
|
+
* // }
|
|
21
|
+
*/
|
|
22
|
+
export function flatObjectToNestedObject<T = unknown, V = unknown>(
|
|
23
|
+
flatStructure: FlatObject<V>,
|
|
24
|
+
): NestedObject<T> {
|
|
25
|
+
const result = {} as Record<string | number, unknown>;
|
|
26
|
+
|
|
27
|
+
for (const [path, value] of Object.entries(flatStructure)) {
|
|
28
|
+
setValueAtPath(result, path, value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result as NestedObject<T>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface GetArraysDiffArgs<T extends Record<string, unknown>> {
|
|
2
|
+
before: Array<T>;
|
|
3
|
+
after: Array<T>;
|
|
4
|
+
key: (item: T) => PropertyKey;
|
|
5
|
+
equals: (a: T, b: T) => boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getArraysDiff<T extends Record<string, unknown>>(
|
|
9
|
+
args: GetArraysDiffArgs<T>,
|
|
10
|
+
): {
|
|
11
|
+
added: Array<T>;
|
|
12
|
+
removed: Array<T>;
|
|
13
|
+
modified: Array<{ before: T; after: T }>;
|
|
14
|
+
} {
|
|
15
|
+
const { before, after, key, equals } = args;
|
|
16
|
+
const isEqual = equals;
|
|
17
|
+
|
|
18
|
+
// Build lookup maps/sets for efficient comparisons by key.
|
|
19
|
+
const beforeMap = new Map(before.map((b) => [key(b), b]));
|
|
20
|
+
const afterMap = new Map(after.map((a) => [key(a), a]));
|
|
21
|
+
|
|
22
|
+
const added = after.filter((a) => !beforeMap.has(key(a)));
|
|
23
|
+
const removed = before.filter((b) => !afterMap.has(key(b)));
|
|
24
|
+
|
|
25
|
+
const modified = after
|
|
26
|
+
.map((a): { before: T; after: T } | undefined => {
|
|
27
|
+
const b = beforeMap.get(key(a));
|
|
28
|
+
return b !== undefined && !isEqual(b, a) ? { before: b, after: a } : undefined;
|
|
29
|
+
})
|
|
30
|
+
.filter((x): x is { before: T; after: T } => x !== undefined);
|
|
31
|
+
|
|
32
|
+
return { added, removed, modified };
|
|
33
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { FlatObject, NestedObject } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a nested structure object to a flat structure object.
|
|
5
|
+
* Recursively walks the nested structure and creates path strings.
|
|
6
|
+
*
|
|
7
|
+
* @param nestedStructure - Nested structure object with hierarchical structure
|
|
8
|
+
* @returns Flat structure object with string paths as keys
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const nested = {
|
|
12
|
+
* user: { name: "John" },
|
|
13
|
+
* addresses: [{ city: "San Francisco" }]
|
|
14
|
+
* };
|
|
15
|
+
* const flat = nestedObjectToFlatObject(nested);
|
|
16
|
+
* // {
|
|
17
|
+
* // "user.name": "John",
|
|
18
|
+
* // "addresses[0].city": "San Francisco"
|
|
19
|
+
* // }
|
|
20
|
+
*/
|
|
21
|
+
export function nestedObjectToFlatObject<T = unknown, V = unknown>(
|
|
22
|
+
nestedStructure: NestedObject<T>,
|
|
23
|
+
): FlatObject<V> {
|
|
24
|
+
const result: FlatObject<V> = {};
|
|
25
|
+
|
|
26
|
+
function walk(obj: unknown, currentPath: string) {
|
|
27
|
+
if (typeof obj !== "object" || obj === null) {
|
|
28
|
+
// Leaf node - this is a value
|
|
29
|
+
result[currentPath] = obj as V;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (Array.isArray(obj)) {
|
|
34
|
+
// Handle arrays
|
|
35
|
+
obj.forEach((item, index) => {
|
|
36
|
+
const newPath = currentPath ? `${currentPath}[${index}]` : `[${index}]`;
|
|
37
|
+
walk(item, newPath);
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
// Handle objects
|
|
41
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
42
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
43
|
+
walk(value, newPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
walk(nestedStructure, "");
|
|
49
|
+
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { stringPathToArrayPath } from "./string-path-to-array-path.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sets a value at a specific path within an object, creating nested objects/arrays as needed.
|
|
5
|
+
* The path is parsed using dot notation and bracket notation to navigate through the object structure.
|
|
6
|
+
* Missing intermediate objects are automatically created as objects or arrays based on the next segment type.
|
|
7
|
+
*
|
|
8
|
+
* @param object - The target object to modify (will be mutated)
|
|
9
|
+
* @param path - The path string indicating where to set the value (e.g., "user.profile[0].name")
|
|
10
|
+
* @param value - The value to set at the specified path
|
|
11
|
+
* @returns The modified input object (same reference, mutated)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const obj = {};
|
|
15
|
+
* setValueAtPath(obj, "user.profile.name", "John");
|
|
16
|
+
* // obj becomes { user: { profile: { name: "John" } } }
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const obj = {};
|
|
20
|
+
* setValueAtPath(obj, "users[0].name", "Alice");
|
|
21
|
+
* // obj becomes { users: [{ name: "Alice" }] }
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const obj = {};
|
|
25
|
+
* setValueAtPath(obj, "config[database][host]", "localhost");
|
|
26
|
+
* // obj becomes { config: { database: { host: "localhost" } } }
|
|
27
|
+
*/
|
|
28
|
+
export function setValueAtPath<T extends Record<string | number, unknown>>(
|
|
29
|
+
object: T,
|
|
30
|
+
path: string,
|
|
31
|
+
value: unknown,
|
|
32
|
+
): T {
|
|
33
|
+
const pathSegments = stringPathToArrayPath(path);
|
|
34
|
+
|
|
35
|
+
// Handle empty path - cannot set value on empty path.
|
|
36
|
+
if (pathSegments.length === 0) {
|
|
37
|
+
throw new Error("Cannot set value on empty path");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Extract leading segments (all but last) and the final segment.
|
|
41
|
+
const leadingSegments = pathSegments.slice(0, -1);
|
|
42
|
+
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
43
|
+
|
|
44
|
+
// Type guard for lastSegment.
|
|
45
|
+
if (lastSegment === undefined) {
|
|
46
|
+
throw new Error("Invalid path: last segment is undefined");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Navigate through the object, creating intermediate objects/arrays as needed.
|
|
50
|
+
let currentObject: Record<string | number, unknown> = object;
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < leadingSegments.length; i++) {
|
|
53
|
+
const currentSegment = leadingSegments[i];
|
|
54
|
+
|
|
55
|
+
// Type guard for currentSegment.
|
|
56
|
+
if (currentSegment === undefined) {
|
|
57
|
+
throw new Error(`Invalid path: segment at index ${i} is undefined`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If the current property doesn't exist, create it.
|
|
61
|
+
if (!(currentSegment in currentObject)) {
|
|
62
|
+
// Determine the next segment to decide whether to create an object or array.
|
|
63
|
+
const nextSegment = leadingSegments[i + 1] ?? lastSegment;
|
|
64
|
+
// Create array if next segment is a number, otherwise create object.
|
|
65
|
+
currentObject[currentSegment] = typeof nextSegment === "number" ? [] : {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Move deeper into the structure.
|
|
69
|
+
const nextValue = currentObject[currentSegment];
|
|
70
|
+
|
|
71
|
+
// Type guard to ensure we have an object-like structure to navigate into.
|
|
72
|
+
if (typeof nextValue !== "object" || nextValue === null) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Cannot navigate through path: expected object at segment "${currentSegment}", got ${typeof nextValue}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
currentObject = nextValue as Record<string | number, unknown>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Set the final value.
|
|
82
|
+
currentObject[lastSegment] = value;
|
|
83
|
+
|
|
84
|
+
return object;
|
|
85
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Regular expression to match bracket notation: [key] followed by rest of path.
|
|
2
|
+
const BRACKET_NOTATION_REGEX = /^\[(.+?)\](.*)$/;
|
|
3
|
+
|
|
4
|
+
// Regular expression to match dot notation: optional dot followed by property
|
|
5
|
+
// name and rest of path.
|
|
6
|
+
const DOT_NOTATION_REGEX = /^\.?([^\.\[\]]+)(.*)$/;
|
|
7
|
+
|
|
8
|
+
// Regular expression to test if a string contains only digits
|
|
9
|
+
// (for array indices).
|
|
10
|
+
const NUMERIC_KEY_REGEX = /^\d+$/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal recursive parser for path segments.
|
|
14
|
+
* Returns null if the path contains invalid syntax that cannot be parsed.
|
|
15
|
+
*
|
|
16
|
+
* @param currentPath - The remaining path to parse
|
|
17
|
+
* @returns Array of path segments, or null if parsing fails
|
|
18
|
+
*/
|
|
19
|
+
function parsePath(currentPath: string): Array<string | number> | null {
|
|
20
|
+
// Base case: empty path
|
|
21
|
+
if (currentPath.length === 0) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Try to match bracket notation first, then dot notation.
|
|
26
|
+
const bracketMatch = currentPath.match(BRACKET_NOTATION_REGEX);
|
|
27
|
+
const dotMatch = currentPath.match(DOT_NOTATION_REGEX);
|
|
28
|
+
|
|
29
|
+
if (bracketMatch) {
|
|
30
|
+
const [, key = "", rest = ""] = bracketMatch;
|
|
31
|
+
// Convert numeric keys to numbers, keep string keys as strings.
|
|
32
|
+
const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;
|
|
33
|
+
|
|
34
|
+
// Recursively process the rest of the path.
|
|
35
|
+
const restResult = parsePath(rest);
|
|
36
|
+
|
|
37
|
+
// If rest parsing failed (returned null), propagate failure
|
|
38
|
+
if (restResult === null) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return [parsedKey, ...restResult];
|
|
43
|
+
} else if (dotMatch) {
|
|
44
|
+
const [, key = "", rest = ""] = dotMatch;
|
|
45
|
+
// Convert numeric keys to numbers, keep string keys as strings.
|
|
46
|
+
const parsedKey = NUMERIC_KEY_REGEX.test(key) ? Number(key) : key;
|
|
47
|
+
|
|
48
|
+
// Recursively process the rest of the path.
|
|
49
|
+
const restResult = parsePath(rest);
|
|
50
|
+
|
|
51
|
+
// If rest parsing failed (returned null), propagate failure
|
|
52
|
+
if (restResult === null) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return [parsedKey, ...restResult];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// No pattern matched but we still have content - invalid path
|
|
60
|
+
// Return null to signal failure
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Converts a string path to an array of path segments.
|
|
66
|
+
* Supports both bracket notation (e.g., "[0]", "[key]") and dot notation (e.g., ".prop", "prop").
|
|
67
|
+
* All numeric strings are converted to numbers regardless of notation type.
|
|
68
|
+
* If the path contains invalid syntax, returns the entire path as a single segment.
|
|
69
|
+
*
|
|
70
|
+
* @param path - The string path to convert (e.g., "user.profile[0].name")
|
|
71
|
+
* @returns Array of path segments where numeric strings become numbers
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* stringPathToArrayPath("user.profile[0].name") // ["user", "profile", 0, "name"]
|
|
75
|
+
* stringPathToArrayPath("users.0[name]") // ["users", 0, "name"]
|
|
76
|
+
* stringPathToArrayPath("[0].title") // [0, "title"]
|
|
77
|
+
* stringPathToArrayPath("") // []
|
|
78
|
+
* stringPathToArrayPath("invalid[[path") // ["invalid[[path"] (invalid syntax)
|
|
79
|
+
*/
|
|
80
|
+
export function stringPathToArrayPath(path: string): Array<string | number> {
|
|
81
|
+
// Handle empty path.
|
|
82
|
+
if (path.length === 0) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Attempt to parse the path
|
|
87
|
+
const result = parsePath(path);
|
|
88
|
+
|
|
89
|
+
// If parsing failed, return the entire path as a single segment
|
|
90
|
+
if (result === null) {
|
|
91
|
+
return [path];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flat structure format - values mapped by string paths
|
|
3
|
+
* @example
|
|
4
|
+
* {
|
|
5
|
+
* "user.name": "John",
|
|
6
|
+
* "addresses[0].city": "San Francisco",
|
|
7
|
+
* "addresses[0].state": "California"
|
|
8
|
+
* }
|
|
9
|
+
*/
|
|
10
|
+
export type FlatObject<V = unknown> = Record<string, V>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Nested structure format - values in a nested object structure
|
|
14
|
+
* @example
|
|
15
|
+
* {
|
|
16
|
+
* user: { name: "John" },
|
|
17
|
+
* addresses: [
|
|
18
|
+
* { city: "San Francisco", state: "California" }
|
|
19
|
+
* ]
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
export type NestedObject<T = unknown> = T extends object
|
|
23
|
+
? {
|
|
24
|
+
[K in keyof T]?: T[K] extends object ? NestedObject<T[K]> : unknown;
|
|
25
|
+
}
|
|
26
|
+
: unknown;
|