Last active
June 25, 2025 13:29
-
-
Save sk22/382e0850e01d618cbf72f73ad3d8e98e to your computer and use it in GitHub Desktop.
Function that recursively/deeply turns an object into placeholders and values
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { expect, test } from 'vitest'; | |
import { objectToPlaceholders } from './objectToPlaceholders.ts'; | |
const date = new Date(); | |
const blob = new Blob(); | |
const file = new File([], 'le file'); | |
const obj = { a: { b: date, foo: 'bar', baz: 123, c: [blob, file] } }; | |
test('objectToPlaceholders', () => { | |
const { placeholders, values } = objectToPlaceholders(obj); | |
expect(placeholders).toEqual({ a: { b: 0, foo: 1, baz: 2, c: [3, 4] } }); | |
expect(values).toEqual([date, 'bar', 123, blob, file]); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const objPrototype = Object.getPrototypeOf({}); | |
const isPlainObject = (value: unknown) => | |
typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === objPrototype; | |
/** | |
* I am a machine that turns | |
* | |
* ```ts | |
* { a: { b: file0, foo: 'bar', baz: 123, c: [file1, file2] } } | |
* ``` | |
* | |
* into | |
* | |
* ```ts | |
* { | |
* placeholders: { a: { b: 0, foo: 1, baz: 2, c: [ 3, 4 ] } }, | |
* values: [ file0, 'bar', 123, file1, file2 ] | |
* } | |
* ``` | |
*/ | |
export function objectToPlaceholders(obj: Record<string, unknown>): { | |
values: unknown[]; | |
placeholders: Record<string, unknown>; | |
} { | |
const values: unknown[] = []; | |
const placeholders: Record<string, unknown> = {}; | |
function processArray(key: number, value: unknown, currentPlaceholder: unknown[]) { | |
if (isPlainObject(value)) { | |
const nextPlaceholder = (currentPlaceholder[key] = {}); | |
recurse(value as Record<string, unknown>, nextPlaceholder); | |
} else if (Array.isArray(value)) { | |
const nextPlaceholder = (currentPlaceholder[key] = []); | |
values.forEach((item, index) => processArray(index, item, nextPlaceholder)); | |
} else { | |
values.push(value); | |
currentPlaceholder[key] = values.length - 1; | |
} | |
} | |
function processItem(key: string, value: unknown, currentPlaceholder: Record<string | number, unknown>) { | |
if (isPlainObject(value)) { | |
const nextPlaceholder = (currentPlaceholder[key] = {}); | |
recurse(value as Record<string, unknown>, nextPlaceholder); | |
} else if (Array.isArray(value)) { | |
const nextPlaceholder = (currentPlaceholder[key] = []); | |
value.forEach((item, index) => processArray(index, item, nextPlaceholder)); | |
} else { | |
values.push(value); | |
currentPlaceholder[key] = values.length - 1; | |
} | |
} | |
function recurse(currentObj: Record<string, unknown>, currentPlaceholder: Record<string, unknown>) { | |
for (const [key, value] of Object.entries(currentObj)) { | |
processItem(key, value, currentPlaceholder); | |
} | |
return currentPlaceholder; | |
} | |
recurse(obj, placeholders); | |
return { values, placeholders }; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { expect, test } from 'vitest'; | |
import { bodySerializerFormData } from './serializer'; | |
const blob = new Blob(); | |
const file1 = new File([], 'file1.pdf'); | |
const file2 = new File([], 'file2.pdf'); | |
test('bodySerializerFormData with deep values', () => { | |
const obj = { a: { b: blob, foo: 'bar', baz: 123, c: [file1, file2] } }; | |
const fd = bodySerializerFormData(obj); | |
// FormData converts Blob into File (subclass of Blob) | |
expect(fd.get('a.b')).toBeInstanceOf(Blob); | |
expect(fd.get('a.foo')).toBe('bar'); | |
expect(fd.getAll('a.c')).toHaveLength(2); | |
expect(fd.getAll('a.c')).toEqual([file1, file2]); | |
}); | |
test('bodySerializerFormData with null/undefined values', () => { | |
const obj = { a: 'yes', b: undefined, c: null, d: file1, files: [null, undefined, file2] }; | |
const fd = bodySerializerFormData(obj); | |
expect(fd.get('a')).toBe('yes'); | |
expect(fd.get('b')).toBe(null); | |
expect(fd.get('c')).toBe(null); | |
expect(fd.get('d')).toBe(file1); | |
expect(fd.getAll('files')).toEqual([file2]); | |
}); | |
test('bodySerializerFormData with shallow values', () => { | |
const fd = bodySerializerFormData({ guid: 'asdf', files: [file1, file2] }); | |
expect(fd.get('guid')).toBe('asdf'); | |
expect(fd.getAll('files')).toEqual([file1, file2]); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { stringify } from 'qs'; | |
import { objectToPlaceholders } from './objectToPlaceholders.ts'; | |
/** | |
* @param body Something like `{ file: File, otherFile?: File }` or `{ files: File[] }` | |
* @returns A FormData instance to be passed as a request's body | |
*/ | |
export function bodySerializerFormData(body?: Record<string, unknown>) { | |
const fd = new FormData(); | |
if (!body) return fd; | |
const { | |
placeholders, // { a: { b: 0, c: [1, 2] } } | |
values, // ['hello', 'wide', 'world'] | |
} = objectToPlaceholders(body); | |
// { a: { b: 0, c: [1, 2] } } -> { 'a.b' => '0', 'a.c[0]' => '1', 'a.c[1]' => '2' } | |
const search = new URLSearchParams(querySerializer(placeholders)); | |
for (const [key, index] of search) { | |
// `array[0]` -> `array` (because form data can have multiple values for the same name) | |
const name = key.replace(/\[\d+\]$/, ''); | |
const value = values[Number(index)]; | |
if (typeof value === 'string' || value instanceof Blob) fd.append(name, value); | |
} | |
return fd; | |
} | |
export function querySerializer(query: unknown) { | |
// backend expects object arrays encoded like `Attachments[0].Title=this works` (+ URI encoded) | |
return stringify(query, { allowDots: true }); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment