@typed-web/form-utils 0.1.1 → 0.2.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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -0
- package/dist/index.js.map +3 -3
- package/dist/lib/preprocess-form-data.d.ts +20 -0
- package/dist/lib/preprocess-form-data.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/lib/preprocess-form-data.ts +44 -0
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,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 { preprocessFormData } from "./lib/preprocess-form-data.ts";
|
|
9
10
|
//# 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,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"}
|
|
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;AAGlE,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -131,11 +131,30 @@ function getArraysDiff(args) {
|
|
|
131
131
|
}).filter((x) => x !== void 0);
|
|
132
132
|
return { added, removed, modified };
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
// src/lib/preprocess-form-data.ts
|
|
136
|
+
function preprocessFormData(formData) {
|
|
137
|
+
const entries = [...formData];
|
|
138
|
+
const map = /* @__PURE__ */ new Map();
|
|
139
|
+
for (const [key, value] of entries) {
|
|
140
|
+
if (map.has(key)) {
|
|
141
|
+
map.get(key)?.push(value);
|
|
142
|
+
} else {
|
|
143
|
+
map.set(key, [value]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const result = [...map].reduce((acc, [key, list]) => {
|
|
147
|
+
const value = list.length === 1 ? list[0] : list;
|
|
148
|
+
return setValueAtPath(acc, key, value);
|
|
149
|
+
}, {});
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
134
152
|
export {
|
|
135
153
|
arrayPathToStringPath,
|
|
136
154
|
flatObjectToNestedObject,
|
|
137
155
|
getArraysDiff,
|
|
138
156
|
nestedObjectToFlatObject,
|
|
157
|
+
preprocessFormData,
|
|
139
158
|
setValueAtPath,
|
|
140
159
|
stringPathToArrayPath
|
|
141
160
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
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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",
|
|
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", "../src/lib/preprocess-form-data.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", "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": ";AACA,IAAM,yBAAyB;AAI/B,IAAM,qBAAqB;AAI3B,IAAM,oBAAoB;AAS1B,SAAS,UAAU,aAAoD;AAErE,MAAI,YAAY,WAAW,GAAG;AAC5B,WAAO,CAAC;AAAA,EACV;AAGA,QAAM,eAAe,YAAY,MAAM,sBAAsB;AAC7D,QAAM,WAAW,YAAY,MAAM,kBAAkB;AAErD,MAAI,cAAc;AAChB,UAAM,CAAC,EAAE,MAAM,IAAI,OAAO,EAAE,IAAI;AAEhC,UAAM,YAAY,kBAAkB,KAAK,GAAG,IAAI,OAAO,GAAG,IAAI;AAG9D,UAAM,aAAa,UAAU,IAAI;AAGjC,QAAI,eAAe,MAAM;AACvB,aAAO;AAAA,IACT;AAEA,WAAO,CAAC,WAAW,GAAG,UAAU;AAAA,EAClC,WAAW,UAAU;AACnB,UAAM,CAAC,EAAE,MAAM,IAAI,OAAO,EAAE,IAAI;AAEhC,UAAM,YAAY,kBAAkB,KAAK,GAAG,IAAI,OAAO,GAAG,IAAI;AAG9D,UAAM,aAAa,UAAU,IAAI;AAGjC,QAAI,eAAe,MAAM;AACvB,aAAO;AAAA,IACT;AAEA,WAAO,CAAC,WAAW,GAAG,UAAU;AAAA,EAClC;AAIA,SAAO;AACT;AAkBO,SAAS,sBAAsB,MAAsC;AAE1E,MAAI,KAAK,WAAW,GAAG;AACrB,WAAO,CAAC;AAAA,EACV;AAGA,QAAM,SAAS,UAAU,IAAI;AAG7B,MAAI,WAAW,MAAM;AACnB,WAAO,CAAC,IAAI;AAAA,EACd;AAEA,SAAO;AACT;;;ACnEO,SAAS,eACd,QACA,MACA,OACG;AACH,QAAM,eAAe,sBAAsB,IAAI;AAG/C,MAAI,aAAa,WAAW,GAAG;AAC7B,UAAM,IAAI,MAAM,gCAAgC;AAAA,EAClD;AAGA,QAAM,kBAAkB,aAAa,MAAM,GAAG,EAAE;AAChD,QAAM,cAAc,aAAa,aAAa,SAAS,CAAC;AAGxD,MAAI,gBAAgB,QAAW;AAC7B,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAGA,MAAI,gBAAkD;AAEtD,WAAS,IAAI,GAAG,IAAI,gBAAgB,QAAQ,KAAK;AAC/C,UAAM,iBAAiB,gBAAgB,CAAC;AAGxC,QAAI,mBAAmB,QAAW;AAChC,YAAM,IAAI,MAAM,kCAAkC,CAAC,eAAe;AAAA,IACpE;AAGA,QAAI,EAAE,kBAAkB,gBAAgB;AAEtC,YAAM,cAAc,gBAAgB,IAAI,CAAC,KAAK;AAE9C,oBAAc,cAAc,IAAI,OAAO,gBAAgB,WAAW,CAAC,IAAI,CAAC;AAAA,IAC1E;AAGA,UAAM,YAAY,cAAc,cAAc;AAG9C,QAAI,OAAO,cAAc,YAAY,cAAc,MAAM;AACvD,YAAM,IAAI;AAAA,QACR,6DAA6D,cAAc,UAAU,OAAO,SAAS;AAAA,MACvG;AAAA,IACF;AAEA,oBAAgB;AAAA,EAClB;AAGA,gBAAc,WAAW,IAAI;AAE7B,SAAO;AACT;;;ACvEO,SAAS,sBAAsB,MAAkC;AACtE,SAAO,KAAK,OAAO,CAAC,KAAa,SAAS;AACxC,QAAI,OAAO,SAAS,UAAU;AAC5B,aAAO,OAAO,QAAQ,KAAK,KAAK,OAAO,KAAK,SAAS;AAAA,IACvD;AACA,QAAI,OAAO,SAAS,YAAY,CAAC,OAAO,MAAM,OAAO,IAAI,CAAC,GAAG;AAC3D,aAAO,MAAM,MAAM,OAAO;AAAA,IAC5B;AACA,WAAO,OAAO,QAAQ,KAAK,KAAK,OAAO;AAAA,EACzC,GAAG,EAAE;AACP;;;ACFO,SAAS,yBACd,eACiB;AACjB,QAAM,SAAS,CAAC;AAEhB,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,aAAa,GAAG;AACzD,mBAAe,QAAQ,MAAM,KAAK;AAAA,EACpC;AAEA,SAAO;AACT;;;ACXO,SAAS,yBACd,iBACe;AACf,QAAM,SAAwB,CAAC;AAE/B,WAAS,KAAK,KAAc,aAAqB;AAC/C,QAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAE3C,aAAO,WAAW,IAAI;AACtB;AAAA,IACF;AAEA,QAAI,MAAM,QAAQ,GAAG,GAAG;AAEtB,UAAI,QAAQ,CAAC,MAAM,UAAU;AAC3B,cAAM,UAAU,cAAc,GAAG,WAAW,IAAI,KAAK,MAAM,IAAI,KAAK;AACpE,aAAK,MAAM,OAAO;AAAA,MACpB,CAAC;AAAA,IACH,OAAO;AAEL,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC9C,cAAM,UAAU,cAAc,GAAG,WAAW,IAAI,GAAG,KAAK;AACxD,aAAK,OAAO,OAAO;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAEA,OAAK,iBAAiB,EAAE;AAExB,SAAO;AACT;;;AC3CO,SAAS,cACd,MAKA;AACA,QAAM,EAAE,QAAQ,OAAO,KAAK,OAAO,IAAI;AACvC,QAAM,UAAU;AAGhB,QAAM,YAAY,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACxD,QAAM,WAAW,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAEtD,QAAM,QAAQ,MAAM,OAAO,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC;AACxD,QAAM,UAAU,OAAO,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC;AAE1D,QAAM,WAAW,MACd,IAAI,CAAC,MAA2C;AAC/C,UAAM,IAAI,UAAU,IAAI,IAAI,CAAC,CAAC;AAC9B,WAAO,MAAM,UAAa,CAAC,QAAQ,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,IAAI;AAAA,EACvE,CAAC,EACA,OAAO,CAAC,MAAoC,MAAM,MAAS;AAE9D,SAAO,EAAE,OAAO,SAAS,SAAS;AACpC;;;ACZO,SAAS,mBACd,UACyB;AACzB,QAAM,UAAU,CAAC,GAAG,QAAQ;AAG5B,QAAM,MAAM,oBAAI,IAA4B;AAC5C,aAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AAClC,QAAI,IAAI,IAAI,GAAG,GAAG;AAChB,UAAI,IAAI,GAAG,GAAG,KAAK,KAAK;AAAA,IAC1B,OAAO;AACL,UAAI,IAAI,KAAK,CAAC,KAAK,CAAC;AAAA,IACtB;AAAA,EACF;AAGA,QAAM,SAAS,CAAC,GAAG,GAAG,EAAE,OAAgC,CAAC,KAAK,CAAC,KAAK,IAAI,MAAM;AAE5E,UAAM,QAAQ,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI;AAC5C,WAAO,eAAe,KAAK,KAAK,KAAK;AAAA,EACvC,GAAG,CAAC,CAAC;AAEL,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transforms flat FormData/URLSearchParams entries into a nested object structure.
|
|
3
|
+
* Handles dot notation (e.g., "address.street") and bracket notation (e.g., "items[0].name").
|
|
4
|
+
* Multiple values for the same key are automatically grouped into arrays.
|
|
5
|
+
*
|
|
6
|
+
* @param formData - Iterable of key-value pairs (FormData or URLSearchParams)
|
|
7
|
+
* @returns Nested object with grouped values
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const formData = new URLSearchParams([
|
|
11
|
+
* ["name", "John"],
|
|
12
|
+
* ["address.street", "123 Main St"],
|
|
13
|
+
* ["hobbies", "reading"],
|
|
14
|
+
* ["hobbies", "coding"]
|
|
15
|
+
* ]);
|
|
16
|
+
* const result = preprocessFormData(formData);
|
|
17
|
+
* // Result: { name: "John", address: { street: "123 Main St" }, hobbies: ["reading", "coding"] }
|
|
18
|
+
*/
|
|
19
|
+
export declare function preprocessFormData(formData: Iterable<unknown>): Record<string, unknown>;
|
|
20
|
+
//# sourceMappingURL=preprocess-form-data.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preprocess-form-data.d.ts","sourceRoot":"","sources":["../../src/lib/preprocess-form-data.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,GAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqBzB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typed-web/form-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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
|
@@ -13,3 +13,6 @@ export { nestedObjectToFlatObject } from "./lib/nested-object-to-flat-object.ts"
|
|
|
13
13
|
// Array utilities
|
|
14
14
|
export { getArraysDiff } from "./lib/get-arrays-diff.ts";
|
|
15
15
|
export type { GetArraysDiffArgs } from "./lib/get-arrays-diff.ts";
|
|
16
|
+
|
|
17
|
+
// FormData utilities
|
|
18
|
+
export { preprocessFormData } from "./lib/preprocess-form-data.ts";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { setValueAtPath } from "./set-value-at-path.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transforms flat FormData/URLSearchParams entries into a nested object structure.
|
|
5
|
+
* Handles dot notation (e.g., "address.street") and bracket notation (e.g., "items[0].name").
|
|
6
|
+
* Multiple values for the same key are automatically grouped into arrays.
|
|
7
|
+
*
|
|
8
|
+
* @param formData - Iterable of key-value pairs (FormData or URLSearchParams)
|
|
9
|
+
* @returns Nested object with grouped values
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const formData = new URLSearchParams([
|
|
13
|
+
* ["name", "John"],
|
|
14
|
+
* ["address.street", "123 Main St"],
|
|
15
|
+
* ["hobbies", "reading"],
|
|
16
|
+
* ["hobbies", "coding"]
|
|
17
|
+
* ]);
|
|
18
|
+
* const result = preprocessFormData(formData);
|
|
19
|
+
* // Result: { name: "John", address: { street: "123 Main St" }, hobbies: ["reading", "coding"] }
|
|
20
|
+
*/
|
|
21
|
+
export function preprocessFormData(
|
|
22
|
+
formData: Iterable<unknown>,
|
|
23
|
+
): Record<string, unknown> {
|
|
24
|
+
const entries = [...formData] as Array<[string, unknown]>;
|
|
25
|
+
|
|
26
|
+
// Group values by key (handling multiple values for same key)
|
|
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
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Build nested object using setValueAtPath for dot/bracket notation
|
|
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;
|
|
44
|
+
}
|