From ae574099aa06f7c3771dbf9cc41a6cce01c655f4 Mon Sep 17 00:00:00 2001 From: MarcelOlsen Date: Wed, 24 Dec 2025 12:43:46 +0100 Subject: [PATCH] fix: prevent optionalsInArray pollution across sibling arrays The issue occurred when processing sibling arrays at the same nesting level (e.g., 'pours' and 'tags' both inside data items). After processing one array's items, its optionals remained in optionalsInArray and were incorrectly applied to the next sibling array, causing cleanup code to try deleting non-existent properties. Solution: Clear optionalsInArray[i + 1] after using it to generate cleanup code, ensuring each array only processes its own optionals. --- src/index.ts | 14 +++- test/array-nested-optionals.test.ts | 105 ++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 test/array-nested-optionals.test.ts 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') + }) +})