@strapi/utils
Version:
Shared utilities for the Strapi packages
279 lines (276 loc) • 8.76 kB
JavaScript
import { isNil, identity, constant, isString, split, join, trim, isEmpty, first, cloneDeep, isObject, curry, isArray } from 'lodash/fp';
import traverseFactory from './factory.mjs';
import { isMorphToRelationalAttribute } from '../content-types.mjs';
const isKeyword = (keyword)=>{
return ({ key, attribute })=>{
return !attribute && keyword === key;
};
};
const isWildcard = (value)=>value === '*';
const isPopulateString = (value)=>{
return isString(value) && !isWildcard(value);
};
const isStringArray = (value)=>isArray(value) && value.every(isString);
const isObj = (value)=>isObject(value);
const populate = traverseFactory().intercept(isPopulateString, async (visitor, options, populate, { recurse })=>{
/**
* Ensure the populate clause its in the extended format ( { populate: { ... } }, and not just a string)
* This gives a consistent structure to track the "parent" node of each nested populate clause
*/ const populateObject = pathsToObjectPopulate([
populate
]);
const traversedPopulate = await recurse(visitor, options, populateObject);
const [result] = objectPopulateToPaths(traversedPopulate);
return result;
})// Array of strings ['foo', 'bar.baz'] => map(recurse), then filter out empty items
.intercept(isStringArray, async (visitor, options, populate, { recurse })=>{
const paths = await Promise.all(populate.map((subClause)=>recurse(visitor, options, subClause)));
return paths.filter((item)=>!isNil(item));
})// for wildcard, generate custom utilities to modify the values
.parse(isWildcard, ()=>({
/**
* Since value is '*', we don't need to transform it
*/ transform: identity,
/**
* '*' isn't a key/value structure, so regardless
* of the given key, it returns the data ('*')
*/ get: (_key, data)=>data,
/**
* '*' isn't a key/value structure, so regardless
* of the given `key`, use `value` as the new `data`
*/ set: (_key, value)=>value,
/**
* '*' isn't a key/value structure, but we need to simulate at least one to enable
* the data traversal. We're using '' since it represents a falsy string value
*/ keys: constant([
''
]),
/**
* Removing '*' means setting it to undefined, regardless of the given key
*/ remove: constant(undefined)
}))// 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) || isEmpty(value) ? root : `${root}.${value}`;
},
keys (data) {
const v = first(tokenize(data));
return v ? [
v
] : [];
},
get (key, data) {
const [root, ...rest] = tokenize(data);
return key === root ? recompose(rest) : undefined;
}
};
})// Parse object values
.parse(isObj, ()=>({
transform: cloneDeep,
remove (key, data) {
// eslint-disable-next-line no-unused-vars
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 })=>{
// we don't want to recurse using traversePopulate and instead let
// the visitors recurse with the appropriate traversal (sort, filters, etc...)
return [
'sort',
'filters',
'fields'
].includes(key) && !attribute;
}).on(// Handle recursion on populate."populate"
isKeyword('populate'), async ({ key, visitor, path, value, schema, getModel, attribute }, { set, recurse })=>{
const parent = {
key,
path,
schema,
attribute
};
const newValue = await recurse(visitor, {
schema,
path,
getModel,
parent
}, value);
set(key, newValue);
}).on(isKeyword('on'), async ({ key, visitor, path, value, getModel, parent }, { set, recurse })=>{
const newOn = {};
if (!isObj(value)) {
return;
}
for (const [uid, subPopulate] of Object.entries(value)){
const model = getModel(uid);
const newPath = {
...path,
raw: `${path.raw}[${uid}]`
};
newOn[uid] = await recurse(visitor, {
schema: model,
path: newPath,
getModel,
parent
}, subPopulate);
}
set(key, newOn);
})// Handle populate on relation
.onRelation(async ({ key, value, attribute, visitor, path, schema, getModel }, { set, recurse })=>{
if (isNil(value)) {
return;
}
const parent = {
key,
path,
schema,
attribute
};
if (isMorphToRelationalAttribute(attribute)) {
// Don't traverse values that cannot be parsed
if (!isObject(value) || !('on' in value && isObject(value?.on))) {
return;
}
// If there is a populate fragment defined, traverse it
const newValue = await recurse(visitor, {
schema,
path,
getModel,
parent
}, {
on: value?.on
});
set(key, newValue);
return;
}
const targetSchemaUID = attribute.target;
const targetSchema = getModel(targetSchemaUID);
const newValue = await recurse(visitor, {
schema: targetSchema,
path,
getModel,
parent
}, value);
set(key, newValue);
})// Handle populate on media
.onMedia(async ({ key, path, schema, attribute, visitor, value, getModel }, { recurse, set })=>{
if (isNil(value)) {
return;
}
const parent = {
key,
path,
schema,
attribute
};
const targetSchemaUID = 'plugin::upload.file';
const targetSchema = getModel(targetSchemaUID);
const newValue = await recurse(visitor, {
schema: targetSchema,
path,
getModel,
parent
}, value);
set(key, newValue);
})// Handle populate on components
.onComponent(async ({ key, value, schema, visitor, path, attribute, getModel }, { recurse, set })=>{
if (isNil(value)) {
return;
}
const parent = {
key,
path,
schema,
attribute
};
const targetSchema = getModel(attribute.component);
const newValue = await recurse(visitor, {
schema: targetSchema,
path,
getModel,
parent
}, value);
set(key, newValue);
})// Handle populate on dynamic zones
.onDynamicZone(async ({ key, value, schema, visitor, path, attribute, getModel }, { set, recurse })=>{
if (isNil(value) || !isObject(value)) {
return;
}
const parent = {
key,
path,
schema,
attribute
};
// Handle fragment syntax
if ('on' in value && value.on) {
const newOn = await recurse(visitor, {
schema,
path,
getModel,
parent
}, {
on: value.on
});
set(key, newOn);
}
});
var traverseQueryPopulate = curry(populate.traverse);
const objectPopulateToPaths = (input)=>{
const paths = [];
function traverse(currentObj, parentPath) {
for (const [key, value] of Object.entries(currentObj)){
const currentPath = parentPath ? `${parentPath}.${key}` : key;
if (value === true) {
paths.push(currentPath);
} else {
traverse(value.populate, currentPath);
}
}
}
traverse(input, '');
return paths;
};
const pathsToObjectPopulate = (input)=>{
const result = {};
function traverse(object, keys) {
const [first, ...rest] = keys;
if (rest.length === 0) {
object[first] = true;
} else {
if (!object[first] || typeof object[first] === 'boolean') {
object[first] = {
populate: {}
};
}
traverse(object[first].populate, rest);
}
}
input.forEach((clause)=>traverse(result, clause.split('.')));
return result;
};
export { traverseQueryPopulate as default };
//# sourceMappingURL=query-populate.mjs.map