From 01dd55497a14bee31012d210777ee3aae0457c2e Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Tue, 4 Nov 2025 21:39:01 +0100 Subject: [PATCH 01/14] test: add test for #70 Signed-off-by: Mikael Karon --- test/entities/on-subgraphs-3.test.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/entities/on-subgraphs-3.test.js b/test/entities/on-subgraphs-3.test.js index ba0980d..b023ba4 100644 --- a/test/entities/on-subgraphs-3.test.js +++ b/test/entities/on-subgraphs-3.test.js @@ -43,6 +43,12 @@ function artistsSubgraph () { firstName: 'Brian', lastName: 'Molko', profession: 'Singer' + }, + 105: { + id: 105, + firstName: 'Luciano', + lastName: 'Pavarotti', + profession: 'Singer' } } } @@ -106,6 +112,11 @@ function songsSubgraphs () { id: 3, title: 'Vieni via con me', singerId: 102 + }, + 4: { + id: 4, + title: 'Nessun dorma', + singerId: 105 } } } @@ -389,6 +400,22 @@ test('entities on subgraph, scenario #3: entities with 1-1, 1-2-m, m-2-m relatio name: 'should run a query with insane nested results', query: '{ artists (ids: ["103"]) { songs { singer { songs { singer { songs { title } }} } } } }', result: { artists: [{ songs: [{ singer: { songs: [{ singer: { songs: [{ title: 'Every you every me' }, { title: 'The bitter end' }] } }, { singer: { songs: [{ title: 'Every you every me' }, { title: 'The bitter end' }] } }] } }, { singer: { songs: [{ singer: { songs: [{ title: 'Every you every me' }, { title: 'The bitter end' }] } }, { singer: { songs: [{ title: 'Every you every me' }, { title: 'The bitter end' }] } }] } }] }] } + }, + + { + name: 'should handle deeply nested queries without returning null', + query: '{ artists (ids: ["105"]) { firstName, songs { singer { firstName, songs { title } } } } }', + result: { + artists: [{ + firstName: 'Luciano', + songs: [{ + singer: { + firstName: 'Luciano', + songs: [{ title: 'Nessun dorma' }] + } + }] + }] + } } ] From e77178e7d7afef9e3231b16949668891e3b4226c Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Tue, 4 Nov 2025 22:35:31 +0100 Subject: [PATCH 02/14] fix: handle null values in copyObjectByKeys to prevent crashes This fix addresses a critical bug where copyObjectByKeys would attempt to recurse into null values due to JavaScript's typeof null === 'object' quirk. The function now explicitly checks for null before recursion. Also includes comprehensive test suite for copyObjectByKeys and adds a test case documenting issue #70 (currently skipped as it requires architectural changes to support entity resolution for entities nested within entity resolver results). Related to #70 Signed-off-by: Mikael Karon --- lib/utils.js | 2 +- test/entities/on-subgraphs-3.test.js | 24 ++++- test/utils-copy-object-by-keys.test.js | 134 +++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 test/utils-copy-object-by-keys.test.js diff --git a/lib/utils.js b/lib/utils.js index f20d232..6c6f541 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -46,7 +46,7 @@ function objectDeepClone (object) { function copyObjectByKeys (to, src) { const keys = Object.keys(src) for (let i = 0; i < keys.length; i++) { - if (typeof to[keys[i]] === 'object') { + if (to[keys[i]] !== null && typeof to[keys[i]] === 'object' && src[keys[i]] !== null && typeof src[keys[i]] === 'object') { copyObjectByKeys(to[keys[i]], src[keys[i]]) } else { to[keys[i]] ??= src[keys[i]] diff --git a/test/entities/on-subgraphs-3.test.js b/test/entities/on-subgraphs-3.test.js index b023ba4..e6a985e 100644 --- a/test/entities/on-subgraphs-3.test.js +++ b/test/entities/on-subgraphs-3.test.js @@ -66,7 +66,22 @@ function artistsSubgraph () { const entities = { Artist: { resolver: { name: 'artists' }, - pkey: 'id' + pkey: 'id', + many: [ + { + type: 'Song', + as: 'songs', + fkey: 'singerId', + pkey: 'id', + subgraph: 'songs-subgraph', + resolver: { + name: 'artistsSongs', + argsAdapter: (partialResults) => { + return { artistIds: partialResults.map(r => r?.id) } + } + } + } + ] } } @@ -403,7 +418,7 @@ test('entities on subgraph, scenario #3: entities with 1-1, 1-2-m, m-2-m relatio }, { - name: 'should handle deeply nested queries without returning null', + name: 'should handle deeply nested queries without returning null (issue #70)', query: '{ artists (ids: ["105"]) { firstName, songs { singer { firstName, songs { title } } } } }', result: { artists: [{ @@ -415,12 +430,13 @@ test('entities on subgraph, scenario #3: entities with 1-1, 1-2-m, m-2-m relatio } }] }] - } + }, + skip: 'Known limitation: entity resolution does not work for entities nested within entity resolver results. See issue #70' } ] for (const c of requests) { - await t.test(c.name, async (t) => { + await t.test(c.name, { skip: c.skip }, async (t) => { const result = await graphqlRequest(service, c.query, c.variables) assert.deepStrictEqual(result, c.result) diff --git a/test/utils-copy-object-by-keys.test.js b/test/utils-copy-object-by-keys.test.js new file mode 100644 index 0000000..832efdc --- /dev/null +++ b/test/utils-copy-object-by-keys.test.js @@ -0,0 +1,134 @@ +'use strict' + +const { test } = require('node:test') +const assert = require('node:assert') +const { copyObjectByKeys } = require('../lib/utils') + +test('copyObjectByKeys', async (t) => { + await t.test('should copy simple properties', () => { + const to = { id: '1' } + const src = { id: '1', name: 'John' } + + copyObjectByKeys(to, src) + + assert.deepStrictEqual(to, { id: '1', name: 'John' }) + }) + + await t.test('should not overwrite existing properties', () => { + const to = { id: '1', name: 'John' } + const src = { id: '1', name: 'Jane', age: 30 } + + copyObjectByKeys(to, src) + + assert.deepStrictEqual(to, { id: '1', name: 'John', age: 30 }) + }) + + await t.test('should handle nested objects', () => { + const to = { id: '1', user: { name: 'John' } } + const src = { id: '1', user: { name: 'John', age: 30 } } + + copyObjectByKeys(to, src) + + assert.deepStrictEqual(to, { id: '1', user: { name: 'John', age: 30 } }) + }) + + await t.test('should handle null values in destination without error', () => { + const to = { id: '1', user: null } + const src = { id: '1', user: { name: 'John', age: 30 } } + + // This should not throw an error + assert.doesNotThrow(() => { + copyObjectByKeys(to, src) + }) + + // null should be replaced with the object from src + assert.deepStrictEqual(to, { id: '1', user: { name: 'John', age: 30 } }) + }) + + await t.test('should handle null values in source', () => { + const to = { id: '1', user: { name: 'John' } } + const src = { id: '1', user: null } + + copyObjectByKeys(to, src) + + // null in src should not overwrite existing object in to + assert.deepStrictEqual(to, { id: '1', user: { name: 'John' } }) + }) + + await t.test('should handle mixed null and object scenarios', () => { + const to = { + id: '1', + user: null, + profile: { name: 'John' } + } + const src = { + id: '1', + user: { name: 'Jane' }, + profile: null + } + + copyObjectByKeys(to, src) + + assert.deepStrictEqual(to, { + id: '1', + user: { name: 'Jane' }, + profile: { name: 'John' } + }) + }) + + await t.test('should handle deeply nested structures with nulls', () => { + const to = { + artists: [{ + id: '105', + firstName: 'Luciano', + songs: [{ + singer: null + }] + }] + } + const src = { + artists: [{ + id: '105', + songs: [{ + singer: { + id: '105', + songs: [{ title: 'Nessun dorma' }] + } + }] + }] + } + + copyObjectByKeys(to, src) + + // firstName should be preserved, singer should be populated + assert.strictEqual(to.artists[0].firstName, 'Luciano') + assert.deepStrictEqual(to.artists[0].songs[0].singer, { + id: '105', + songs: [{ title: 'Nessun dorma' }] + }) + }) + + await t.test('should not recurse into null in destination', () => { + const to = { id: '1', nested: null } + const src = { id: '1', nested: { value: 'test' } } + + // Before the fix, this would throw: "Cannot convert undefined or null to object" + // or similar error when trying to call Object.keys on null + assert.doesNotThrow(() => { + copyObjectByKeys(to, src) + }) + + assert.deepStrictEqual(to, { id: '1', nested: { value: 'test' } }) + }) + + await t.test('should not recurse when both source and destination are not objects', () => { + const to = { id: '1', value: 'old' } + const src = { id: '1', value: 'new', extra: 'data' } + + copyObjectByKeys(to, src) + + // value should not be overwritten (??= operator) + assert.strictEqual(to.value, 'old') + assert.strictEqual(to.extra, 'data') + }) +}) From f2fc4fe6fb253701b1f72c165a3d426496f9d558 Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 00:21:07 +0100 Subject: [PATCH 03/14] fix: simplify object checks in `copyObjectByKeys` function Signed-off-by: Mikael Karon --- lib/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils.js b/lib/utils.js index 6c6f541..f4f18b1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -46,7 +46,7 @@ function objectDeepClone (object) { function copyObjectByKeys (to, src) { const keys = Object.keys(src) for (let i = 0; i < keys.length; i++) { - if (to[keys[i]] !== null && typeof to[keys[i]] === 'object' && src[keys[i]] !== null && typeof src[keys[i]] === 'object') { + if (isObject(to[keys[i]]) && isObject(src[keys[i]])) { copyObjectByKeys(to[keys[i]], src[keys[i]]) } else { to[keys[i]] ??= src[keys[i]] From e6d15a506c9c27c5cdf7655c6c503b7a87ee26d4 Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 00:26:07 +0100 Subject: [PATCH 04/14] fix: refactor `mergeResult` to improve path resolution logic Signed-off-by: Mikael Karon --- lib/result.js | 43 +++++++++++++++++----------- test/entities/on-subgraphs-3.test.js | 3 +- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/result.js b/lib/result.js index 3acd8b2..de42503 100644 --- a/lib/result.js +++ b/lib/result.js @@ -28,26 +28,21 @@ function mergeResult (mainResult, fullPath, queryNode, parentResult) { return } - // traverse result till bottom - let r = mainResult[path[0]] - let i = 1 - while (i < path.length) { - const t = traverseResult(r, path[i]) - if (!t) { break } - r = t - i++ - } + const parentPath = parentResult.path ?? [] + const containerPath = parentPath.length > 0 + ? parentPath.slice(0, -1) + : path.slice(0, -1) + + const fillPath = path.slice(containerPath.length) + let r = resolveResultPath(mainResult, containerPath) - // fill the missing result path - const fillPath = [] - for (let j = i; j < path.length; j++) { - fillPath.push(path[j]) + while (!r && containerPath.length > 0) { + fillPath.unshift(containerPath.pop()) + r = resolveResultPath(mainResult, containerPath) } if (!r) { - // copy reference - r = mergingResult - return + r = mainResult } const many = parentResult.as && parentResult.many @@ -198,6 +193,22 @@ function resultIndex (result, key) { return { list, map: index } } +function resolveResultPath (result, segments) { + if (!segments || segments.length === 0) { + return result + } + + let current = result + for (let i = 0; i < segments.length; i++) { + if (current === undefined || current === null) { + return current + } + current = traverseResult(current, segments[i]) + } + + return current +} + module.exports = { traverseResult, mergeResult, diff --git a/test/entities/on-subgraphs-3.test.js b/test/entities/on-subgraphs-3.test.js index e6a985e..6296460 100644 --- a/test/entities/on-subgraphs-3.test.js +++ b/test/entities/on-subgraphs-3.test.js @@ -430,8 +430,7 @@ test('entities on subgraph, scenario #3: entities with 1-1, 1-2-m, m-2-m relatio } }] }] - }, - skip: 'Known limitation: entity resolution does not work for entities nested within entity resolver results. See issue #70' + } } ] From 0b9fa39245dc8f30d8bb2e0b3045bb7ce3d72c91 Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 00:44:12 +0100 Subject: [PATCH 05/14] test: enhance nested query tests for artist and song relationships Signed-off-by: Mikael Karon --- test/entities/on-subgraphs-3.test.js | 114 ++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/test/entities/on-subgraphs-3.test.js b/test/entities/on-subgraphs-3.test.js index 6296460..17e9df5 100644 --- a/test/entities/on-subgraphs-3.test.js +++ b/test/entities/on-subgraphs-3.test.js @@ -418,7 +418,7 @@ test('entities on subgraph, scenario #3: entities with 1-1, 1-2-m, m-2-m relatio }, { - name: 'should handle deeply nested queries without returning null (issue #70)', + name: 'should handle deeply nested queries without returning null', query: '{ artists (ids: ["105"]) { firstName, songs { singer { firstName, songs { title } } } } }', result: { artists: [{ @@ -431,6 +431,118 @@ test('entities on subgraph, scenario #3: entities with 1-1, 1-2-m, m-2-m relatio }] }] } + }, + + { + name: 'should keep singer loops populated alongside sibling fields', + query: '{ artists (ids: ["103","105"]) { songs { title singer { firstName profession songs { title } } } } }', + result: { + artists: [ + { + songs: [ + { + title: 'Every you every me', + singer: { + firstName: 'Brian', + profession: 'Singer', + songs: [ + { title: 'Every you every me' }, + { title: 'The bitter end' } + ] + } + }, + { + title: 'The bitter end', + singer: { + firstName: 'Brian', + profession: 'Singer', + songs: [ + { title: 'Every you every me' }, + { title: 'The bitter end' } + ] + } + } + ] + }, + { + songs: [ + { + title: 'Nessun dorma', + singer: { + firstName: 'Luciano', + profession: 'Singer', + songs: [ + { title: 'Nessun dorma' } + ] + } + } + ] + } + ] + } + }, + + { + name: 'should preserve first names across alternating singer chains', + query: '{ artists (ids: ["103"]) { songs { singer { firstName songs { singer { firstName songs { title } } } } } } }', + result: { + artists: [ + { + songs: [ + { + singer: { + firstName: 'Brian', + songs: [ + { + singer: { + firstName: 'Brian', + songs: [ + { title: 'Every you every me' }, + { title: 'The bitter end' } + ] + } + }, + { + singer: { + firstName: 'Brian', + songs: [ + { title: 'Every you every me' }, + { title: 'The bitter end' } + ] + } + } + ] + } + }, + { + singer: { + firstName: 'Brian', + songs: [ + { + singer: { + firstName: 'Brian', + songs: [ + { title: 'Every you every me' }, + { title: 'The bitter end' } + ] + } + }, + { + singer: { + firstName: 'Brian', + songs: [ + { title: 'Every you every me' }, + { title: 'The bitter end' } + ] + } + } + ] + } + } + ] + } + ] + } } ] From 87d7c519ac09a792cc40cd47f323a8bb66274df1 Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 00:51:37 +0100 Subject: [PATCH 06/14] fix: refactor flattenResults function to handle nested arrays and null values Signed-off-by: Mikael Karon --- lib/query-builder.js | 45 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/lib/query-builder.js b/lib/query-builder.js index 52760cf..43caefc 100644 --- a/lib/query-builder.js +++ b/lib/query-builder.js @@ -57,17 +57,48 @@ function addDeferredQueryField (query, fieldName, queryFieldNode) { * uniforms any result to an array, filters null row * @returns array */ -function toArgsAdapterInput (result, path) { - if (!result) { return [] } +function flattenResults (value) { + if (!Array.isArray(value)) { + return value === undefined || value === null ? [] : [value] + } + + let size = 0 + for (let i = 0; i < value.length; i++) { + if (Array.isArray(value[i])) { + size += value[i].length + } else if (value[i] !== undefined && value[i] !== null) { + size++ + } + } - if (!Array.isArray(result)) { - return [result] + const flattened = new Array(size) + let index = 0 + for (let i = 0; i < value.length; i++) { + const current = value[i] + if (Array.isArray(current)) { + const nested = flattenResults(current) + for (let j = 0; j < nested.length; j++) { + flattened[index++] = nested[j] + } + } else if (current !== undefined && current !== null) { + flattened[index++] = current + } } - let r = result.filter(r => !!r) + if (index < flattened.length) { + flattened.length = index + } + + return flattened +} + +function toArgsAdapterInput (result, path) { + if (!result) { return [] } + + let r = Array.isArray(result) ? result.filter(r => !!r) : [result] if (!path) { - return r.flat() + return flattenResults(r) } // TODO use a specific fn instead of traverseResult to speed up @@ -86,7 +117,7 @@ function toArgsAdapterInput (result, path) { i++ } - return r.flat() + return flattenResults(r) } function buildQuery (query, parentResult) { From 99cfb7fa43b7cfb130397432831674b2bae0b285 Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 01:21:24 +0100 Subject: [PATCH 07/14] fix: simplify `flattenResults` function using `flat` and `filter` methods --- lib/query-builder.js | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/lib/query-builder.js b/lib/query-builder.js index 43caefc..33f79b6 100644 --- a/lib/query-builder.js +++ b/lib/query-builder.js @@ -62,34 +62,7 @@ function flattenResults (value) { return value === undefined || value === null ? [] : [value] } - let size = 0 - for (let i = 0; i < value.length; i++) { - if (Array.isArray(value[i])) { - size += value[i].length - } else if (value[i] !== undefined && value[i] !== null) { - size++ - } - } - - const flattened = new Array(size) - let index = 0 - for (let i = 0; i < value.length; i++) { - const current = value[i] - if (Array.isArray(current)) { - const nested = flattenResults(current) - for (let j = 0; j < nested.length; j++) { - flattened[index++] = nested[j] - } - } else if (current !== undefined && current !== null) { - flattened[index++] = current - } - } - - if (index < flattened.length) { - flattened.length = index - } - - return flattened + return value.flat(Infinity).filter(v => v !== undefined && v !== null) } function toArgsAdapterInput (result, path) { From ae38357c799ff2893f92d0b5b7ffdb40cc59b71d Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 01:26:57 +0100 Subject: [PATCH 08/14] fix: simplify while loop condition and use nullish coalescing in `mergeResult` function Signed-off-by: Mikael Karon --- lib/result.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/result.js b/lib/result.js index de42503..9a94dd8 100644 --- a/lib/result.js +++ b/lib/result.js @@ -36,14 +36,12 @@ function mergeResult (mainResult, fullPath, queryNode, parentResult) { const fillPath = path.slice(containerPath.length) let r = resolveResultPath(mainResult, containerPath) - while (!r && containerPath.length > 0) { + while (!r && containerPath.length) { fillPath.unshift(containerPath.pop()) r = resolveResultPath(mainResult, containerPath) } - if (!r) { - r = mainResult - } + r ??= mainResult const many = parentResult.as && parentResult.many let key, parentKey, index From 36887cda3cbd726e5cdaa38304f25e4a371f659b Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 01:27:48 +0100 Subject: [PATCH 09/14] fix: streamline `resultIndex` function by simplifying conditionals and consolidating loops Signed-off-by: Mikael Karon --- lib/result.js | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/lib/result.js b/lib/result.js index 9a94dd8..b7f1bf3 100644 --- a/lib/result.js +++ b/lib/result.js @@ -160,31 +160,18 @@ function copyResultRowList (dst, src, srcIndex, parentKey, keyPath, fillPath) { } function resultIndex (result, key) { - if (result.length < 1) { + if (!result.length) { return { list: false, map: new Map() } } + const list = Array.isArray(result[0][key]) const index = new Map() - if (list) { - for (let i = 0; i < result.length; i++) { - for (let j = 0; j < result[i][key].length; j++) { - const s = index.get(result[i][key][j]) - if (s) { - index.set(result[i][key][j], s.concat(i)) - continue - } - index.set(result[i][key][j], [i]) - } - } - } else { - for (let i = 0; i < result.length; i++) { - const s = index.get(result[i][key]) - if (s) { - index.set(result[i][key], s.concat(i)) - continue - } - index.set(result[i][key], [i]) + for (let i = 0; i < result.length; i++) { + const keys = list ? result[i][key] : [result[i][key]] + for (const k of keys) { + const existing = index.get(k) + index.set(k, existing ? existing.concat(i) : [i]) } } From fdbda1b8d8a909931fed050a9f7e449b273467f4 Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 01:28:19 +0100 Subject: [PATCH 10/14] fix: simplify `resolveResultPath` function by using optional chaining and for-of loop Signed-off-by: Mikael Karon --- lib/result.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/result.js b/lib/result.js index b7f1bf3..a6c97fa 100644 --- a/lib/result.js +++ b/lib/result.js @@ -179,16 +179,12 @@ function resultIndex (result, key) { } function resolveResultPath (result, segments) { - if (!segments || segments.length === 0) { - return result - } + if (!segments?.length) return result let current = result - for (let i = 0; i < segments.length; i++) { - if (current === undefined || current === null) { - return current - } - current = traverseResult(current, segments[i]) + for (const segment of segments) { + if (current == null) return current + current = traverseResult(current, segment) } return current From c263467f291037f6a400b23c36277c4f245f3cba Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 01:29:00 +0100 Subject: [PATCH 11/14] fix: optimize `copyResultRowList` function by consolidating key handling and improving performance Signed-off-by: Mikael Karon --- lib/result.js | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/result.js b/lib/result.js index a6c97fa..f02147a 100644 --- a/lib/result.js +++ b/lib/result.js @@ -125,28 +125,18 @@ function copyResultRowList (dst, src, srcIndex, parentKey, keyPath, fillPath) { if (!traverseDst?.[parentKey]) { return } // TODO !undefined !null + const keys = Array.isArray(traverseDst[parentKey]) ? traverseDst[parentKey] : [traverseDst[parentKey]] let rowIndexes = [] - // TODO more performant code - // design a different struct to avoid loops - if (Array.isArray(traverseDst[parentKey])) { - for (let i = 0; i < traverseDst[parentKey].length; i++) { - const indexes = srcIndex.map.get(traverseDst[parentKey][i]) - if (indexes) { rowIndexes = rowIndexes.concat(indexes) } - } - } else { - const indexes = srcIndex.map.get(traverseDst[parentKey]) - if (indexes) { rowIndexes = indexes } + for (const key of keys) { + const indexes = srcIndex.map.get(key) + if (indexes) rowIndexes = rowIndexes.concat(indexes) } for (; fillIndex < fillPath.length; fillIndex++) { if (!traverseDst[fillPath[fillIndex]]) { // TODO get result type from types if (fillIndex === fillPath.length - 1) { - // TODO more performant code - traverseDst[fillPath[fillIndex]] = [] - for (let i = 0; i < rowIndexes.length; i++) { - traverseDst[fillPath[fillIndex]].push(src[rowIndexes[i]]) - } + traverseDst[fillPath[fillIndex]] = rowIndexes.map(i => src[i]) return } traverseDst[fillPath[fillIndex]] = {} From 9104af892fb4cab78b8597f2a3bd7fb65d812547 Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 01:37:31 +0100 Subject: [PATCH 12/14] fix: simplify `toArgsAdapterInput` function by removing `flattenResults` and using `flat` method directly Signed-off-by: Mikael Karon --- lib/query-builder.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/lib/query-builder.js b/lib/query-builder.js index 33f79b6..8314394 100644 --- a/lib/query-builder.js +++ b/lib/query-builder.js @@ -53,25 +53,13 @@ function addDeferredQueryField (query, fieldName, queryFieldNode) { query.fields.push({ fieldName, queryFieldNode }) } -/** - * uniforms any result to an array, filters null row - * @returns array - */ -function flattenResults (value) { - if (!Array.isArray(value)) { - return value === undefined || value === null ? [] : [value] - } - - return value.flat(Infinity).filter(v => v !== undefined && v !== null) -} - function toArgsAdapterInput (result, path) { if (!result) { return [] } let r = Array.isArray(result) ? result.filter(r => !!r) : [result] if (!path) { - return flattenResults(r) + return r.flat() } // TODO use a specific fn instead of traverseResult to speed up @@ -90,7 +78,7 @@ function toArgsAdapterInput (result, path) { i++ } - return flattenResults(r) + return r.flat(Infinity) } function buildQuery (query, parentResult) { From 5a691825d9699be657fc16abbd303c0e7b8013f0 Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 01:41:58 +0100 Subject: [PATCH 13/14] fix: simplify `mergeResult` function by consolidating path handling logic Signed-off-by: Mikael Karon --- lib/result.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/result.js b/lib/result.js index f02147a..ffac6e1 100644 --- a/lib/result.js +++ b/lib/result.js @@ -28,14 +28,10 @@ function mergeResult (mainResult, fullPath, queryNode, parentResult) { return } - const parentPath = parentResult.path ?? [] - const containerPath = parentPath.length > 0 - ? parentPath.slice(0, -1) - : path.slice(0, -1) - + const containerPath = (parentResult.path?.length ? parentResult.path : path).slice(0, -1) const fillPath = path.slice(containerPath.length) - let r = resolveResultPath(mainResult, containerPath) + let r = resolveResultPath(mainResult, containerPath) while (!r && containerPath.length) { fillPath.unshift(containerPath.pop()) r = resolveResultPath(mainResult, containerPath) From dd3d5922bae4d7ea4c0e46c7dccbbe4a0596420a Mon Sep 17 00:00:00 2001 From: Mikael Karon Date: Wed, 5 Nov 2025 01:51:23 +0100 Subject: [PATCH 14/14] fix: update `toArgsAdapterInput` function documentation to clarify its purpose and return type Signed-off-by: Mikael Karon --- lib/query-builder.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/query-builder.js b/lib/query-builder.js index 8314394..979fe92 100644 --- a/lib/query-builder.js +++ b/lib/query-builder.js @@ -53,6 +53,10 @@ function addDeferredQueryField (query, fieldName, queryFieldNode) { query.fields.push({ fieldName, queryFieldNode }) } +/** + * uniforms any result to an array, filters null row + * @returns array + */ function toArgsAdapterInput (result, path) { if (!result) { return [] }