diff --git a/src/index.ts b/src/index.ts index d34a8f8..c490bb0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -120,12 +120,15 @@ const handleRecord = ( `ar${i}v[ar${i}s[i]]=${mirror(child, `ar${i}p`, instruction)}` const optionals = instruction.optionalsInArray[i + 1] - if (optionals) + if (optionals) { for (let oi = 0; oi < optionals.length; oi++) { const target = `ar${i}v[ar${i}s[i]]${optionals[oi]}` v += `;if(${target}===undefined)delete ${target}` } + // Clear the optionals array after use to prevent pollution across sibling arrays + instruction.optionalsInArray[i + 1] = [] + } v += `}` + `return ar${i}v` + `})()` @@ -394,8 +397,11 @@ const mirror = ( } const array = instruction.optionalsInArray - if (array[index]) array[index].push(refName) - else array[index] = [refName] + if (array[index]) { + array[index].push(refName) + } else { + array[index] = [refName] + } } else { instruction.optionals.push(name) } @@ -481,6 +487,8 @@ const mirror = ( // we can add semi-colon here because it delimit recursive mirror v += `;if(${target}===undefined)delete ${target}` } + // Clear the optionals array after use to prevent pollution across sibling arrays + instruction.optionalsInArray[i + 1] = [] } v += `}` diff --git a/test/array-nested-optionals.test.ts b/test/array-nested-optionals.test.ts new file mode 100644 index 0000000..dd9c726 --- /dev/null +++ b/test/array-nested-optionals.test.ts @@ -0,0 +1,105 @@ +import { expect, test, describe } from 'bun:test' +import { Type } from '@sinclair/typebox' +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { createMirror } from '../src/index' + +describe('Nested Array with Optional Properties', () => { + test('should preserve array items when cleaning nested arrays', () => { + const WeightSchema = Type.Object({ + amount: Type.Number(), + unit: Type.Union([ + Type.Literal('g'), + Type.Literal('oz'), + Type.Literal('lb'), + Type.Literal('kg') + ]) + }) + + const PourSchema = Type.Object({ + weight: WeightSchema, + time: Type.Number() + }) + + const ResponseSchema = Type.Object({ + data: Type.Array( + Type.Object({ + id: Type.String(), + pours: Type.Union([Type.Null(), Type.Array(PourSchema)]), + tags: Type.Array(Type.Object({ name: Type.String() })), + createdAt: Type.Transform( + Type.Union([Type.Date(), Type.String()]) + ) + .Decode((v) => (v instanceof Date ? v : new Date(v))) + .Encode((v) => v.toISOString()) + }) + ) + }) + + const clean = createMirror(ResponseSchema, { TypeCompiler }) + + const input = { + data: [ + { + id: 'test-1', + pours: null, + tags: [{ name: 'test' }], + createdAt: new Date('2025-01-01'), + extraProp: 'should-be-removed' + } + ] + } + + const result = clean(input) + + // Array items should be preserved + expect(result.data).toHaveLength(1) + expect(result.data[0].id).toBe('test-1') + expect(result.data[0].pours).toBe(null) + expect(result.data[0].tags).toHaveLength(1) + expect(result.data[0].tags[0].name).toBe('test') + + // Extra property should be removed + expect(result.data[0]).not.toHaveProperty('extraProp') + }) + + test('should handle multiple nested arrays correctly', () => { + const Schema = Type.Object({ + outer: Type.Array( + Type.Object({ + middle: Type.Array( + Type.Object({ + inner: Type.Array(Type.String()), + value: Type.String() + }) + ) + }) + ) + }) + + const clean = createMirror(Schema, { TypeCompiler }) + + const input = { + outer: [ + { + middle: [ + { + inner: ['a', 'b'], + value: 'test', + extra: 'remove' + } + ], + extraOuter: 'remove' + } + ] + } + + const result = clean(input) + + expect(result.outer).toHaveLength(1) + expect(result.outer[0].middle).toHaveLength(1) + expect(result.outer[0].middle[0].inner).toHaveLength(2) + expect(result.outer[0].middle[0].value).toBe('test') + expect(result.outer[0].middle[0]).not.toHaveProperty('extra') + expect(result.outer[0]).not.toHaveProperty('extraOuter') + }) +})