Files
payload/src/fields/hooks/afterRead/promise.ts

356 lines
8.9 KiB
TypeScript

/* eslint-disable no-param-reassign */
import { Field, fieldAffectsData, TabAsField, tabHasName } from '../../config/types';
import { PayloadRequest } from '../../../express/types';
import { traverseFields } from './traverseFields';
import richTextRelationshipPromise from '../../richText/richTextRelationshipPromise';
import relationshipPopulationPromise from './relationshipPopulationPromise';
type Args = {
currentDepth: number
depth: number
doc: Record<string, unknown>
field: Field | TabAsField
fieldPromises: Promise<void>[]
findMany: boolean
flattenLocales: boolean
populationPromises: Promise<void>[]
req: PayloadRequest
overrideAccess: boolean
siblingDoc: Record<string, unknown>
showHiddenFields: boolean
}
// This function is responsible for the following actions, in order:
// - Remove hidden fields from response
// - Flatten locales into requested locale
// - Sanitize outgoing data (point field, etc)
// - Execute field hooks
// - Execute read access control
// - Populate relationships
export const promise = async ({
currentDepth,
depth,
doc,
field,
fieldPromises,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
req,
siblingDoc,
showHiddenFields,
}: Args): Promise<void> => {
if (fieldAffectsData(field) && field.hidden && typeof siblingDoc[field.name] !== 'undefined' && !showHiddenFields) {
delete siblingDoc[field.name];
}
const shouldHoistLocalizedValue = flattenLocales
&& fieldAffectsData(field)
&& (typeof siblingDoc[field.name] === 'object' && siblingDoc[field.name] !== null)
&& field.localized
&& req.locale !== 'all'
&& req.payload.config.localization;
if (shouldHoistLocalizedValue) {
// replace actual value with localized value before sanitizing
// { [locale]: fields } -> fields
const { locale } = req;
const value = siblingDoc[field.name][locale];
const fallbackLocale = req.payload.config.localization && req.payload.config.localization?.fallback && req.fallbackLocale;
let hoistedValue = value;
if (fallbackLocale && fallbackLocale !== locale) {
const fallbackValue = siblingDoc[field.name][fallbackLocale];
const isNullOrUndefined = typeof value === 'undefined' || value === null;
if (fallbackValue) {
switch (field.type) {
case 'text':
case 'textarea': {
if (value === '' || isNullOrUndefined) {
hoistedValue = fallbackValue;
}
break;
}
default: {
if (isNullOrUndefined) {
hoistedValue = fallbackValue;
}
break;
}
}
}
}
siblingDoc[field.name] = hoistedValue;
}
// Sanitize outgoing field value
switch (field.type) {
case 'group': {
// Fill groups with empty objects so fields with hooks within groups can populate
// themselves virtually as necessary
if (typeof siblingDoc[field.name] === 'undefined') {
siblingDoc[field.name] = {};
}
break;
}
case 'tabs': {
field.tabs.forEach((tab) => {
if (tabHasName(tab) && (typeof siblingDoc[tab.name] === 'undefined' || siblingDoc[tab.name] === null)) {
siblingDoc[tab.name] = {};
}
});
break;
}
case 'richText': {
if (((field.admin?.elements?.includes('relationship') || field.admin?.elements?.includes('upload') || field.admin?.elements?.includes('link')) || !field?.admin?.elements)) {
populationPromises.push(richTextRelationshipPromise({
currentDepth,
depth,
field,
overrideAccess,
req,
siblingDoc,
showHiddenFields,
}));
}
break;
}
case 'point': {
const pointDoc = siblingDoc[field.name] as Record<string, unknown>;
if (Array.isArray(pointDoc?.coordinates) && pointDoc.coordinates.length === 2) {
siblingDoc[field.name] = pointDoc.coordinates;
} else {
siblingDoc[field.name] = undefined;
}
break;
}
default: {
break;
}
}
if (fieldAffectsData(field)) {
// Execute hooks
if (field.hooks?.afterRead) {
await field.hooks.afterRead.reduce(async (priorHook, currentHook) => {
await priorHook;
const shouldRunHookOnAllLocales = field.localized
&& (req.locale === 'all' || !flattenLocales)
&& typeof siblingDoc[field.name] === 'object';
if (shouldRunHookOnAllLocales) {
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) => (async () => {
const hookedValue = await currentHook({
value,
originalDoc: doc,
data: doc,
siblingData: siblingDoc,
operation: 'read',
req,
});
if (hookedValue !== undefined) {
siblingDoc[field.name][locale] = hookedValue;
}
})());
await Promise.all(hookPromises);
} else {
const hookedValue = await currentHook({
data: doc,
findMany,
originalDoc: doc,
operation: 'read',
siblingData: siblingDoc,
req,
value: siblingDoc[field.name],
});
if (hookedValue !== undefined) {
siblingDoc[field.name] = hookedValue;
}
}
}, Promise.resolve());
}
// Execute access control
if (field.access && field.access.read) {
const result = overrideAccess ? true : await field.access.read({ req, id: doc.id as string | number, siblingData: siblingDoc, data: doc, doc });
if (!result) {
delete siblingDoc[field.name];
}
}
if (field.type === 'relationship' || field.type === 'upload') {
populationPromises.push(relationshipPopulationPromise({
currentDepth,
depth,
field,
overrideAccess,
req,
showHiddenFields,
siblingDoc,
}));
}
}
switch (field.type) {
case 'group': {
let groupDoc = siblingDoc[field.name] as Record<string, unknown>;
if (typeof siblingDoc[field.name] !== 'object') groupDoc = {};
traverseFields({
currentDepth,
depth,
doc,
fieldPromises,
fields: field.fields,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
req,
siblingDoc: groupDoc,
showHiddenFields,
});
break;
}
case 'array': {
const rows = siblingDoc[field.name];
if (Array.isArray(rows)) {
rows.forEach((row) => {
traverseFields({
currentDepth,
depth,
doc,
fields: field.fields,
fieldPromises,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
req,
siblingDoc: row || {},
showHiddenFields,
});
});
}
break;
}
case 'blocks': {
const rows = siblingDoc[field.name];
if (Array.isArray(rows)) {
rows.forEach((row) => {
const block = field.blocks.find((blockType) => blockType.slug === row.blockType);
if (block) {
traverseFields({
currentDepth,
depth,
doc,
fields: block.fields,
fieldPromises,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
req,
siblingDoc: row || {},
showHiddenFields,
});
}
});
}
break;
}
case 'row':
case 'collapsible': {
traverseFields({
currentDepth,
depth,
doc,
fieldPromises,
fields: field.fields,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
req,
siblingDoc,
showHiddenFields,
});
break;
}
case 'tab': {
let tabDoc = siblingDoc;
if (tabHasName(field)) {
tabDoc = siblingDoc[field.name] as Record<string, unknown>;
if (typeof siblingDoc[field.name] !== 'object') tabDoc = {};
}
await traverseFields({
currentDepth,
depth,
doc,
fieldPromises,
fields: field.fields,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
req,
siblingDoc: tabDoc,
showHiddenFields,
});
break;
}
case 'tabs': {
traverseFields({
currentDepth,
depth,
doc,
fieldPromises,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
findMany,
flattenLocales,
overrideAccess,
populationPromises,
req,
siblingDoc,
showHiddenFields,
});
break;
}
default: {
break;
}
}
};