@typed-web/form-utils 0.2.0 → 0.3.0
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/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -37
- package/dist/index.js.map +3 -3
- package/dist/lib/object-from-path-entries.d.ts +24 -0
- package/dist/lib/object-from-path-entries.d.ts.map +1 -0
- package/dist/lib/preprocess-form-data.d.ts +8 -3
- package/dist/lib/preprocess-form-data.d.ts.map +1 -1
- package/dist/lib/string-path-to-array-path.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +3 -1
- package/src/lib/object-from-path-entries.ts +45 -0
- package/src/lib/preprocess-form-data.ts +14 -23
- package/src/lib/string-path-to-array-path.ts +16 -38
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export type { FlatObject, NestedObject } from "./lib/types.ts";
|
|
2
|
+
export { arrayPathToStringPath } from "./lib/array-path-to-string-path.ts";
|
|
2
3
|
export { setValueAtPath } from "./lib/set-value-at-path.ts";
|
|
3
4
|
export { stringPathToArrayPath } from "./lib/string-path-to-array-path.ts";
|
|
4
|
-
export { arrayPathToStringPath } from "./lib/array-path-to-string-path.ts";
|
|
5
5
|
export { flatObjectToNestedObject } from "./lib/flat-object-to-nested-object.ts";
|
|
6
6
|
export { nestedObjectToFlatObject } from "./lib/nested-object-to-flat-object.ts";
|
|
7
7
|
export { getArraysDiff } from "./lib/get-arrays-diff.ts";
|
|
8
8
|
export type { GetArraysDiffArgs } from "./lib/get-arrays-diff.ts";
|
|
9
|
+
export { objectFromPathEntries } from "./lib/object-from-path-entries.ts";
|
|
10
|
+
export type { PathEntry } from "./lib/object-from-path-entries.ts";
|
|
9
11
|
export { preprocessFormData } from "./lib/preprocess-form-data.ts";
|
|
10
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,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;AAGlE,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,YAAY,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,31 +1,33 @@
|
|
|
1
|
+
// src/lib/array-path-to-string-path.ts
|
|
2
|
+
function arrayPathToStringPath(path) {
|
|
3
|
+
return path.reduce((str, item) => {
|
|
4
|
+
if (typeof item === "symbol") {
|
|
5
|
+
return str + (str === "" ? "" : ".") + item.toString();
|
|
6
|
+
}
|
|
7
|
+
if (typeof item === "number" || !Number.isNaN(Number(item))) {
|
|
8
|
+
return str + "[" + item + "]";
|
|
9
|
+
}
|
|
10
|
+
return str + (str === "" ? "" : ".") + item;
|
|
11
|
+
}, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
1
14
|
// src/lib/string-path-to-array-path.ts
|
|
2
15
|
var BRACKET_NOTATION_REGEX = /^\[(.+?)\](.*)$/;
|
|
3
16
|
var DOT_NOTATION_REGEX = /^\.?([^\.\[\]]+)(.*)$/;
|
|
4
17
|
var NUMERIC_KEY_REGEX = /^\d+$/;
|
|
5
|
-
function parsePath(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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) {
|
|
18
|
+
function parsePath(path) {
|
|
19
|
+
const result = [];
|
|
20
|
+
let remaining = path;
|
|
21
|
+
while (remaining.length > 0) {
|
|
22
|
+
const match = remaining.match(BRACKET_NOTATION_REGEX) ?? remaining.match(DOT_NOTATION_REGEX);
|
|
23
|
+
if (match === null) {
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
const [, key = "", rest = ""] = match;
|
|
27
|
+
result.push(NUMERIC_KEY_REGEX.test(key) ? Number(key) : key);
|
|
28
|
+
remaining = rest;
|
|
27
29
|
}
|
|
28
|
-
return
|
|
30
|
+
return result;
|
|
29
31
|
}
|
|
30
32
|
function stringPathToArrayPath(path) {
|
|
31
33
|
if (path.length === 0) {
|
|
@@ -71,19 +73,6 @@ function setValueAtPath(object, path, value) {
|
|
|
71
73
|
return object;
|
|
72
74
|
}
|
|
73
75
|
|
|
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
76
|
// src/lib/flat-object-to-nested-object.ts
|
|
88
77
|
function flatObjectToNestedObject(flatStructure) {
|
|
89
78
|
const result = {};
|
|
@@ -132,9 +121,8 @@ function getArraysDiff(args) {
|
|
|
132
121
|
return { added, removed, modified };
|
|
133
122
|
}
|
|
134
123
|
|
|
135
|
-
// src/lib/
|
|
136
|
-
function
|
|
137
|
-
const entries = [...formData];
|
|
124
|
+
// src/lib/object-from-path-entries.ts
|
|
125
|
+
function objectFromPathEntries(entries) {
|
|
138
126
|
const map = /* @__PURE__ */ new Map();
|
|
139
127
|
for (const [key, value] of entries) {
|
|
140
128
|
if (map.has(key)) {
|
|
@@ -149,11 +137,20 @@ function preprocessFormData(formData) {
|
|
|
149
137
|
}, {});
|
|
150
138
|
return result;
|
|
151
139
|
}
|
|
140
|
+
|
|
141
|
+
// src/lib/preprocess-form-data.ts
|
|
142
|
+
function preprocessFormData(data) {
|
|
143
|
+
if ("entries" in data && typeof data.entries === "function") {
|
|
144
|
+
return objectFromPathEntries([...data.entries()]);
|
|
145
|
+
}
|
|
146
|
+
return objectFromPathEntries(Object.entries(data));
|
|
147
|
+
}
|
|
152
148
|
export {
|
|
153
149
|
arrayPathToStringPath,
|
|
154
150
|
flatObjectToNestedObject,
|
|
155
151
|
getArraysDiff,
|
|
156
152
|
nestedObjectToFlatObject,
|
|
153
|
+
objectFromPathEntries,
|
|
157
154
|
preprocessFormData,
|
|
158
155
|
setValueAtPath,
|
|
159
156
|
stringPathToArrayPath
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/lib/
|
|
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", "import { setValueAtPath } from \"./set-value-at-path.ts\";\n\n/**\n * Transforms flat FormData/URLSearchParams entries into a nested object structure.\n * Handles dot notation (e.g., \"address.street\") and bracket notation (e.g., \"items[0].name\").\n * Multiple values for the same key are automatically grouped into arrays.\n *\n * @param formData - Iterable of key-value pairs (FormData or URLSearchParams)\n * @returns Nested object with grouped values\n *\n * @example\n * const formData = new URLSearchParams([\n * [\"name\", \"John\"],\n * [\"address.street\", \"123 Main St\"],\n * [\"hobbies\", \"reading\"],\n * [\"hobbies\", \"coding\"]\n * ]);\n * const result = preprocessFormData(formData);\n * // Result: { name: \"John\", address: { street: \"123 Main St\" }, hobbies: [\"reading\", \"coding\"] }\n */\nexport function preprocessFormData(\n formData: Iterable<unknown>,\n): Record<string, unknown> {\n const entries = [...formData] as Array<[string, unknown]>;\n\n // Group values by key (handling multiple values for same key)\n const map = new Map<string, Array<unknown>>();\n for (const [key, value] of entries) {\n if (map.has(key)) {\n map.get(key)?.push(value);\n } else {\n map.set(key, [value]);\n }\n }\n\n // Build nested object using setValueAtPath for dot/bracket notation\n const result = [...map].reduce<Record<string, unknown>>((acc, [key, list]) => {\n // Single value stays as single value, multiple values become array\n const value = list.length === 1 ? list[0] : list;\n return setValueAtPath(acc, key, value);\n }, {});\n\n return result;\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
3
|
+
"sources": ["../src/lib/array-path-to-string-path.ts", "../src/lib/string-path-to-array-path.ts", "../src/lib/set-value-at-path.ts", "../src/lib/flat-object-to-nested-object.ts", "../src/lib/nested-object-to-flat-object.ts", "../src/lib/get-arrays-diff.ts", "../src/lib/object-from-path-entries.ts", "../src/lib/preprocess-form-data.ts"],
|
|
4
|
+
"sourcesContent": ["/**\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", "// 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 iterative parser for path segments.\n * Consumes the path string segment by segment, trying bracket notation first,\n * then dot notation. Returns null if any remaining portion cannot be parsed.\n *\n * @param path - The path string to parse\n * @returns Array of path segments, or null if parsing fails\n */\nfunction parsePath(path: string): Array<string | number> | null {\n const result: Array<string | number> = [];\n let remaining = path;\n\n while (remaining.length > 0) {\n // Try bracket notation first (e.g. \"[0]\", \"[key]\"), fall back to dot notation (e.g. \".prop\", \"prop\").\n const match = remaining.match(BRACKET_NOTATION_REGEX) ?? remaining.match(DOT_NOTATION_REGEX);\n\n // No pattern matched but we still have content \u2014 invalid path.\n if (match === null) {\n return null;\n }\n\n const [, key = \"\", rest = \"\"] = match;\n // Convert numeric keys to numbers, keep string keys as strings.\n result.push(NUMERIC_KEY_REGEX.test(key) ? Number(key) : key);\n remaining = rest;\n }\n\n return result;\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", "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", "import { setValueAtPath } from \"./set-value-at-path.ts\";\n\n/**\n * Entry type for path-based key-value pairs.\n */\nexport type PathEntry = [string, unknown];\n\n/**\n * Transforms an array of path entries into a nested object structure.\n * Handles dot notation (e.g., \"address.street\") and bracket notation (e.g., \"items[0].name\").\n * Multiple values for the same key are automatically grouped into arrays.\n *\n * @param entries - Array of [key, value] pairs where keys can use path notation\n * @returns Nested object with grouped values\n *\n * @example\n * const entries = [\n * [\"name\", \"John\"],\n * [\"address.street\", \"123 Main St\"],\n * [\"hobbies\", \"reading\"],\n * [\"hobbies\", \"coding\"]\n * ];\n * const result = objectFromPathEntries(entries);\n * // Result: { name: \"John\", address: { street: \"123 Main St\" }, hobbies: [\"reading\", \"coding\"] }\n */\nexport function objectFromPathEntries(entries: Array<PathEntry>): Record<string, unknown> {\n // Group values by key (handling multiple values for same key)\n const map = new Map<string, Array<unknown>>();\n for (const [key, value] of entries) {\n if (map.has(key)) {\n map.get(key)?.push(value);\n } else {\n map.set(key, [value]);\n }\n }\n\n // Build nested object using setValueAtPath for dot/bracket notation\n const result = [...map].reduce<Record<string, unknown>>((acc, [key, list]) => {\n // Single value stays as single value, multiple values become array\n const value = list.length === 1 ? list[0] : list;\n return setValueAtPath(acc, key, value);\n }, {});\n\n return result;\n}\n", "import { objectFromPathEntries } from \"./object-from-path-entries.ts\";\n\n/**\n * Transforms FormData, URLSearchParams, or plain objects into a nested object structure.\n * Handles dot notation (e.g., \"address.street\") and bracket notation (e.g., \"items[0].name\").\n * Multiple values for the same key are automatically grouped into arrays.\n *\n * @param data - FormData, URLSearchParams, or plain object to transform\n * @returns Nested object with grouped values\n *\n * @example\n * const formData = new URLSearchParams([\n * [\"name\", \"John\"],\n * [\"address.street\", \"123 Main St\"],\n * [\"hobbies\", \"reading\"],\n * [\"hobbies\", \"coding\"]\n * ]);\n * const result = preprocessFormData(formData);\n * // Result: { name: \"John\", address: { street: \"123 Main St\" }, hobbies: [\"reading\", \"coding\"] }\n *\n * @example\n * const data = { name: \"John\", age: \"30\" };\n * const result = preprocessFormData(data);\n * // Result: { name: \"John\", age: \"30\" }\n */\nexport function preprocessFormData(\n data: FormData | URLSearchParams | Record<string, unknown>,\n): Record<string, unknown> {\n // Check if data has entries method (FormData, URLSearchParams, Map, etc.)\n if (\"entries\" in data && typeof data.entries === \"function\") {\n return objectFromPathEntries([...data.entries()]);\n }\n // Plain object - use Object.entries\n return objectFromPathEntries(Object.entries(data));\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry type for path-based key-value pairs.
|
|
3
|
+
*/
|
|
4
|
+
export type PathEntry = [string, unknown];
|
|
5
|
+
/**
|
|
6
|
+
* Transforms an array of path entries into a nested object structure.
|
|
7
|
+
* Handles dot notation (e.g., "address.street") and bracket notation (e.g., "items[0].name").
|
|
8
|
+
* Multiple values for the same key are automatically grouped into arrays.
|
|
9
|
+
*
|
|
10
|
+
* @param entries - Array of [key, value] pairs where keys can use path notation
|
|
11
|
+
* @returns Nested object with grouped values
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const entries = [
|
|
15
|
+
* ["name", "John"],
|
|
16
|
+
* ["address.street", "123 Main St"],
|
|
17
|
+
* ["hobbies", "reading"],
|
|
18
|
+
* ["hobbies", "coding"]
|
|
19
|
+
* ];
|
|
20
|
+
* const result = objectFromPathEntries(entries);
|
|
21
|
+
* // Result: { name: "John", address: { street: "123 Main St" }, hobbies: ["reading", "coding"] }
|
|
22
|
+
*/
|
|
23
|
+
export declare function objectFromPathEntries(entries: Array<PathEntry>): Record<string, unknown>;
|
|
24
|
+
//# sourceMappingURL=object-from-path-entries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-from-path-entries.d.ts","sourceRoot":"","sources":["../../src/lib/object-from-path-entries.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmBxF"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transforms
|
|
2
|
+
* Transforms FormData, URLSearchParams, or plain objects into a nested object structure.
|
|
3
3
|
* Handles dot notation (e.g., "address.street") and bracket notation (e.g., "items[0].name").
|
|
4
4
|
* Multiple values for the same key are automatically grouped into arrays.
|
|
5
5
|
*
|
|
6
|
-
* @param
|
|
6
|
+
* @param data - FormData, URLSearchParams, or plain object to transform
|
|
7
7
|
* @returns Nested object with grouped values
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* ]);
|
|
16
16
|
* const result = preprocessFormData(formData);
|
|
17
17
|
* // Result: { name: "John", address: { street: "123 Main St" }, hobbies: ["reading", "coding"] }
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const data = { name: "John", age: "30" };
|
|
21
|
+
* const result = preprocessFormData(data);
|
|
22
|
+
* // Result: { name: "John", age: "30" }
|
|
18
23
|
*/
|
|
19
|
-
export declare function preprocessFormData(
|
|
24
|
+
export declare function preprocessFormData(data: FormData | URLSearchParams | Record<string, unknown>): Record<string, unknown>;
|
|
20
25
|
//# sourceMappingURL=preprocess-form-data.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preprocess-form-data.d.ts","sourceRoot":"","sources":["../../src/lib/preprocess-form-data.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"preprocess-form-data.d.ts","sourceRoot":"","sources":["../../src/lib/preprocess-form-data.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,GAAG,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACzD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOzB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"string-path-to-array-path.d.ts","sourceRoot":"","sources":["../../src/lib/string-path-to-array-path.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"string-path-to-array-path.d.ts","sourceRoot":"","sources":["../../src/lib/string-path-to-array-path.ts"],"names":[],"mappings":"AAyCA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAe1E"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed-web/form-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Shared utilities for form handling: path parsing, error conversion, and array diffing",
|
|
5
5
|
"author": "Łukasz Jagodziński <luke.jagodzinski@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
export type { FlatObject, NestedObject } from "./lib/types.ts";
|
|
3
3
|
|
|
4
4
|
// Path utilities
|
|
5
|
+
export { arrayPathToStringPath } from "./lib/array-path-to-string-path.ts";
|
|
5
6
|
export { setValueAtPath } from "./lib/set-value-at-path.ts";
|
|
6
7
|
export { stringPathToArrayPath } from "./lib/string-path-to-array-path.ts";
|
|
7
|
-
export { arrayPathToStringPath } from "./lib/array-path-to-string-path.ts";
|
|
8
8
|
|
|
9
9
|
// Structure conversion
|
|
10
10
|
export { flatObjectToNestedObject } from "./lib/flat-object-to-nested-object.ts";
|
|
@@ -15,4 +15,6 @@ export { getArraysDiff } from "./lib/get-arrays-diff.ts";
|
|
|
15
15
|
export type { GetArraysDiffArgs } from "./lib/get-arrays-diff.ts";
|
|
16
16
|
|
|
17
17
|
// FormData utilities
|
|
18
|
+
export { objectFromPathEntries } from "./lib/object-from-path-entries.ts";
|
|
19
|
+
export type { PathEntry } from "./lib/object-from-path-entries.ts";
|
|
18
20
|
export { preprocessFormData } from "./lib/preprocess-form-data.ts";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { setValueAtPath } from "./set-value-at-path.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Entry type for path-based key-value pairs.
|
|
5
|
+
*/
|
|
6
|
+
export type PathEntry = [string, unknown];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Transforms an array of path entries into a nested object structure.
|
|
10
|
+
* Handles dot notation (e.g., "address.street") and bracket notation (e.g., "items[0].name").
|
|
11
|
+
* Multiple values for the same key are automatically grouped into arrays.
|
|
12
|
+
*
|
|
13
|
+
* @param entries - Array of [key, value] pairs where keys can use path notation
|
|
14
|
+
* @returns Nested object with grouped values
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const entries = [
|
|
18
|
+
* ["name", "John"],
|
|
19
|
+
* ["address.street", "123 Main St"],
|
|
20
|
+
* ["hobbies", "reading"],
|
|
21
|
+
* ["hobbies", "coding"]
|
|
22
|
+
* ];
|
|
23
|
+
* const result = objectFromPathEntries(entries);
|
|
24
|
+
* // Result: { name: "John", address: { street: "123 Main St" }, hobbies: ["reading", "coding"] }
|
|
25
|
+
*/
|
|
26
|
+
export function objectFromPathEntries(entries: Array<PathEntry>): Record<string, unknown> {
|
|
27
|
+
// Group values by key (handling multiple values for same key)
|
|
28
|
+
const map = new Map<string, Array<unknown>>();
|
|
29
|
+
for (const [key, value] of entries) {
|
|
30
|
+
if (map.has(key)) {
|
|
31
|
+
map.get(key)?.push(value);
|
|
32
|
+
} else {
|
|
33
|
+
map.set(key, [value]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Build nested object using setValueAtPath for dot/bracket notation
|
|
38
|
+
const result = [...map].reduce<Record<string, unknown>>((acc, [key, list]) => {
|
|
39
|
+
// Single value stays as single value, multiple values become array
|
|
40
|
+
const value = list.length === 1 ? list[0] : list;
|
|
41
|
+
return setValueAtPath(acc, key, value);
|
|
42
|
+
}, {});
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { objectFromPathEntries } from "./object-from-path-entries.ts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Transforms
|
|
4
|
+
* Transforms FormData, URLSearchParams, or plain objects into a nested object structure.
|
|
5
5
|
* Handles dot notation (e.g., "address.street") and bracket notation (e.g., "items[0].name").
|
|
6
6
|
* Multiple values for the same key are automatically grouped into arrays.
|
|
7
7
|
*
|
|
8
|
-
* @param
|
|
8
|
+
* @param data - FormData, URLSearchParams, or plain object to transform
|
|
9
9
|
* @returns Nested object with grouped values
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
@@ -17,28 +17,19 @@ import { setValueAtPath } from "./set-value-at-path.ts";
|
|
|
17
17
|
* ]);
|
|
18
18
|
* const result = preprocessFormData(formData);
|
|
19
19
|
* // Result: { name: "John", address: { street: "123 Main St" }, hobbies: ["reading", "coding"] }
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const data = { name: "John", age: "30" };
|
|
23
|
+
* const result = preprocessFormData(data);
|
|
24
|
+
* // Result: { name: "John", age: "30" }
|
|
20
25
|
*/
|
|
21
26
|
export function preprocessFormData(
|
|
22
|
-
|
|
27
|
+
data: FormData | URLSearchParams | Record<string, unknown>,
|
|
23
28
|
): Record<string, unknown> {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const map = new Map<string, Array<unknown>>();
|
|
28
|
-
for (const [key, value] of entries) {
|
|
29
|
-
if (map.has(key)) {
|
|
30
|
-
map.get(key)?.push(value);
|
|
31
|
-
} else {
|
|
32
|
-
map.set(key, [value]);
|
|
33
|
-
}
|
|
29
|
+
// Check if data has entries method (FormData, URLSearchParams, Map, etc.)
|
|
30
|
+
if ("entries" in data && typeof data.entries === "function") {
|
|
31
|
+
return objectFromPathEntries([...data.entries()]);
|
|
34
32
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const result = [...map].reduce<Record<string, unknown>>((acc, [key, list]) => {
|
|
38
|
-
// Single value stays as single value, multiple values become array
|
|
39
|
-
const value = list.length === 1 ? list[0] : list;
|
|
40
|
-
return setValueAtPath(acc, key, value);
|
|
41
|
-
}, {});
|
|
42
|
-
|
|
43
|
-
return result;
|
|
33
|
+
// Plain object - use Object.entries
|
|
34
|
+
return objectFromPathEntries(Object.entries(data));
|
|
44
35
|
}
|
|
@@ -10,55 +10,33 @@ const DOT_NOTATION_REGEX = /^\.?([^\.\[\]]+)(.*)$/;
|
|
|
10
10
|
const NUMERIC_KEY_REGEX = /^\d+$/;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Internal
|
|
14
|
-
*
|
|
13
|
+
* Internal iterative parser for path segments.
|
|
14
|
+
* Consumes the path string segment by segment, trying bracket notation first,
|
|
15
|
+
* then dot notation. Returns null if any remaining portion cannot be parsed.
|
|
15
16
|
*
|
|
16
|
-
* @param
|
|
17
|
+
* @param path - The path string to parse
|
|
17
18
|
* @returns Array of path segments, or null if parsing fails
|
|
18
19
|
*/
|
|
19
|
-
function parsePath(
|
|
20
|
-
|
|
21
|
-
|
|
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);
|
|
20
|
+
function parsePath(path: string): Array<string | number> | null {
|
|
21
|
+
const result: Array<string | number> = [];
|
|
22
|
+
let remaining = path;
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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);
|
|
24
|
+
while (remaining.length > 0) {
|
|
25
|
+
// Try bracket notation first (e.g. "[0]", "[key]"), fall back to dot notation (e.g. ".prop", "prop").
|
|
26
|
+
const match = remaining.match(BRACKET_NOTATION_REGEX) ?? remaining.match(DOT_NOTATION_REGEX);
|
|
36
27
|
|
|
37
|
-
//
|
|
38
|
-
if (
|
|
28
|
+
// No pattern matched but we still have content — invalid path.
|
|
29
|
+
if (match === null) {
|
|
39
30
|
return null;
|
|
40
31
|
}
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
} else if (dotMatch) {
|
|
44
|
-
const [, key = "", rest = ""] = dotMatch;
|
|
33
|
+
const [, key = "", rest = ""] = match;
|
|
45
34
|
// Convert numeric keys to numbers, keep string keys as strings.
|
|
46
|
-
|
|
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];
|
|
35
|
+
result.push(NUMERIC_KEY_REGEX.test(key) ? Number(key) : key);
|
|
36
|
+
remaining = rest;
|
|
57
37
|
}
|
|
58
38
|
|
|
59
|
-
|
|
60
|
-
// Return null to signal failure
|
|
61
|
-
return null;
|
|
39
|
+
return result;
|
|
62
40
|
}
|
|
63
41
|
|
|
64
42
|
/**
|