strapi/packages/core/utils/lib/traverse/query-populate.js

192 lines
5.0 KiB
JavaScript

'use strict';
const {
curry,
isString,
isArray,
eq,
constant,
split,
isObject,
trim,
isNil,
cloneDeep,
join,
first,
} = require('lodash/fp');
const traverseFactory = require('./factory');
const isKeyword =
(keyword) =>
({ key, attribute }) => {
return !attribute && keyword === key;
};
const isStringArray = (value) => isArray(value) && value.every(isString);
const populate = traverseFactory()
// Array of strings ['foo', 'foo.bar'] => map(recurse), then filter out empty items
.intercept(isStringArray, async (visitor, options, populate, { recurse }) => {
return Promise.all(populate.map((nestedPopulate) => recurse(visitor, options, nestedPopulate)));
})
// Return wildcards as is
.intercept(eq('*'), constant('*'))
// Parse string values
.parse(isString, () => {
const tokenize = split('.');
const recompose = join('.');
return {
transform: trim,
remove(key, data) {
const [root] = tokenize(data);
return root === key ? undefined : data;
},
set(key, value, data) {
const [root] = tokenize(data);
if (root !== key) {
return data;
}
return isNil(value) ? root : `${root}.${value}`;
},
keys(data) {
return [first(tokenize(data))];
},
get(key, data) {
const [root, ...rest] = tokenize(data);
return key === root ? recompose(rest) : undefined;
},
};
})
// Parse object values
.parse(isObject, () => ({
transform: cloneDeep,
remove(key, data) {
const { [key]: ignored, ...rest } = data;
return rest;
},
set(key, value, data) {
return { ...data, [key]: value };
},
keys(data) {
return Object.keys(data);
},
get(key, data) {
return data[key];
},
}))
.ignore(({ key, attribute }) => {
return ['sort', 'filters', 'fields'].includes(key) && !attribute;
})
.on(
// Handle recursion on populate."populate"
isKeyword('populate'),
async ({ key, visitor, path, value, schema }, { set, recurse }) => {
const newValue = await recurse(visitor, { schema, path }, value);
set(key, newValue);
}
)
.on(isKeyword('on'), async ({ key, visitor, path, value }, { set, recurse }) => {
const newOn = {};
for (const [uid, subPopulate] of Object.entries(value)) {
const model = strapi.getModel(uid);
const newPath = { ...path, raw: `${path.raw}[${uid}]` };
const newSubPopulate = await recurse(visitor, { schema: model, path: newPath }, subPopulate);
newOn[uid] = newSubPopulate;
}
set(key, newOn);
})
// Handle populate on relation
.onRelation(async ({ key, value, attribute, visitor, path, schema }, { set, recurse }) => {
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
if (isMorphRelation) {
// Don't traverse values that cannot be parsed
if (!isObject(value) || !isObject(value?.on)) {
return;
}
// If there is a populate fragment defined, traverse it
const newValue = await recurse(visitor, { schema, path }, { on: value.on });
set(key, { on: newValue });
}
const targetSchemaUID = attribute.target;
const targetSchema = strapi.getModel(targetSchemaUID);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
})
// Handle populate on media
.onMedia(async ({ key, path, visitor, value }, { recurse, set }) => {
const targetSchemaUID = 'plugin::upload.file';
const targetSchema = strapi.getModel(targetSchemaUID);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
})
// Handle populate on components
.onComponent(async ({ key, value, visitor, path, attribute }, { recurse, set }) => {
const targetSchema = strapi.getModel(attribute.component);
const newValue = await recurse(visitor, { schema: targetSchema, path }, value);
set(key, newValue);
})
// Handle populate on dynamic zones
.onDynamicZone(async ({ key, value, attribute, schema, visitor, path }, { set, recurse }) => {
if (isObject(value)) {
const { components } = attribute;
const { on, ...properties } = value;
const newValue = {};
// Handle legacy DZ params
let newProperties = properties;
for (const componentUID of components) {
const componentSchema = strapi.getModel(componentUID);
newProperties = await recurse(visitor, { schema: componentSchema, path }, newProperties);
}
Object.assign(newValue, newProperties);
// Handle new morph fragment syntax
if (on) {
const newOn = await recurse(visitor, { schema, path }, { on });
// Recompose both syntaxes
Object.assign(newValue, newOn);
}
set(key, newValue);
} else {
const newValue = await recurse(visitor, { schema, path }, value);
set(key, newValue);
}
});
module.exports = curry(populate.traverse);