/
githubmirror
/
strapi
Обзор
Документация
Войти
/
githubmirror
/
strapi
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
packages/core/utils/src/traverse-entity.ts
316 строк
9 KB
DMehaffy
enhancement(utils): memoize scope decisions and keep the relation visitor sync (#27145)
07 авг 2026, 17:37
Не верифицирован
07 авг 2026, 17:37
0a8a9b4
Код
Авторство
О чём код?
import { clone, isObject, isArray, isNil, curry } from 'lodash/fp'; import type { Attribute, AnyAttribute, Model, Data } from './types'; import { isRelationalAttribute, isMediaAttribute } from './content-types'; /** * Execute promises in parallel but throw errors in array index order. */ const parallelWithOrderedErrors = async <T>(promises: Promise<T>[]): Promise<T[]> => { const results = await Promise.allSettled(promises); // Throw first error in array index order (matches sequential behavior) for (let i = 0; i < results.length; i += 1) { const result = results[i]; if (result.status === 'rejected') { throw result.reason; } } return results.map((r) => (r as PromiseFulfilledResult<T>).value); }; export type VisitorUtils = ReturnType<typeof createVisitorUtils>; export interface VisitorOptions { data: Record<string, unknown>; schema: Model; key: string; value: Data[keyof Data]; attribute?: AnyAttribute; path: Path; getModel(uid: string): Model; parent?: Parent; /** Extra root-level keys allowed (e.g. registered input params). Only used when path.attribute === null. */ allowedExtraRootKeys?: string[]; } export type Visitor = (visitorOptions: VisitorOptions, visitorUtils: VisitorUtils) => void; export interface Path { raw: string | null; attribute: string | null; rawWithIndices?: string | null; } export interface TraverseOptions { schema: Model; path?: Path; parent?: Parent; getModel(uid: string): Model; /** Extra root-level keys allowed (e.g. registered input params). Only used when path.attribute === null. */ allowedExtraRootKeys?: string[]; } export interface Parent { attribute?: Attribute; key: string | null; path: Path; schema: Model; } const traverseEntity = async ( visitor: Visitor, options: TraverseOptions, entity: Data ): Promise<Data> => { const { path = { raw: null, attribute: null, rawWithIndices: null }, schema, getModel, allowedExtraRootKeys, } = options; let parent = options.parent; const traverseMorphRelationTarget = async (visitor: Visitor, path: Path, entry: Data) => { const targetSchema = getModel(entry.__type!); const traverseOptions: TraverseOptions = { schema: targetSchema, path, getModel, parent, allowedExtraRootKeys, }; return traverseEntity(visitor, traverseOptions, entry); }; const traverseRelationTarget = (schema: Model) => async (visitor: Visitor, path: Path, entry: Data) => { const traverseOptions: TraverseOptions = { schema, path, getModel, parent, allowedExtraRootKeys, }; return traverseEntity(visitor, traverseOptions, entry); }; const traverseMediaTarget = async (visitor: Visitor, path: Path, entry: Data) => { const targetSchemaUID = 'plugin::upload.file'; const targetSchema = getModel(targetSchemaUID); const traverseOptions: TraverseOptions = { schema: targetSchema, path, getModel, parent, allowedExtraRootKeys, }; return traverseEntity(visitor, traverseOptions, entry); }; const traverseComponent = async (visitor: Visitor, path: Path, schema: Model, entry: Data) => { const traverseOptions: TraverseOptions = { schema, path, getModel, parent, allowedExtraRootKeys, }; return traverseEntity(visitor, traverseOptions, entry); }; const visitDynamicZoneEntry = async (visitor: Visitor, path: Path, entry: Data) => { // A dynamic zone array can contain a `null` entry (for example a relation // created inline inside a dynamic zone component can leave a null item). // Reading `__component` on it crashed traversal; pass nil entries through // untouched, consistent with how `traverseEntity` ends recursion on nil. (#24303) if (isNil(entry)) { return entry; } const targetSchema = getModel(entry.__component!); const traverseOptions: TraverseOptions = { schema: targetSchema, path, getModel, parent, allowedExtraRootKeys, }; return traverseEntity(visitor, traverseOptions, entry); }; // End recursion if (!isObject(entity) || isNil(schema)) { return entity; } // Don't mutate the original entity object // only clone at 1st level as the next level will get clone when traversed const copy = clone(entity); const visitorUtils = createVisitorUtils({ data: copy }); const keys = Object.keys(copy); for (let i = 0; i < keys.length; i += 1) { const key = keys[i]; // Retrieve the attribute definition associated to the key from the schema const attribute = schema.attributes[key] as AnyAttribute | undefined; const newPath = { ...path }; newPath.raw = isNil(path.raw) ? key : `${path.raw}.${key}`; newPath.rawWithIndices = isNil(path.rawWithIndices) ? key : `${path.rawWithIndices}.${key}`; if (!isNil(attribute)) { newPath.attribute = isNil(path.attribute) ? key : `${path.attribute}.${key}`; } // Visit the current attribute const visitorOptions: VisitorOptions = { data: copy, schema, key, value: copy[key], attribute, path: newPath, getModel, parent, allowedExtraRootKeys, }; // Awaited only when the visitor actually returns something thenable. Most visitors // finish synchronously for most keys — a scalar is not a relation, so the relation // visitor returns immediately — and `await` on a non-thenable still allocates a // promise and defers the rest of the loop to a microtask. Over every key of every // node of every entity in a page that was the largest single source of promise churn. const visited = visitor(visitorOptions, visitorUtils) as unknown; if (visited != null && typeof (visited as PromiseLike<void>).then === 'function') { await visited; } // Extract the value for the current key (after calling the visitor) const value = copy[key]; // Ignore Nil values or attributes if (isNil(value) || isNil(attribute)) { continue; } if (isRelationalAttribute(attribute)) { parent = { schema, key, attribute, path: newPath }; const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph'); const method = isMorphRelation ? traverseMorphRelationTarget : traverseRelationTarget(getModel(attribute.target!)); if (isArray(value)) { // Process array items in parallel with ordered error handling copy[key] = await parallelWithOrderedErrors( value.map((item, i) => { const arrayPath = { ...newPath, rawWithIndices: isNil(newPath.rawWithIndices) ? `${i}` : `${newPath.rawWithIndices}.${i}`, }; return method(visitor, arrayPath, item); }) ); } else { copy[key] = await method(visitor, newPath, value as Data); } continue; } if (isMediaAttribute(attribute)) { parent = { schema, key, attribute, path: newPath }; if (isArray(value)) { // Process media array items in parallel with ordered error handling copy[key] = await parallelWithOrderedErrors( value.map((item, i) => { const arrayPath = { ...newPath, rawWithIndices: isNil(newPath.rawWithIndices) ? `${i}` : `${newPath.rawWithIndices}.${i}`, }; return traverseMediaTarget(visitor, arrayPath, item); }) ); } else { copy[key] = await traverseMediaTarget(visitor, newPath, value as Data); } continue; } if (attribute.type === 'component') { parent = { schema, key, attribute, path: newPath }; const targetSchema = getModel(attribute.component); if (isArray(value)) { // Process component array items in parallel with ordered error handling copy[key] = await parallelWithOrderedErrors( value.map((item, i) => { const arrayPath = { ...newPath, rawWithIndices: isNil(newPath.rawWithIndices) ? `${i}` : `${newPath.rawWithIndices}.${i}`, }; return traverseComponent(visitor, arrayPath, targetSchema, item); }) ); } else { copy[key] = await traverseComponent(visitor, newPath, targetSchema, value as Data); } continue; } if (attribute.type === 'dynamiczone' && isArray(value)) { parent = { schema, key, attribute, path: newPath }; // Process dynamic zone items in parallel with ordered error handling copy[key] = await parallelWithOrderedErrors( value.map((item, i) => { const arrayPath = { ...newPath, rawWithIndices: isNil(newPath.rawWithIndices) ? `${i}` : `${newPath.rawWithIndices}.${i}`, }; return visitDynamicZoneEntry(visitor, arrayPath, item); }) ); continue; } } return copy; }; const createVisitorUtils = ({ data }: { data: Data }) => ({ remove(key: string) { delete data[key]; }, set(key: string, value: Data) { data[key] = value; }, }); export default curry(traverseEntity);