feat: optimizes field operations
* wip: beforeChange field op pattern * feat: optimizes field-level beforeChange * feat: optimizes beforeValidate * feat: optimizes afterChange * feat: optimizes afterRead * chore: comment accuracy
This commit is contained in:
@@ -1,73 +0,0 @@
|
||||
import { Payload } from '..';
|
||||
import { HookName, FieldAffectingData } from './config/types';
|
||||
import relationshipPopulationPromise from './relationshipPopulationPromise';
|
||||
import { Operation } from '../types';
|
||||
import { PayloadRequest } from '../express/types';
|
||||
|
||||
type Arguments = {
|
||||
data: Record<string, unknown>
|
||||
fullData: Record<string, unknown>
|
||||
originalDoc: Record<string, unknown>
|
||||
field: FieldAffectingData
|
||||
operation: Operation
|
||||
overrideAccess: boolean
|
||||
req: PayloadRequest
|
||||
id: string | number
|
||||
relationshipPopulations: (() => Promise<void>)[]
|
||||
depth: number
|
||||
currentDepth: number
|
||||
hook: HookName
|
||||
payload: Payload
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
const accessPromise = async ({
|
||||
data,
|
||||
fullData,
|
||||
field,
|
||||
operation,
|
||||
overrideAccess,
|
||||
req,
|
||||
id,
|
||||
relationshipPopulations,
|
||||
depth,
|
||||
currentDepth,
|
||||
hook,
|
||||
payload,
|
||||
showHiddenFields,
|
||||
originalDoc,
|
||||
}: Arguments): Promise<void> => {
|
||||
const resultingData = data;
|
||||
|
||||
let accessOperation;
|
||||
|
||||
if (hook === 'afterRead') {
|
||||
accessOperation = 'read';
|
||||
} else if (hook === 'beforeValidate') {
|
||||
if (operation === 'update') accessOperation = 'update';
|
||||
if (operation === 'create') accessOperation = 'create';
|
||||
}
|
||||
|
||||
if (field.access && field.access[accessOperation]) {
|
||||
const result = overrideAccess ? true : await field.access[accessOperation]({ req, id, siblingData: data, data: fullData, doc: originalDoc });
|
||||
|
||||
if (!result) {
|
||||
delete resultingData[field.name];
|
||||
}
|
||||
}
|
||||
|
||||
if ((field.type === 'relationship' || field.type === 'upload') && hook === 'afterRead') {
|
||||
relationshipPopulations.push(relationshipPopulationPromise({
|
||||
showHiddenFields,
|
||||
data,
|
||||
field,
|
||||
depth,
|
||||
currentDepth,
|
||||
req,
|
||||
overrideAccess,
|
||||
payload,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
export default accessPromise;
|
||||
@@ -22,7 +22,7 @@ export type FieldHook<T extends TypeWithID = any, P = any, S = any> = (args: Fie
|
||||
|
||||
export type FieldAccess<T extends TypeWithID = any, P = any> = (args: {
|
||||
req: PayloadRequest
|
||||
id?: string
|
||||
id?: string | number
|
||||
data?: Partial<T>
|
||||
siblingData?: Partial<P>
|
||||
doc?: T
|
||||
@@ -228,6 +228,10 @@ export type ValueWithRelation = {
|
||||
value: string | number
|
||||
}
|
||||
|
||||
export function valueIsValueWithRelation(value: unknown): value is ValueWithRelation {
|
||||
return typeof value === 'object' && 'relationTo' in value && 'value' in value;
|
||||
}
|
||||
|
||||
export type RelationshipValue = (string | number)
|
||||
| (string | number)[]
|
||||
| ValueWithRelation
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { PayloadRequest } from '../express/types';
|
||||
import { Operation } from '../types';
|
||||
import { HookName, FieldAffectingData, FieldHook } from './config/types';
|
||||
|
||||
type Arguments = {
|
||||
data: Record<string, unknown>
|
||||
field: FieldAffectingData
|
||||
hook: HookName
|
||||
req: PayloadRequest
|
||||
operation: Operation
|
||||
fullOriginalDoc: Record<string, unknown>
|
||||
fullData: Record<string, unknown>
|
||||
flattenLocales: boolean
|
||||
isVersion: boolean
|
||||
}
|
||||
|
||||
type ExecuteHookArguments = {
|
||||
currentHook: FieldHook
|
||||
value: unknown
|
||||
} & Arguments;
|
||||
|
||||
const executeHook = async ({
|
||||
currentHook,
|
||||
fullOriginalDoc,
|
||||
fullData,
|
||||
data,
|
||||
operation,
|
||||
req,
|
||||
value,
|
||||
}: ExecuteHookArguments) => {
|
||||
let hookedValue = await currentHook({
|
||||
value,
|
||||
originalDoc: fullOriginalDoc,
|
||||
data: fullData,
|
||||
siblingData: data,
|
||||
operation,
|
||||
req,
|
||||
});
|
||||
|
||||
if (typeof hookedValue === 'undefined') {
|
||||
hookedValue = value;
|
||||
}
|
||||
|
||||
return hookedValue;
|
||||
};
|
||||
|
||||
const hookPromise = async (args: Arguments): Promise<void> => {
|
||||
const {
|
||||
field,
|
||||
hook,
|
||||
req,
|
||||
flattenLocales,
|
||||
data,
|
||||
} = args;
|
||||
|
||||
if (field.hooks && field.hooks[hook]) {
|
||||
await field.hooks[hook].reduce(async (priorHook, currentHook) => {
|
||||
await priorHook;
|
||||
|
||||
const shouldRunHookOnAllLocales = hook === 'afterRead'
|
||||
&& field.localized
|
||||
&& (req.locale === 'all' || !flattenLocales)
|
||||
&& typeof data[field.name] === 'object';
|
||||
|
||||
if (shouldRunHookOnAllLocales) {
|
||||
const hookPromises = Object.entries(data[field.name]).map(([locale, value]) => (async () => {
|
||||
const hookedValue = await executeHook({
|
||||
...args,
|
||||
currentHook,
|
||||
value,
|
||||
});
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
data[field.name][locale] = hookedValue;
|
||||
}
|
||||
})());
|
||||
|
||||
await Promise.all(hookPromises);
|
||||
} else {
|
||||
const hookedValue = await executeHook({
|
||||
...args,
|
||||
value: data[field.name],
|
||||
currentHook,
|
||||
});
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
data[field.name] = hookedValue;
|
||||
}
|
||||
}
|
||||
}, Promise.resolve());
|
||||
}
|
||||
};
|
||||
|
||||
export default hookPromise;
|
||||
40
src/fields/hooks/afterChange/index.ts
Normal file
40
src/fields/hooks/afterChange/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { SanitizedCollectionConfig } from '../../../collections/config/types';
|
||||
import { SanitizedGlobalConfig } from '../../../globals/config/types';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { traverseFields } from './traverseFields';
|
||||
import deepCopyObject from '../../../utilities/deepCopyObject';
|
||||
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
|
||||
operation: 'create' | 'update'
|
||||
req: PayloadRequest
|
||||
}
|
||||
|
||||
export const afterChange = async ({
|
||||
data,
|
||||
doc: incomingDoc,
|
||||
entityConfig,
|
||||
operation,
|
||||
req,
|
||||
}: Args): Promise<Record<string, unknown>> => {
|
||||
const promises = [];
|
||||
|
||||
const doc = deepCopyObject(incomingDoc);
|
||||
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
fields: entityConfig.fields,
|
||||
operation,
|
||||
promises,
|
||||
req,
|
||||
siblingDoc: doc,
|
||||
siblingData: data,
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return doc;
|
||||
};
|
||||
133
src/fields/hooks/afterChange/promise.ts
Normal file
133
src/fields/hooks/afterChange/promise.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { Field, fieldAffectsData } from '../../config/types';
|
||||
import { traverseFields } from './traverseFields';
|
||||
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
field: Field
|
||||
operation: 'create' | 'update'
|
||||
promises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Execute field hooks
|
||||
|
||||
export const promise = async ({
|
||||
data,
|
||||
doc,
|
||||
field,
|
||||
operation,
|
||||
promises,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args): Promise<void> => {
|
||||
if (fieldAffectsData(field)) {
|
||||
// Execute hooks
|
||||
if (field.hooks?.afterChange) {
|
||||
await field.hooks.afterChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook;
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
value: siblingData[field.name],
|
||||
originalDoc: doc,
|
||||
data,
|
||||
siblingData,
|
||||
operation,
|
||||
req,
|
||||
});
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name] = hookedValue;
|
||||
}
|
||||
}, Promise.resolve());
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse subfields
|
||||
switch (field.type) {
|
||||
case 'group': {
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
operation,
|
||||
promises,
|
||||
req,
|
||||
siblingData: siblingData[field.name] as Record<string, unknown> || {},
|
||||
siblingDoc: siblingDoc[field.name] as Record<string, unknown>,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const rows = siblingDoc[field.name];
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
operation,
|
||||
promises,
|
||||
req,
|
||||
siblingData: siblingData[field.name]?.[i] || {},
|
||||
siblingDoc: { ...row } || {},
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingDoc[field.name];
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find((blockType) => blockType.slug === row.blockType);
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
fields: block.fields,
|
||||
operation,
|
||||
promises,
|
||||
req,
|
||||
siblingData: siblingData[field.name]?.[i] || {},
|
||||
siblingDoc: { ...row } || {},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'row': {
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
operation,
|
||||
promises,
|
||||
req,
|
||||
siblingData: siblingData || {},
|
||||
siblingDoc: { ...siblingDoc },
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
38
src/fields/hooks/afterChange/traverseFields.ts
Normal file
38
src/fields/hooks/afterChange/traverseFields.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Field } from '../../config/types';
|
||||
import { promise } from './promise';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
fields: Field[]
|
||||
operation: 'create' | 'update'
|
||||
promises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
data,
|
||||
doc,
|
||||
fields,
|
||||
operation,
|
||||
promises,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args): void => {
|
||||
fields.forEach((field) => {
|
||||
promises.push(promise({
|
||||
data,
|
||||
doc,
|
||||
field,
|
||||
operation,
|
||||
promises,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}));
|
||||
});
|
||||
};
|
||||
62
src/fields/hooks/afterRead/index.ts
Normal file
62
src/fields/hooks/afterRead/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { SanitizedCollectionConfig } from '../../../collections/config/types';
|
||||
import { SanitizedGlobalConfig } from '../../../globals/config/types';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { traverseFields } from './traverseFields';
|
||||
import deepCopyObject from '../../../utilities/deepCopyObject';
|
||||
|
||||
type Args = {
|
||||
currentDepth?: number
|
||||
depth: number
|
||||
doc: Record<string, unknown>
|
||||
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
|
||||
flattenLocales?: boolean
|
||||
req: PayloadRequest
|
||||
overrideAccess: boolean
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
export async function afterRead<T = any>(args: Args): Promise<T> {
|
||||
const {
|
||||
currentDepth: incomingCurrentDepth,
|
||||
depth: incomingDepth,
|
||||
doc: incomingDoc,
|
||||
entityConfig,
|
||||
flattenLocales = true,
|
||||
req,
|
||||
overrideAccess,
|
||||
showHiddenFields,
|
||||
} = args;
|
||||
|
||||
const doc = deepCopyObject(incomingDoc);
|
||||
const fieldPromises = [];
|
||||
const populationPromises = [];
|
||||
|
||||
let depth = 0;
|
||||
|
||||
if (req.payloadAPI === 'REST' || req.payloadAPI === 'local') {
|
||||
depth = (incomingDepth || incomingDepth === 0) ? parseInt(String(incomingDepth), 10) : req.payload.config.defaultDepth;
|
||||
|
||||
if (depth > req.payload.config.maxDepth) depth = req.payload.config.maxDepth;
|
||||
}
|
||||
|
||||
const currentDepth = incomingCurrentDepth || 1;
|
||||
|
||||
traverseFields({
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
fields: entityConfig.fields,
|
||||
fieldPromises,
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
siblingDoc: doc,
|
||||
showHiddenFields,
|
||||
});
|
||||
|
||||
await Promise.all(fieldPromises);
|
||||
await Promise.all(populationPromises);
|
||||
|
||||
return doc;
|
||||
}
|
||||
265
src/fields/hooks/afterRead/promise.ts
Normal file
265
src/fields/hooks/afterRead/promise.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { Field, fieldAffectsData } from '../../config/types';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { traverseFields } from './traverseFields';
|
||||
import richTextRelationshipPromise from '../../richText/relationshipPromise';
|
||||
import relationshipPopulationPromise from './relationshipPopulationPromise';
|
||||
|
||||
type Args = {
|
||||
currentDepth: number
|
||||
depth: number
|
||||
doc: Record<string, unknown>
|
||||
field: Field
|
||||
fieldPromises: Promise<void>[]
|
||||
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,
|
||||
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 hasLocalizedValue = flattenLocales
|
||||
&& fieldAffectsData(field)
|
||||
&& (typeof siblingDoc[field.name] === 'object' && siblingDoc[field.name] !== null)
|
||||
&& field.name
|
||||
&& field.localized
|
||||
&& req.locale !== 'all';
|
||||
|
||||
if (hasLocalizedValue) {
|
||||
let localizedValue = siblingDoc[field.name][req.locale];
|
||||
if (typeof localizedValue === 'undefined' && req.fallbackLocale) localizedValue = siblingDoc[field.name][req.fallbackLocale];
|
||||
if (typeof localizedValue === 'undefined' && field.type === 'group') localizedValue = {};
|
||||
if (typeof localizedValue === 'undefined') localizedValue = null;
|
||||
siblingDoc[field.name] = localizedValue;
|
||||
}
|
||||
|
||||
// Sanitize outgoing data
|
||||
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 'richText': {
|
||||
if (((field.admin?.elements?.includes('relationship') || field.admin?.elements?.includes('upload')) || !field?.admin?.elements)) {
|
||||
populationPromises.push(richTextRelationshipPromise({
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
overrideAccess,
|
||||
req,
|
||||
siblingDoc,
|
||||
showHiddenFields,
|
||||
}));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
const pointDoc = siblingDoc[field.name] as any;
|
||||
if (Array.isArray(pointDoc?.coordinates) && pointDoc.coordinates.length === 2) {
|
||||
siblingDoc[field.name] = pointDoc.coordinates;
|
||||
}
|
||||
|
||||
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[field.name],
|
||||
operation: 'read',
|
||||
req,
|
||||
});
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingDoc[field.name][locale] = hookedValue;
|
||||
}
|
||||
})());
|
||||
|
||||
await Promise.all(hookPromises);
|
||||
} else {
|
||||
const hookedValue = await currentHook({
|
||||
value: siblingDoc[field.name],
|
||||
originalDoc: doc,
|
||||
data: doc,
|
||||
siblingData: siblingDoc[field.name],
|
||||
operation: 'read',
|
||||
req,
|
||||
});
|
||||
|
||||
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,
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
siblingDoc: groupDoc,
|
||||
showHiddenFields,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const rows = siblingDoc[field.name];
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
fieldPromises,
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
siblingDoc: row || {},
|
||||
showHiddenFields,
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingDoc[field.name];
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find((blockType) => blockType.slug === row.blockType);
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
fields: block.fields,
|
||||
fieldPromises,
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
siblingDoc: row || {},
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'row': {
|
||||
traverseFields({
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
fieldPromises,
|
||||
fields: field.fields,
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
siblingDoc,
|
||||
showHiddenFields,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { PayloadRequest } from '../express/types';
|
||||
import { RelationshipField, fieldSupportsMany, fieldHasMaxDepth, UploadField } from './config/types';
|
||||
import { Payload } from '..';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { RelationshipField, fieldSupportsMany, fieldHasMaxDepth, UploadField } from '../../config/types';
|
||||
|
||||
type PopulateArgs = {
|
||||
depth: number
|
||||
@@ -11,7 +10,6 @@ type PopulateArgs = {
|
||||
data: Record<string, unknown>
|
||||
field: RelationshipField | UploadField
|
||||
index?: number
|
||||
payload: Payload
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
@@ -24,13 +22,12 @@ const populate = async ({
|
||||
data,
|
||||
field,
|
||||
index,
|
||||
payload,
|
||||
showHiddenFields,
|
||||
}: PopulateArgs) => {
|
||||
const dataToUpdate = dataReference;
|
||||
|
||||
const relation = Array.isArray(field.relationTo) ? (data.relationTo as string) : field.relationTo;
|
||||
const relatedCollection = payload.collections[relation];
|
||||
const relatedCollection = req.payload.collections[relation];
|
||||
|
||||
if (relatedCollection) {
|
||||
let idString = Array.isArray(field.relationTo) ? data.value : data;
|
||||
@@ -42,7 +39,7 @@ const populate = async ({
|
||||
let populatedRelationship;
|
||||
|
||||
if (depth && currentDepth <= depth) {
|
||||
populatedRelationship = await payload.findByID({
|
||||
populatedRelationship = await req.payload.findByID({
|
||||
req,
|
||||
collection: relatedCollection.config.slug,
|
||||
id: idString as string,
|
||||
@@ -72,33 +69,31 @@ const populate = async ({
|
||||
};
|
||||
|
||||
type PromiseArgs = {
|
||||
data: Record<string, any>
|
||||
siblingDoc: Record<string, any>
|
||||
field: RelationshipField | UploadField
|
||||
depth: number
|
||||
currentDepth: number
|
||||
req: PayloadRequest
|
||||
overrideAccess: boolean
|
||||
payload: Payload
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
const relationshipPopulationPromise = ({
|
||||
data,
|
||||
const relationshipPopulationPromise = async ({
|
||||
siblingDoc,
|
||||
field,
|
||||
depth,
|
||||
currentDepth,
|
||||
req,
|
||||
overrideAccess,
|
||||
payload,
|
||||
showHiddenFields,
|
||||
}: PromiseArgs) => async (): Promise<void> => {
|
||||
const resultingData = data;
|
||||
}: PromiseArgs): Promise<void> => {
|
||||
const resultingDoc = siblingDoc;
|
||||
const populateDepth = fieldHasMaxDepth(field) && field.maxDepth < depth ? field.maxDepth : depth;
|
||||
|
||||
if (fieldSupportsMany(field) && field.hasMany && Array.isArray(data[field.name])) {
|
||||
if (fieldSupportsMany(field) && field.hasMany && Array.isArray(siblingDoc[field.name])) {
|
||||
const rowPromises = [];
|
||||
|
||||
data[field.name].forEach((relatedDoc, index) => {
|
||||
siblingDoc[field.name].forEach((relatedDoc, index) => {
|
||||
const rowPromise = async () => {
|
||||
if (relatedDoc) {
|
||||
await populate({
|
||||
@@ -107,10 +102,9 @@ const relationshipPopulationPromise = ({
|
||||
req,
|
||||
overrideAccess,
|
||||
data: relatedDoc,
|
||||
dataReference: resultingData,
|
||||
dataReference: resultingDoc,
|
||||
field,
|
||||
index,
|
||||
payload,
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
@@ -120,16 +114,15 @@ const relationshipPopulationPromise = ({
|
||||
});
|
||||
|
||||
await Promise.all(rowPromises);
|
||||
} else if (data[field.name]) {
|
||||
} else if (siblingDoc[field.name]) {
|
||||
await populate({
|
||||
depth: populateDepth,
|
||||
currentDepth,
|
||||
req,
|
||||
overrideAccess,
|
||||
dataReference: resultingData,
|
||||
data: data[field.name],
|
||||
dataReference: resultingDoc,
|
||||
data: siblingDoc[field.name],
|
||||
field,
|
||||
payload,
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
47
src/fields/hooks/afterRead/traverseFields.ts
Normal file
47
src/fields/hooks/afterRead/traverseFields.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Field } from '../../config/types';
|
||||
import { promise } from './promise';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
|
||||
type Args = {
|
||||
currentDepth: number
|
||||
depth: number
|
||||
doc: Record<string, unknown>
|
||||
fieldPromises: Promise<void>[]
|
||||
fields: Field[]
|
||||
flattenLocales: boolean
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
overrideAccess: boolean
|
||||
siblingDoc: Record<string, unknown>
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
fieldPromises,
|
||||
fields,
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
siblingDoc,
|
||||
showHiddenFields,
|
||||
}: Args): void => {
|
||||
fields.forEach((field) => {
|
||||
fieldPromises.push(promise({
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
field,
|
||||
fieldPromises,
|
||||
flattenLocales,
|
||||
overrideAccess,
|
||||
populationPromises,
|
||||
req,
|
||||
siblingDoc,
|
||||
showHiddenFields,
|
||||
}));
|
||||
});
|
||||
};
|
||||
62
src/fields/hooks/beforeChange/index.ts
Normal file
62
src/fields/hooks/beforeChange/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { SanitizedCollectionConfig } from '../../../collections/config/types';
|
||||
import { SanitizedGlobalConfig } from '../../../globals/config/types';
|
||||
import { Operation } from '../../../types';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { traverseFields } from './traverseFields';
|
||||
import { ValidationError } from '../../../errors';
|
||||
import deepCopyObject from '../../../utilities/deepCopyObject';
|
||||
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
docWithLocales: Record<string, unknown>
|
||||
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
|
||||
id?: string | number
|
||||
operation: Operation
|
||||
req: PayloadRequest
|
||||
skipValidation?: boolean
|
||||
}
|
||||
|
||||
export const beforeChange = async ({
|
||||
data: incomingData,
|
||||
doc,
|
||||
docWithLocales,
|
||||
entityConfig,
|
||||
id,
|
||||
operation,
|
||||
req,
|
||||
skipValidation,
|
||||
}: Args): Promise<Record<string, unknown>> => {
|
||||
const data = deepCopyObject(incomingData);
|
||||
const promises = [];
|
||||
const mergeLocaleActions = [];
|
||||
const errors: { message: string, field: string }[] = [];
|
||||
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
id,
|
||||
operation,
|
||||
path: '',
|
||||
mergeLocaleActions,
|
||||
promises,
|
||||
req,
|
||||
siblingData: data,
|
||||
siblingDoc: doc,
|
||||
siblingDocWithLocales: docWithLocales,
|
||||
fields: entityConfig.fields,
|
||||
skipValidation,
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new ValidationError(errors);
|
||||
}
|
||||
|
||||
mergeLocaleActions.forEach((action) => action());
|
||||
|
||||
return data;
|
||||
};
|
||||
285
src/fields/hooks/beforeChange/promise.ts
Normal file
285
src/fields/hooks/beforeChange/promise.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import merge from 'deepmerge';
|
||||
import { Field, fieldAffectsData } from '../../config/types';
|
||||
import { Operation } from '../../../types';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import getValueWithDefault from '../../getDefaultValue';
|
||||
import { traverseFields } from './traverseFields';
|
||||
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
docWithLocales: Record<string, unknown>
|
||||
errors: { message: string, field: string }[]
|
||||
field: Field
|
||||
id?: string | number
|
||||
mergeLocaleActions: (() => void)[]
|
||||
operation: Operation
|
||||
path: string
|
||||
promises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
siblingDocWithLocales?: Record<string, unknown>
|
||||
skipValidation: boolean
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Run condition
|
||||
// - Merge original document data into incoming data
|
||||
// - Compute default values for undefined fields
|
||||
// - Execute field hooks
|
||||
// - Validate data
|
||||
// - Transform data for storage
|
||||
// - Unflatten locales
|
||||
|
||||
export const promise = async ({
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
field,
|
||||
id,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path,
|
||||
promises,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
skipValidation,
|
||||
}: Args): Promise<void> => {
|
||||
const passesCondition = (field.admin?.condition) ? field.admin.condition(data, siblingData) : true;
|
||||
const skipValidationFromHere = skipValidation || !passesCondition;
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
if (typeof siblingData[field.name] === 'undefined') {
|
||||
// If no incoming data, but existing document data is found, merge it in
|
||||
if (typeof siblingDoc[field.name] !== 'undefined') {
|
||||
if (field.localized && typeof siblingDoc[field.name] === 'object') {
|
||||
siblingData[field.name] = siblingDoc[field.name][req.locale];
|
||||
} else {
|
||||
siblingData[field.name] = siblingDoc[field.name];
|
||||
}
|
||||
|
||||
// Otherwise compute default value
|
||||
} else if (typeof field.defaultValue !== 'undefined') {
|
||||
siblingData[field.name] = await getValueWithDefault({
|
||||
value: siblingData[field.name],
|
||||
defaultValue: field.defaultValue,
|
||||
locale: req.locale,
|
||||
user: req.user,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Execute hooks
|
||||
if (field.hooks?.beforeChange) {
|
||||
await field.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook;
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
value: siblingData[field.name],
|
||||
originalDoc: doc,
|
||||
data,
|
||||
siblingData,
|
||||
operation,
|
||||
req,
|
||||
});
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue;
|
||||
}
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
// Validate
|
||||
if (!skipValidationFromHere && field.validate) {
|
||||
let valueToValidate;
|
||||
|
||||
if (['array', 'blocks'].includes(field.type)) {
|
||||
const rows = siblingData[field.name];
|
||||
valueToValidate = Array.isArray(rows) ? rows.length : 0;
|
||||
} else {
|
||||
valueToValidate = siblingData[field.name];
|
||||
}
|
||||
|
||||
const validationResult = await field.validate(valueToValidate, {
|
||||
...field,
|
||||
data: merge(doc, data),
|
||||
siblingData: merge(siblingDoc, siblingData),
|
||||
id,
|
||||
operation,
|
||||
user: req.user,
|
||||
payload: req.payload,
|
||||
});
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
errors.push({
|
||||
message: validationResult,
|
||||
field: `${path}${field.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Push merge locale action if applicable
|
||||
if (field.localized) {
|
||||
mergeLocaleActions.push(() => {
|
||||
const localeData = req.payload.config.localization.locales.reduce((locales, localeID) => {
|
||||
let valueToSet = siblingData[field.name];
|
||||
|
||||
if (localeID !== req.locale) {
|
||||
valueToSet = siblingDocWithLocales?.[field.name]?.[localeID];
|
||||
}
|
||||
|
||||
if (typeof valueToSet !== 'undefined') {
|
||||
return {
|
||||
...locales,
|
||||
[localeID]: valueToSet,
|
||||
};
|
||||
}
|
||||
|
||||
return locales;
|
||||
}, {});
|
||||
|
||||
// If there are locales with data, set the data
|
||||
if (Object.keys(localeData).length > 0) {
|
||||
siblingData[field.name] = localeData;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'point': {
|
||||
// Transform point data for storage
|
||||
if (Array.isArray(siblingData[field.name]) && siblingData[field.name][0] !== null && siblingData[field.name][1] !== null) {
|
||||
siblingData[field.name] = {
|
||||
type: 'Point',
|
||||
coordinates: [
|
||||
parseFloat(siblingData[field.name][0]),
|
||||
parseFloat(siblingData[field.name][1]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
let groupData = siblingData[field.name] as Record<string, unknown>;
|
||||
let groupDoc = siblingDoc[field.name] as Record<string, unknown>;
|
||||
let groupDocWithLocales = siblingDocWithLocales[field.name] as Record<string, unknown>;
|
||||
|
||||
if (typeof siblingData[field.name] !== 'object') groupData = {};
|
||||
if (typeof siblingDoc[field.name] !== 'object') groupDoc = {};
|
||||
if (typeof siblingDocWithLocales[field.name] !== 'object') groupDocWithLocales = {};
|
||||
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.fields,
|
||||
id,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: `${path}${field.name}.`,
|
||||
promises,
|
||||
req,
|
||||
siblingData: groupData,
|
||||
siblingDoc: groupDoc,
|
||||
siblingDocWithLocales: groupDocWithLocales,
|
||||
skipValidation: skipValidationFromHere,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const rows = siblingData[field.name];
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.fields,
|
||||
id,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: `${path}${field.name}.${i}.`,
|
||||
promises,
|
||||
req,
|
||||
siblingData: row,
|
||||
siblingDoc: siblingDoc[field.name]?.[i] || {},
|
||||
siblingDocWithLocales: siblingDocWithLocales[field.name]?.[i] || {},
|
||||
skipValidation: skipValidationFromHere,
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingData[field.name];
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find((blockType) => blockType.slug === row.blockType);
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: block.fields,
|
||||
id,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path: `${path}${field.name}.${i}.`,
|
||||
promises,
|
||||
req,
|
||||
siblingData: row,
|
||||
siblingDoc: siblingDoc[field.name]?.[i] || {},
|
||||
siblingDocWithLocales: siblingDocWithLocales[field.name]?.[i] || {},
|
||||
skipValidation: skipValidationFromHere,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'row': {
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields: field.fields,
|
||||
id,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path,
|
||||
promises,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
skipValidation: skipValidationFromHere,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
60
src/fields/hooks/beforeChange/traverseFields.ts
Normal file
60
src/fields/hooks/beforeChange/traverseFields.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Field } from '../../config/types';
|
||||
import { promise } from './promise';
|
||||
import { Operation } from '../../../types';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
docWithLocales: Record<string, unknown>
|
||||
errors: { message: string, field: string }[]
|
||||
fields: Field[]
|
||||
id?: string | number
|
||||
mergeLocaleActions: (() => void)[]
|
||||
operation: Operation
|
||||
path: string
|
||||
promises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
siblingDocWithLocales: Record<string, unknown>
|
||||
skipValidation?: boolean
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
fields,
|
||||
id,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path,
|
||||
promises,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
skipValidation,
|
||||
}: Args): void => {
|
||||
fields.forEach((field) => {
|
||||
promises.push(promise({
|
||||
data,
|
||||
doc,
|
||||
docWithLocales,
|
||||
errors,
|
||||
field,
|
||||
id,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
path,
|
||||
promises,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
skipValidation,
|
||||
}));
|
||||
});
|
||||
};
|
||||
45
src/fields/hooks/beforeValidate/index.ts
Normal file
45
src/fields/hooks/beforeValidate/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { SanitizedCollectionConfig } from '../../../collections/config/types';
|
||||
import { SanitizedGlobalConfig } from '../../../globals/config/types';
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { traverseFields } from './traverseFields';
|
||||
import deepCopyObject from '../../../utilities/deepCopyObject';
|
||||
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig
|
||||
id?: string | number
|
||||
operation: 'create' | 'update'
|
||||
overrideAccess: boolean
|
||||
req: PayloadRequest
|
||||
}
|
||||
|
||||
export const beforeValidate = async ({
|
||||
data: incomingData,
|
||||
doc,
|
||||
entityConfig,
|
||||
id,
|
||||
operation,
|
||||
overrideAccess,
|
||||
req,
|
||||
}: Args): Promise<Record<string, unknown>> => {
|
||||
const promises = [];
|
||||
const data = deepCopyObject(incomingData);
|
||||
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
fields: entityConfig.fields,
|
||||
id,
|
||||
operation,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
siblingData: data,
|
||||
siblingDoc: doc,
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return data;
|
||||
};
|
||||
273
src/fields/hooks/beforeValidate/promise.ts
Normal file
273
src/fields/hooks/beforeValidate/promise.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { Field, fieldAffectsData, valueIsValueWithRelation } from '../../config/types';
|
||||
import { traverseFields } from './traverseFields';
|
||||
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
field: Field
|
||||
id?: string | number
|
||||
operation: 'create' | 'update'
|
||||
overrideAccess: boolean
|
||||
promises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
// - Sanitize incoming data
|
||||
// - Execute field hooks
|
||||
// - Execute field access control
|
||||
|
||||
export const promise = async ({
|
||||
data,
|
||||
doc,
|
||||
field,
|
||||
id,
|
||||
operation,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args): Promise<void> => {
|
||||
if (fieldAffectsData(field)) {
|
||||
if (field.name === 'id') {
|
||||
if (field.type === 'number' && typeof siblingData[field.name] === 'string') {
|
||||
const value = siblingData[field.name] as string;
|
||||
|
||||
siblingData[field.name] = parseFloat(value);
|
||||
}
|
||||
|
||||
if (field.type === 'text' && typeof siblingData[field.name]?.toString === 'function' && typeof siblingData[field.name] !== 'string') {
|
||||
siblingData[field.name] = siblingData[field.name].toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize incoming data
|
||||
switch (field.type) {
|
||||
case 'number': {
|
||||
if (typeof siblingData[field.name] === 'string') {
|
||||
const value = siblingData[field.name] as string;
|
||||
const trimmed = value.trim();
|
||||
siblingData[field.name] = (trimmed.length === 0) ? null : parseFloat(trimmed);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'checkbox': {
|
||||
if (siblingData[field.name] === 'true') siblingData[field.name] = true;
|
||||
if (siblingData[field.name] === 'false') siblingData[field.name] = false;
|
||||
if (siblingData[field.name] === '') siblingData[field.name] = false;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (typeof siblingData[field.name] === 'string') {
|
||||
try {
|
||||
const richTextJSON = JSON.parse(siblingData[field.name] as string);
|
||||
siblingData[field.name] = richTextJSON;
|
||||
} catch {
|
||||
// Disregard this data as it is not valid.
|
||||
// Will be reported to user by field validation
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
if (siblingData[field.name] === '' || siblingData[field.name] === 'none' || siblingData[field.name] === 'null' || siblingData[field.name] === null) {
|
||||
if (field.type === 'relationship' && field.hasMany === true) {
|
||||
siblingData[field.name] = [];
|
||||
} else {
|
||||
siblingData[field.name] = null;
|
||||
}
|
||||
}
|
||||
|
||||
const value = siblingData[field.name];
|
||||
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((relatedDoc: { value: unknown, relationTo: string }, i) => {
|
||||
const relatedCollection = req.payload.config.collections.find((collection) => collection.slug === relatedDoc.relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldAffectsData(collectionField) && collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name][i] = { ...relatedDoc, value: parseFloat(relatedDoc.value as string) };
|
||||
}
|
||||
});
|
||||
}
|
||||
if (field.type === 'relationship' && field.hasMany !== true && valueIsValueWithRelation(value)) {
|
||||
const relatedCollection = req.payload.config.collections.find((collection) => collection.slug === value.relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldAffectsData(collectionField) && collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name] = { ...value, value: parseFloat(value.value as string) };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((relatedDoc: unknown, i) => {
|
||||
const relatedCollection = req.payload.config.collections.find((collection) => collection.slug === field.relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldAffectsData(collectionField) && collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name][i] = parseFloat(relatedDoc as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (field.type === 'relationship' && field.hasMany !== true && value) {
|
||||
const relatedCollection = req.payload.config.collections.find((collection) => collection.slug === field.relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldAffectsData(collectionField) && collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
siblingData[field.name] = parseFloat(value as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'array':
|
||||
case 'blocks': {
|
||||
// Handle cases of arrays being intentionally set to 0
|
||||
if (siblingData[field.name] === '0' || siblingData[field.name] === 0 || siblingData[field.name] === null) {
|
||||
siblingData[field.name] = [];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute hooks
|
||||
if (field.hooks?.beforeValidate) {
|
||||
await field.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
|
||||
await priorHook;
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
value: siblingData[field.name],
|
||||
originalDoc: doc,
|
||||
data,
|
||||
siblingData,
|
||||
operation,
|
||||
req,
|
||||
});
|
||||
|
||||
if (hookedValue !== undefined) {
|
||||
siblingData[field.name] = hookedValue;
|
||||
}
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
// Execute access control
|
||||
if (field.access && field.access[operation]) {
|
||||
const result = overrideAccess ? true : await field.access[operation]({ req, id, siblingData, data, doc });
|
||||
|
||||
if (!result) {
|
||||
delete siblingData[field.name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse subfields
|
||||
switch (field.type) {
|
||||
case 'group': {
|
||||
let groupData = siblingData[field.name] as Record<string, unknown>;
|
||||
let groupDoc = siblingDoc[field.name] as Record<string, unknown>;
|
||||
|
||||
if (typeof siblingData[field.name] !== 'object') groupData = {};
|
||||
if (typeof siblingDoc[field.name] !== 'object') groupDoc = {};
|
||||
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
id,
|
||||
operation,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
siblingData: groupData,
|
||||
siblingDoc: groupDoc,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const rows = siblingData[field.name];
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
id,
|
||||
operation,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
siblingData: row,
|
||||
siblingDoc: siblingDoc[field.name]?.[i] || {},
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingData[field.name];
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find((blockType) => blockType.slug === row.blockType);
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
fields: block.fields,
|
||||
id,
|
||||
operation,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
siblingData: row,
|
||||
siblingDoc: siblingDoc[field.name]?.[i] || {},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'row': {
|
||||
traverseFields({
|
||||
data,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
id,
|
||||
operation,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
44
src/fields/hooks/beforeValidate/traverseFields.ts
Normal file
44
src/fields/hooks/beforeValidate/traverseFields.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { PayloadRequest } from '../../../express/types';
|
||||
import { Field } from '../../config/types';
|
||||
import { promise } from './promise';
|
||||
|
||||
type Args = {
|
||||
data: Record<string, unknown>
|
||||
doc: Record<string, unknown>
|
||||
fields: Field[]
|
||||
id?: string | number
|
||||
operation: 'create' | 'update'
|
||||
overrideAccess: boolean
|
||||
promises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
data,
|
||||
doc,
|
||||
fields,
|
||||
id,
|
||||
operation,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args): void => {
|
||||
fields.forEach((field) => {
|
||||
promises.push(promise({
|
||||
data,
|
||||
doc,
|
||||
field,
|
||||
id,
|
||||
operation,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}));
|
||||
});
|
||||
};
|
||||
@@ -1,144 +0,0 @@
|
||||
import { Payload } from '..';
|
||||
import { ValidationError } from '../errors';
|
||||
import sanitizeFallbackLocale from '../localization/sanitizeFallbackLocale';
|
||||
import traverseFields from './traverseFields';
|
||||
import { SanitizedCollectionConfig } from '../collections/config/types';
|
||||
import { SanitizedGlobalConfig } from '../globals/config/types';
|
||||
import { Operation } from '../types';
|
||||
import { PayloadRequest } from '../express/types';
|
||||
import { HookName } from './config/types';
|
||||
import deepCopyObject from '../utilities/deepCopyObject';
|
||||
|
||||
type Arguments = {
|
||||
data: Record<string, unknown>
|
||||
operation: Operation
|
||||
hook?: HookName
|
||||
req: PayloadRequest
|
||||
overrideAccess: boolean
|
||||
flattenLocales?: boolean
|
||||
unflattenLocales?: boolean
|
||||
originalDoc?: Record<string, unknown>
|
||||
docWithLocales?: Record<string, unknown>
|
||||
id?: string | number
|
||||
showHiddenFields?: boolean
|
||||
depth?: number
|
||||
currentDepth?: number
|
||||
isVersion?: boolean
|
||||
skipValidation?: boolean
|
||||
}
|
||||
|
||||
export default async function performFieldOperations(this: Payload, entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig, args: Arguments): Promise<any> {
|
||||
const {
|
||||
data,
|
||||
originalDoc: fullOriginalDoc,
|
||||
docWithLocales,
|
||||
operation,
|
||||
hook,
|
||||
req,
|
||||
id,
|
||||
req: {
|
||||
payloadAPI,
|
||||
locale,
|
||||
},
|
||||
overrideAccess,
|
||||
flattenLocales,
|
||||
unflattenLocales = false,
|
||||
showHiddenFields = false,
|
||||
isVersion = false,
|
||||
skipValidation = false,
|
||||
} = args;
|
||||
|
||||
const fullData = deepCopyObject(data);
|
||||
|
||||
const fallbackLocale = sanitizeFallbackLocale(req.fallbackLocale);
|
||||
|
||||
let depth = 0;
|
||||
|
||||
if (payloadAPI === 'REST' || payloadAPI === 'local') {
|
||||
depth = (args.depth || args.depth === 0) ? parseInt(String(args.depth), 10) : this.config.defaultDepth;
|
||||
|
||||
if (depth > this.config.maxDepth) depth = this.config.maxDepth;
|
||||
}
|
||||
|
||||
const currentDepth = args.currentDepth || 1;
|
||||
|
||||
// Maintain a top-level list of promises
|
||||
// so that all async field access / validations / hooks
|
||||
// can run in parallel
|
||||
const valuePromises = [];
|
||||
const validationPromises = [];
|
||||
const accessPromises = [];
|
||||
const relationshipPopulations = [];
|
||||
const hookPromises = [];
|
||||
const unflattenLocaleActions = [];
|
||||
const transformActions = [];
|
||||
const errors: { message: string, field: string }[] = [];
|
||||
|
||||
// //////////////////////////////////////////
|
||||
// Entry point for field validation
|
||||
// //////////////////////////////////////////
|
||||
|
||||
traverseFields({
|
||||
fields: entityConfig.fields,
|
||||
data: fullData,
|
||||
originalDoc: fullOriginalDoc,
|
||||
path: '',
|
||||
flattenLocales,
|
||||
locale,
|
||||
fallbackLocale,
|
||||
accessPromises,
|
||||
operation,
|
||||
overrideAccess,
|
||||
req,
|
||||
id,
|
||||
relationshipPopulations,
|
||||
depth,
|
||||
currentDepth,
|
||||
hook,
|
||||
hookPromises,
|
||||
fullOriginalDoc,
|
||||
fullData,
|
||||
valuePromises,
|
||||
validationPromises,
|
||||
errors,
|
||||
payload: this,
|
||||
showHiddenFields,
|
||||
unflattenLocales,
|
||||
unflattenLocaleActions,
|
||||
transformActions,
|
||||
docWithLocales,
|
||||
isVersion,
|
||||
skipValidation,
|
||||
});
|
||||
|
||||
if (hook === 'afterRead') {
|
||||
transformActions.forEach((action) => action());
|
||||
}
|
||||
|
||||
const hookResults = hookPromises.map((promise) => promise());
|
||||
await Promise.all(hookResults);
|
||||
|
||||
const valueResults = valuePromises.map((promise) => promise());
|
||||
await Promise.all(valueResults);
|
||||
|
||||
const validationResults = validationPromises.map((promise) => promise());
|
||||
await Promise.all(validationResults);
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new ValidationError(errors);
|
||||
}
|
||||
|
||||
if (hook === 'beforeChange') {
|
||||
transformActions.forEach((action) => action());
|
||||
}
|
||||
|
||||
unflattenLocaleActions.forEach((action) => action());
|
||||
|
||||
const accessResults = accessPromises.map((promise) => promise());
|
||||
await Promise.all(accessResults);
|
||||
|
||||
const relationshipPopulationResults = relationshipPopulations.map((population) => population());
|
||||
await Promise.all(relationshipPopulationResults);
|
||||
|
||||
return fullData;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { Collection } from '../../collections/config/types';
|
||||
import { Payload } from '../..';
|
||||
import { RichTextField, Field } from '../config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
|
||||
@@ -10,7 +9,6 @@ type Arguments = {
|
||||
key: string | number
|
||||
depth: number
|
||||
currentDepth?: number
|
||||
payload: Payload
|
||||
field: RichTextField
|
||||
req: PayloadRequest
|
||||
showHiddenFields: boolean
|
||||
@@ -24,7 +22,6 @@ export const populate = async ({
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}: Omit<Arguments, 'field'> & {
|
||||
@@ -34,7 +31,7 @@ export const populate = async ({
|
||||
}): Promise<void> => {
|
||||
const dataRef = data as Record<string, unknown>;
|
||||
|
||||
const doc = await payload.findByID({
|
||||
const doc = await req.payload.findByID({
|
||||
req,
|
||||
collection: collection.config.slug,
|
||||
id,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
import { Payload } from '../..';
|
||||
import { Field, fieldHasSubFields, fieldIsArrayType, fieldAffectsData } from '../config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { populate } from './populate';
|
||||
@@ -10,7 +9,6 @@ type NestedRichTextFieldsArgs = {
|
||||
data: unknown
|
||||
fields: Field[]
|
||||
req: PayloadRequest
|
||||
payload: Payload
|
||||
overrideAccess: boolean
|
||||
depth: number
|
||||
currentDepth?: number
|
||||
@@ -22,7 +20,6 @@ export const recurseNestedFields = ({
|
||||
data,
|
||||
fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess = false,
|
||||
depth,
|
||||
currentDepth = 0,
|
||||
@@ -34,7 +31,7 @@ export const recurseNestedFields = ({
|
||||
if (field.hasMany && Array.isArray(data[field.name])) {
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
data[field.name].forEach(({ relationTo, value }, i) => {
|
||||
const collection = payload.collections[relationTo];
|
||||
const collection = req.payload.collections[relationTo];
|
||||
if (collection) {
|
||||
promises.push(populate({
|
||||
id: value,
|
||||
@@ -45,7 +42,6 @@ export const recurseNestedFields = ({
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}));
|
||||
@@ -53,7 +49,7 @@ export const recurseNestedFields = ({
|
||||
});
|
||||
} else {
|
||||
data[field.name].forEach((id, i) => {
|
||||
const collection = payload.collections[field.relationTo as string];
|
||||
const collection = req.payload.collections[field.relationTo as string];
|
||||
if (collection) {
|
||||
promises.push(populate({
|
||||
id,
|
||||
@@ -64,7 +60,6 @@ export const recurseNestedFields = ({
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}));
|
||||
@@ -72,7 +67,7 @@ export const recurseNestedFields = ({
|
||||
});
|
||||
}
|
||||
} else if (Array.isArray(field.relationTo) && data[field.name]?.value && data[field.name]?.relationTo) {
|
||||
const collection = payload.collections[data[field.name].relationTo];
|
||||
const collection = req.payload.collections[data[field.name].relationTo];
|
||||
promises.push(populate({
|
||||
id: data[field.name].value,
|
||||
field,
|
||||
@@ -82,14 +77,13 @@ export const recurseNestedFields = ({
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (typeof data[field.name] !== 'undefined' && typeof field.relationTo === 'string') {
|
||||
const collection = payload.collections[field.relationTo];
|
||||
const collection = req.payload.collections[field.relationTo];
|
||||
promises.push(populate({
|
||||
id: data[field.name],
|
||||
field,
|
||||
@@ -99,7 +93,6 @@ export const recurseNestedFields = ({
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
req,
|
||||
showHiddenFields,
|
||||
}));
|
||||
@@ -111,7 +104,6 @@ export const recurseNestedFields = ({
|
||||
data: data[field.name],
|
||||
fields: field.fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
@@ -123,7 +115,6 @@ export const recurseNestedFields = ({
|
||||
data,
|
||||
fields: field.fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
@@ -140,7 +131,6 @@ export const recurseNestedFields = ({
|
||||
data: data[field.name][i],
|
||||
fields: block.fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
@@ -157,7 +147,6 @@ export const recurseNestedFields = ({
|
||||
data: data[field.name][i],
|
||||
fields: field.fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
@@ -173,7 +162,6 @@ export const recurseNestedFields = ({
|
||||
recurseRichText({
|
||||
req,
|
||||
children: node.children,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Payload } from '../..';
|
||||
import { RichTextField } from '../config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { recurseNestedFields } from './recurseNestedFields';
|
||||
import { populate } from './populate';
|
||||
|
||||
type Arguments = {
|
||||
data: unknown
|
||||
overrideAccess?: boolean
|
||||
depth: number
|
||||
type Args = {
|
||||
currentDepth?: number
|
||||
payload: Payload
|
||||
depth: number
|
||||
field: RichTextField
|
||||
overrideAccess?: boolean
|
||||
req: PayloadRequest
|
||||
siblingDoc: Record<string, unknown>
|
||||
showHiddenFields: boolean
|
||||
}
|
||||
|
||||
@@ -20,7 +18,6 @@ type RecurseRichTextArgs = {
|
||||
overrideAccess: boolean
|
||||
depth: number
|
||||
currentDepth: number
|
||||
payload: Payload
|
||||
field: RichTextField
|
||||
req: PayloadRequest
|
||||
promises: Promise<void>[]
|
||||
@@ -30,7 +27,6 @@ type RecurseRichTextArgs = {
|
||||
export const recurseRichText = ({
|
||||
req,
|
||||
children,
|
||||
payload,
|
||||
overrideAccess = false,
|
||||
depth,
|
||||
currentDepth = 0,
|
||||
@@ -40,7 +36,7 @@ export const recurseRichText = ({
|
||||
}: RecurseRichTextArgs): void => {
|
||||
if (Array.isArray(children)) {
|
||||
(children as any[]).forEach((element) => {
|
||||
const collection = payload.collections[element?.relationTo];
|
||||
const collection = req.payload.collections[element?.relationTo];
|
||||
|
||||
if ((element.type === 'relationship' || element.type === 'upload')
|
||||
&& element?.value?.id
|
||||
@@ -52,7 +48,6 @@ export const recurseRichText = ({
|
||||
data: element.fields || {},
|
||||
fields: field.admin.upload.collections[element.relationTo].fields,
|
||||
req,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
@@ -67,7 +62,6 @@ export const recurseRichText = ({
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
payload,
|
||||
field,
|
||||
collection,
|
||||
showHiddenFields,
|
||||
@@ -76,14 +70,13 @@ export const recurseRichText = ({
|
||||
|
||||
if (element?.children) {
|
||||
recurseRichText({
|
||||
req,
|
||||
children: element.children,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
@@ -91,27 +84,25 @@ export const recurseRichText = ({
|
||||
}
|
||||
};
|
||||
|
||||
const richTextRelationshipPromise = ({
|
||||
req,
|
||||
data,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
const richTextRelationshipPromise = async ({
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
overrideAccess,
|
||||
req,
|
||||
siblingDoc,
|
||||
showHiddenFields,
|
||||
}: Arguments) => async (): Promise<void> => {
|
||||
}: Args): Promise<void> => {
|
||||
const promises = [];
|
||||
|
||||
recurseRichText({
|
||||
req,
|
||||
children: data[field.name],
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
children: siblingDoc[field.name] as unknown[],
|
||||
currentDepth,
|
||||
depth,
|
||||
field,
|
||||
overrideAccess,
|
||||
promises,
|
||||
req,
|
||||
showHiddenFields,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
import validationPromise from './validationPromise';
|
||||
import accessPromise from './accessPromise';
|
||||
import hookPromise from './hookPromise';
|
||||
import {
|
||||
Field,
|
||||
fieldHasSubFields,
|
||||
fieldIsArrayType,
|
||||
fieldIsBlockType,
|
||||
fieldAffectsData,
|
||||
HookName,
|
||||
} from './config/types';
|
||||
import { Operation } from '../types';
|
||||
import { PayloadRequest } from '../express/types';
|
||||
import { Payload } from '..';
|
||||
import richTextRelationshipPromise from './richText/relationshipPromise';
|
||||
import getValueWithDefault from './getDefaultValue';
|
||||
|
||||
type Arguments = {
|
||||
fields: Field[]
|
||||
data: Record<string, any>
|
||||
originalDoc: Record<string, any>
|
||||
path: string
|
||||
flattenLocales: boolean
|
||||
locale: string
|
||||
fallbackLocale: string
|
||||
accessPromises: (() => Promise<void>)[]
|
||||
operation: Operation
|
||||
overrideAccess: boolean
|
||||
req: PayloadRequest
|
||||
id?: string | number
|
||||
relationshipPopulations: (() => Promise<void>)[]
|
||||
depth: number
|
||||
currentDepth: number
|
||||
hook: HookName
|
||||
hookPromises: (() => Promise<void>)[]
|
||||
fullOriginalDoc: Record<string, any>
|
||||
fullData: Record<string, any>
|
||||
valuePromises: (() => Promise<void>)[]
|
||||
validationPromises: (() => Promise<string | boolean>)[]
|
||||
errors: { message: string, field: string }[]
|
||||
payload: Payload
|
||||
showHiddenFields: boolean
|
||||
unflattenLocales: boolean
|
||||
unflattenLocaleActions: (() => void)[]
|
||||
transformActions: (() => void)[]
|
||||
docWithLocales?: Record<string, any>
|
||||
skipValidation?: boolean
|
||||
isVersion: boolean
|
||||
}
|
||||
|
||||
const traverseFields = (args: Arguments): void => {
|
||||
const {
|
||||
fields,
|
||||
data = {},
|
||||
originalDoc = {},
|
||||
path,
|
||||
flattenLocales,
|
||||
locale,
|
||||
fallbackLocale,
|
||||
accessPromises,
|
||||
operation,
|
||||
overrideAccess,
|
||||
req,
|
||||
id,
|
||||
relationshipPopulations,
|
||||
depth,
|
||||
currentDepth,
|
||||
hook,
|
||||
hookPromises,
|
||||
fullOriginalDoc,
|
||||
fullData,
|
||||
valuePromises,
|
||||
validationPromises,
|
||||
errors,
|
||||
payload,
|
||||
showHiddenFields,
|
||||
unflattenLocaleActions,
|
||||
unflattenLocales,
|
||||
transformActions,
|
||||
docWithLocales = {},
|
||||
skipValidation,
|
||||
isVersion,
|
||||
} = args;
|
||||
|
||||
fields.forEach((field) => {
|
||||
const dataCopy = data;
|
||||
|
||||
if (hook === 'afterRead') {
|
||||
if (field.type === 'group') {
|
||||
// Fill groups with empty objects so fields with hooks within groups can populate
|
||||
// themselves virtually as necessary
|
||||
if (typeof data[field.name] === 'undefined' && typeof originalDoc[field.name] === 'undefined') {
|
||||
data[field.name] = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field) && field.hidden && typeof data[field.name] !== 'undefined' && !showHiddenFields) {
|
||||
delete data[field.name];
|
||||
}
|
||||
|
||||
if (field.type === 'point') {
|
||||
transformActions.push(() => {
|
||||
if (data[field.name]?.coordinates && Array.isArray(data[field.name].coordinates) && data[field.name].coordinates.length === 2) {
|
||||
data[field.name] = data[field.name].coordinates;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ((field.type === 'upload' || field.type === 'relationship')
|
||||
&& (data[field.name] === '' || data[field.name] === 'none' || data[field.name] === 'null')) {
|
||||
if (field.type === 'relationship' && field.hasMany === true) {
|
||||
dataCopy[field.name] = [];
|
||||
} else {
|
||||
dataCopy[field.name] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'relationship' && field.hasMany && (data[field.name] === '' || data[field.name] === 'none' || data[field.name] === 'null' || data[field.name] === null)) {
|
||||
dataCopy[field.name] = [];
|
||||
}
|
||||
|
||||
if (field.type === 'number' && typeof data[field.name] === 'string') {
|
||||
const trimmed = data[field.name].trim();
|
||||
dataCopy[field.name] = (trimmed.length === 0) ? null : parseFloat(trimmed);
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field) && field.name === 'id') {
|
||||
if (field.type === 'number' && typeof data[field.name] === 'string') {
|
||||
dataCopy[field.name] = parseFloat(data[field.name]);
|
||||
}
|
||||
if (field.type === 'text' && typeof data[field.name]?.toString === 'function' && typeof data[field.name] !== 'string') {
|
||||
dataCopy[field.name] = dataCopy[field.name].toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'checkbox') {
|
||||
if (data[field.name] === 'true') dataCopy[field.name] = true;
|
||||
if (data[field.name] === 'false') dataCopy[field.name] = false;
|
||||
if (data[field.name] === '') dataCopy[field.name] = false;
|
||||
}
|
||||
|
||||
if (field.type === 'richText') {
|
||||
if (typeof data[field.name] === 'string') {
|
||||
try {
|
||||
const richTextJSON = JSON.parse(data[field.name] as string);
|
||||
dataCopy[field.name] = richTextJSON;
|
||||
} catch {
|
||||
// Disregard this data as it is not valid.
|
||||
// Will be reported to user by field validation
|
||||
}
|
||||
}
|
||||
|
||||
if (((field.admin?.elements?.includes('relationship') || field.admin?.elements?.includes('upload')) || !field?.admin?.elements) && hook === 'afterRead') {
|
||||
relationshipPopulations.push(richTextRelationshipPromise({
|
||||
req,
|
||||
data,
|
||||
payload,
|
||||
overrideAccess,
|
||||
depth,
|
||||
field,
|
||||
currentDepth,
|
||||
showHiddenFields,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const hasLocalizedValue = fieldAffectsData(field)
|
||||
&& (typeof data?.[field.name] === 'object' && data?.[field.name] !== null)
|
||||
&& field.name
|
||||
&& field.localized
|
||||
&& locale !== 'all'
|
||||
&& flattenLocales;
|
||||
|
||||
if (hasLocalizedValue) {
|
||||
let localizedValue = data[field.name][locale];
|
||||
if (typeof localizedValue === 'undefined' && fallbackLocale) localizedValue = data[field.name][fallbackLocale];
|
||||
if (typeof localizedValue === 'undefined' && field.type === 'group') localizedValue = {};
|
||||
if (typeof localizedValue === 'undefined') localizedValue = null;
|
||||
dataCopy[field.name] = localizedValue;
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field) && field.localized && unflattenLocales) {
|
||||
unflattenLocaleActions.push(() => {
|
||||
const localeData = payload.config.localization.locales.reduce((locales, localeID) => {
|
||||
let valueToSet;
|
||||
|
||||
if (localeID === locale) {
|
||||
if (typeof data[field.name] !== 'undefined') {
|
||||
valueToSet = data[field.name];
|
||||
} else if (docWithLocales?.[field.name]?.[localeID]) {
|
||||
valueToSet = docWithLocales?.[field.name]?.[localeID];
|
||||
}
|
||||
} else {
|
||||
valueToSet = docWithLocales?.[field.name]?.[localeID];
|
||||
}
|
||||
|
||||
if (typeof valueToSet !== 'undefined') {
|
||||
return {
|
||||
...locales,
|
||||
[localeID]: valueToSet,
|
||||
};
|
||||
}
|
||||
|
||||
return locales;
|
||||
}, {});
|
||||
|
||||
// If there are locales with data, set the data
|
||||
if (Object.keys(localeData).length > 0) {
|
||||
data[field.name] = localeData;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
accessPromises.push(() => accessPromise({
|
||||
data,
|
||||
fullData,
|
||||
originalDoc,
|
||||
field,
|
||||
operation,
|
||||
overrideAccess,
|
||||
req,
|
||||
id,
|
||||
relationshipPopulations,
|
||||
depth,
|
||||
currentDepth,
|
||||
hook,
|
||||
payload,
|
||||
showHiddenFields,
|
||||
}));
|
||||
|
||||
hookPromises.push(() => hookPromise({
|
||||
data,
|
||||
field,
|
||||
hook,
|
||||
req,
|
||||
operation,
|
||||
fullOriginalDoc,
|
||||
fullData,
|
||||
flattenLocales,
|
||||
isVersion,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
const passesCondition = (field.admin?.condition && hook === 'beforeChange') ? field.admin.condition(fullData, data) : true;
|
||||
const skipValidationFromHere = skipValidation || !passesCondition;
|
||||
|
||||
if (fieldHasSubFields(field)) {
|
||||
if (!fieldAffectsData(field)) {
|
||||
traverseFields({
|
||||
...args,
|
||||
fields: field.fields,
|
||||
skipValidation: skipValidationFromHere,
|
||||
});
|
||||
} else if (fieldIsArrayType(field)) {
|
||||
if (Array.isArray(data[field.name])) {
|
||||
for (let i = 0; i < data[field.name].length; i += 1) {
|
||||
if (typeof (data[field.name][i]) === 'undefined') {
|
||||
data[field.name][i] = {};
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
...args,
|
||||
fields: field.fields,
|
||||
data: data[field.name][i] || {},
|
||||
originalDoc: originalDoc?.[field.name]?.[i],
|
||||
docWithLocales: docWithLocales?.[field.name]?.[i],
|
||||
path: `${path}${field.name}.${i}.`,
|
||||
skipValidation: skipValidationFromHere,
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
traverseFields({
|
||||
...args,
|
||||
fields: field.fields,
|
||||
data: data[field.name] as Record<string, unknown>,
|
||||
originalDoc: originalDoc[field.name],
|
||||
docWithLocales: docWithLocales?.[field.name],
|
||||
path: `${path}${field.name}.`,
|
||||
skipValidation: skipValidationFromHere,
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldIsBlockType(field)) {
|
||||
if (Array.isArray(data[field.name])) {
|
||||
(data[field.name] as Record<string, unknown>[]).forEach((rowData, i) => {
|
||||
const block = field.blocks.find((blockType) => blockType.slug === rowData.blockType);
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
...args,
|
||||
fields: block.fields,
|
||||
data: rowData || {},
|
||||
originalDoc: originalDoc?.[field.name]?.[i],
|
||||
docWithLocales: docWithLocales?.[field.name]?.[i],
|
||||
path: `${path}${field.name}.${i}.`,
|
||||
skipValidation: skipValidationFromHere,
|
||||
showHiddenFields,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (hook === 'beforeChange' && fieldAffectsData(field)) {
|
||||
const updatedData = data;
|
||||
|
||||
if (data?.[field.name] === undefined && originalDoc?.[field.name] === undefined && field.defaultValue) {
|
||||
valuePromises.push(async () => {
|
||||
let valueToUpdate = data?.[field.name];
|
||||
|
||||
if (typeof valueToUpdate === 'undefined' && typeof originalDoc?.[field.name] !== 'undefined') {
|
||||
valueToUpdate = originalDoc?.[field.name];
|
||||
}
|
||||
|
||||
const value = await getValueWithDefault({ value: valueToUpdate, defaultValue: field.defaultValue, locale, user: req.user });
|
||||
updatedData[field.name] = value;
|
||||
});
|
||||
}
|
||||
|
||||
if (field.type === 'relationship' || field.type === 'upload') {
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
if (Array.isArray(dataCopy[field.name])) {
|
||||
dataCopy[field.name].forEach((relatedDoc: { value: unknown, relationTo: string }, i) => {
|
||||
const relatedCollection = payload.config.collections.find((collection) => collection.slug === relatedDoc.relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldAffectsData(collectionField) && collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
dataCopy[field.name][i] = { ...relatedDoc, value: parseFloat(relatedDoc.value as string) };
|
||||
}
|
||||
});
|
||||
}
|
||||
if (field.type === 'relationship' && field.hasMany !== true && dataCopy[field.name]?.relationTo) {
|
||||
const relatedCollection = payload.config.collections.find((collection) => collection.slug === dataCopy[field.name].relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldAffectsData(collectionField) && collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
dataCopy[field.name] = { ...dataCopy[field.name], value: parseFloat(dataCopy[field.name].value as string) };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(dataCopy[field.name])) {
|
||||
dataCopy[field.name].forEach((relatedDoc: unknown, i) => {
|
||||
const relatedCollection = payload.config.collections.find((collection) => collection.slug === field.relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldAffectsData(collectionField) && collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
dataCopy[field.name][i] = parseFloat(relatedDoc as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (field.type === 'relationship' && field.hasMany !== true && dataCopy[field.name]) {
|
||||
const relatedCollection = payload.config.collections.find((collection) => collection.slug === field.relationTo);
|
||||
const relationshipIDField = relatedCollection.fields.find((collectionField) => fieldAffectsData(collectionField) && collectionField.name === 'id');
|
||||
if (relationshipIDField?.type === 'number') {
|
||||
dataCopy[field.name] = parseFloat(dataCopy[field.name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'point' && data[field.name]) {
|
||||
transformActions.push(() => {
|
||||
if (Array.isArray(data[field.name]) && data[field.name][0] !== null && data[field.name][1] !== null) {
|
||||
data[field.name] = {
|
||||
type: 'Point',
|
||||
coordinates: [
|
||||
parseFloat(data[field.name][0]),
|
||||
parseFloat(data[field.name][1]),
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (field.type === 'array' || field.type === 'blocks') {
|
||||
const hasRowsOfNewData = Array.isArray(data[field.name]);
|
||||
const newRowCount = hasRowsOfNewData ? (data[field.name] as Record<string, unknown>[]).length : undefined;
|
||||
|
||||
// Handle cases of arrays being intentionally set to 0
|
||||
if (data[field.name] === '0' || data[field.name] === 0 || data[field.name] === null) {
|
||||
updatedData[field.name] = [];
|
||||
}
|
||||
|
||||
const hasRowsOfExistingData = Array.isArray(originalDoc[field.name]);
|
||||
const existingRowCount = hasRowsOfExistingData ? originalDoc[field.name].length : 0;
|
||||
|
||||
validationPromises.push(() => validationPromise({
|
||||
errors,
|
||||
hook,
|
||||
data: { [field.name]: newRowCount },
|
||||
fullData,
|
||||
originalDoc: { [field.name]: existingRowCount },
|
||||
fullOriginalDoc,
|
||||
field,
|
||||
path,
|
||||
skipValidation: skipValidationFromHere,
|
||||
payload: req.payload,
|
||||
user: req.user,
|
||||
operation,
|
||||
id,
|
||||
}));
|
||||
} else if (fieldAffectsData(field)) {
|
||||
validationPromises.push(() => validationPromise({
|
||||
errors,
|
||||
hook,
|
||||
data,
|
||||
fullData,
|
||||
originalDoc,
|
||||
fullOriginalDoc,
|
||||
field,
|
||||
path,
|
||||
skipValidation: skipValidationFromHere,
|
||||
user: req.user,
|
||||
operation,
|
||||
id,
|
||||
payload: req.payload,
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default traverseFields;
|
||||
@@ -1,67 +0,0 @@
|
||||
import merge from 'deepmerge';
|
||||
import { Payload } from '..';
|
||||
import { User } from '../auth';
|
||||
import { Operation } from '../types';
|
||||
import { HookName, FieldAffectingData } from './config/types';
|
||||
|
||||
type Arguments = {
|
||||
hook: HookName
|
||||
field: FieldAffectingData
|
||||
path: string
|
||||
errors: {message: string, field: string}[]
|
||||
data: Record<string, unknown>
|
||||
fullData: Record<string, unknown>
|
||||
originalDoc: Record<string, unknown>
|
||||
fullOriginalDoc: Record<string, unknown>
|
||||
id?: string | number
|
||||
skipValidation?: boolean
|
||||
user: User
|
||||
operation: Operation
|
||||
payload: Payload
|
||||
}
|
||||
|
||||
const validationPromise = async ({
|
||||
errors,
|
||||
hook,
|
||||
originalDoc,
|
||||
fullOriginalDoc,
|
||||
data,
|
||||
fullData,
|
||||
id,
|
||||
field,
|
||||
path,
|
||||
skipValidation,
|
||||
user,
|
||||
operation,
|
||||
payload,
|
||||
}: Arguments): Promise<string | boolean> => {
|
||||
if (hook !== 'beforeChange' || skipValidation) return true;
|
||||
|
||||
const hasCondition = field.admin && field.admin.condition;
|
||||
const shouldValidate = field.validate && !hasCondition;
|
||||
|
||||
let valueToValidate = data?.[field.name];
|
||||
if (valueToValidate === undefined) valueToValidate = originalDoc?.[field.name];
|
||||
if (valueToValidate === undefined) valueToValidate = field.defaultValue;
|
||||
|
||||
const result = shouldValidate ? await field.validate(valueToValidate, {
|
||||
...field,
|
||||
data: merge(fullOriginalDoc, fullData),
|
||||
siblingData: merge(originalDoc, data),
|
||||
id,
|
||||
operation,
|
||||
user,
|
||||
payload,
|
||||
}) : true;
|
||||
|
||||
if (typeof result === 'string') {
|
||||
errors.push({
|
||||
message: result,
|
||||
field: `${path}${field.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default validationPromise;
|
||||
Reference in New Issue
Block a user