From ac2ab181b22bd0c386e14c78ffaff7dad0ed06dc Mon Sep 17 00:00:00 2001 From: Nikas Praninskas Date: Tue, 10 Dec 2024 14:12:25 +0200 Subject: [PATCH] fix(sanity): optimise getLeafWeights to not stack overflow (#7999) --- .../deriveSearchWeightsFromType.test.ts | 46 ++++++++++++++ .../common/deriveSearchWeightsFromType.ts | 63 +++++++++---------- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/packages/sanity/src/core/search/common/__tests__/deriveSearchWeightsFromType.test.ts b/packages/sanity/src/core/search/common/__tests__/deriveSearchWeightsFromType.test.ts index e9f12e71c4d..4a237594a36 100644 --- a/packages/sanity/src/core/search/common/__tests__/deriveSearchWeightsFromType.test.ts +++ b/packages/sanity/src/core/search/common/__tests__/deriveSearchWeightsFromType.test.ts @@ -433,4 +433,50 @@ describe('deriveSearchWeightsFromType', () => { ], }) }) + + it('works for schemas that branch out a lot', () => { + // schema of 60 "components" with 10 fields each + const range = [...Array(60).keys()] + + const componentRefs = range.map((index) => ({type: `component_${index}`})) + const components = range.map((index) => + defineType({ + name: `component_${index}`, + type: 'object', + fields: [ + ...[...Array(10).keys()].map((fieldIndex) => + defineField({name: `component_${index}_field_${fieldIndex}`, type: 'string'}), + ), + defineField({name: `children_${index}`, type: 'array', of: [...componentRefs]}), + ], + }), + ) + + const schema = createSchema({ + name: 'default', + types: [ + ...components, + defineType({ + name: 'testType', + type: 'document', + fields: [ + defineField({ + name: 'components', + type: 'array', + of: [...componentRefs], + }), + ], + }), + ], + }) + + expect( + deriveSearchWeightsFromType({ + schemaType: schema.get('testType')!, + maxDepth: 5, + }), + ).toMatchObject({ + typeName: 'testType', + }) + }) }) diff --git a/packages/sanity/src/core/search/common/deriveSearchWeightsFromType.ts b/packages/sanity/src/core/search/common/deriveSearchWeightsFromType.ts index 0c5e5a7af2b..fc7228ad05a 100644 --- a/packages/sanity/src/core/search/common/deriveSearchWeightsFromType.ts +++ b/packages/sanity/src/core/search/common/deriveSearchWeightsFromType.ts @@ -67,58 +67,57 @@ function getLeafWeights( type: SchemaType | undefined, path: string, depth: number, + accumulator: SearchWeightEntry[] = [], // use accumulator to avoid stack overflow ): SearchWeightEntry[] { - if (!type) return [] - if (depth > maxDepth) return [] + if (!type) return accumulator + if (depth > maxDepth) return accumulator const typeChain = getTypeChain(type) if (isStringField(type) || isPtField(type)) { const weight = getWeight(type, path) - - if (typeof weight !== 'number') return [] - return [{path, weight, type: isPtField(type) ? 'pt' : 'string'}] + if (typeof weight === 'number') { + accumulator.push({path, weight, type: isPtField(type) ? 'pt' : 'string'}) + } + return accumulator } if (isSlugField(type)) { const weight = getWeight(type, path) - if (typeof weight !== 'number') return [] - return [ - { + if (typeof weight === 'number') { + accumulator.push({ path: getFullyQualifiedPath(type, path), weight, type: isPtField(type) ? 'pt' : 'string', - }, - ] + }) + } + return accumulator } - const results: SearchWeightEntry[] = [] - - const objectTypes = typeChain.filter( - (t): t is Extract => + let recursiveResult = accumulator + for (const t of typeChain) { + if ( t.jsonType === 'object' && !!t.fields?.length && - !ignoredBuiltInObjectTypes.includes(t.name), - ) - for (const objectType of objectTypes) { - for (const field of objectType.fields) { - const nextPath = pathToString([path, field.name].filter(Boolean)) - results.push(...traverse(field.type, nextPath, depth + 1)) - } - } - - const arrayTypes = typeChain.filter( - (t): t is Extract => - t.jsonType === 'array' && !!t.of?.length, - ) - for (const arrayType of arrayTypes) { - for (const arrayItemType of arrayType.of) { - const nextPath = `${path}[]` - results.push(...traverse(arrayItemType, nextPath, depth + 1)) + !ignoredBuiltInObjectTypes.includes(t.name) + ) { + for (const field of t.fields) { + recursiveResult = traverse( + field.type, + pathToString([path, field.name].filter(Boolean)), + depth + 1, + recursiveResult, + ) + } + } else if (t.jsonType === 'array' && !!t.of?.length) { + for (const arrayItemType of t.of) { + // eslint-disable-next-line no-param-reassign + recursiveResult = traverse(arrayItemType, `${path}[]`, depth + 1, recursiveResult) + } } } - return results + return recursiveResult } // Cross Dataset Reference are not part of the schema, so we should not attempt to reconcile them.