Merge branch 'master' of github.com:payloadcms/payload into fix/relationship-access-missing-id

This commit is contained in:
Jarrod Flesch
2021-10-01 17:08:37 -04:00
236 changed files with 5089 additions and 2370 deletions

View File

@@ -1,11 +1,11 @@
import joi from 'joi';
const component = joi.alternatives().try(
joi.object().unknown(),
joi.func(),
);
import { componentSchema } from '../../utilities/componentSchema';
export const baseAdminFields = joi.object().keys({
description: joi.alternatives().try(
joi.string(),
componentSchema,
),
position: joi.string().valid('sidebar'),
width: joi.string(),
style: joi.object().unknown(),
@@ -14,9 +14,9 @@ export const baseAdminFields = joi.object().keys({
disabled: joi.boolean().default(false),
condition: joi.func(),
components: joi.object().keys({
Cell: component,
Field: component,
Filter: component,
Cell: componentSchema,
Field: componentSchema,
Filter: componentSchema,
}).default({}),
});
@@ -47,6 +47,13 @@ export const baseField = joi.object().keys({
admin: baseAdminFields.default(),
}).default();
export const idField = baseField.keys({
name: joi.string().valid('id'),
type: joi.string().valid('text', 'number'),
required: joi.not(false, 0).default(true),
localized: joi.invalid(true),
});
export const text = baseField.keys({
type: joi.string().valid('text').required(),
name: joi.string().required(),
@@ -111,7 +118,7 @@ export const select = baseField.keys({
options: joi.array().items(joi.alternatives().try(
joi.string(),
joi.object({
value: joi.string().allow('').required(),
value: joi.string().required(),
label: joi.string().required(),
}),
)).required(),
@@ -128,7 +135,7 @@ export const radio = baseField.keys({
options: joi.array().items(joi.alternatives().try(
joi.string(),
joi.object({
value: joi.string().allow('').required(),
value: joi.string().required(),
label: joi.string().required(),
}),
)).required(),
@@ -141,16 +148,21 @@ export const radio = baseField.keys({
export const row = baseField.keys({
type: joi.string().valid('row').required(),
fields: joi.array().items(joi.link('#field')),
admin: baseAdminFields.keys({
description: joi.forbidden(),
readOnly: joi.forbidden(),
hidden: joi.forbidden(),
}),
});
export const group = baseField.keys({
type: joi.string().valid('group').required(),
name: joi.string().required(),
label: joi.string(),
fields: joi.array().items(joi.link('#field')),
defaultValue: joi.object(),
admin: baseAdminFields.keys({
hideGutter: joi.boolean().default(false),
description: joi.string(),
}),
});
@@ -180,6 +192,12 @@ export const checkbox = baseField.keys({
defaultValue: joi.boolean(),
});
export const point = baseField.keys({
type: joi.string().valid('point').required(),
name: joi.string().required(),
defaultValue: joi.array().items(joi.number()).max(2).min(2),
});
export const relationship = baseField.keys({
type: joi.string().valid('relationship').required(),
hasMany: joi.boolean().default(false),
@@ -226,9 +244,9 @@ export const richText = baseField.keys({
joi.string(),
joi.object({
name: joi.string().required(),
Button: component,
Element: component,
plugins: joi.array().items(component),
Button: componentSchema,
Element: componentSchema,
plugins: joi.array().items(componentSchema),
}),
),
),
@@ -237,9 +255,9 @@ export const richText = baseField.keys({
joi.string(),
joi.object({
name: joi.string().required(),
Button: component,
Leaf: component,
plugins: joi.array().items(component),
Button: componentSchema,
Leaf: componentSchema,
plugins: joi.array().items(componentSchema),
}),
),
),
@@ -285,6 +303,7 @@ const fieldSchema = joi.alternatives()
richText,
blocks,
date,
point,
)
.id('field');

View File

@@ -22,7 +22,7 @@ export type FieldAccess = (args: {
siblingData: Record<string, unknown>
}) => Promise<boolean> | boolean;
export type Condition = (data: Record<string, unknown>, siblingData: Record<string, unknown>) => boolean
export type Condition = (data: Record<string, unknown>, siblingData: Record<string, unknown>) => boolean;
type Admin = {
position?: string;
@@ -31,10 +31,17 @@ type Admin = {
readOnly?: boolean;
disabled?: boolean;
condition?: Condition;
components?: { [key: string]: React.ComponentType };
description?: Description;
components?: {
Filter?: React.ComponentType;
Cell?: React.ComponentType;
Field?: React.ComponentType;
}
hidden?: boolean
}
export type Description = string | ((value: Record<string, unknown>) => string);
export type Labels = {
singular: string;
plural: string;
@@ -133,7 +140,13 @@ export type GroupField = FieldBase & {
}
}
export type RowField = FieldBase & {
export type RowAdmin = Omit<Admin, 'description'> & {
readOnly?: false;
hidden?: false;
};
export type RowField = Omit<FieldBase, 'admin'> & {
admin?: RowAdmin;
type: 'row';
fields: Field[];
}
@@ -184,7 +197,7 @@ export type RichTextCustomLeaf = {
plugins?: RichTextPlugin[]
}
export type RichTextElement = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote' | 'ul' | 'ol' | 'link' | 'relationship' | RichTextCustomElement;
export type RichTextElement = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote' | 'ul' | 'ol' | 'link' | 'relationship' | 'upload' | RichTextCustomElement;
export type RichTextLeaf = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'code' | RichTextCustomLeaf;
export type RichTextField = FieldBase & {
@@ -214,11 +227,11 @@ export type RadioField = FieldBase & {
}
export type Block = {
slug: string,
labels?: Labels
fields: Field[],
imageURL?: string
imageAltText?: string
slug: string;
labels?: Labels;
fields: Field[];
imageURL?: string;
imageAltText?: string;
}
export type BlockField = FieldBase & {
@@ -230,6 +243,10 @@ export type BlockField = FieldBase & {
labels?: Labels
}
export type PointField = FieldBase & {
type: 'point',
}
export type Field =
TextField
| NumberField
@@ -246,6 +263,7 @@ export type Field =
| SelectField
| UploadField
| CodeField
| PointField
| RowField;
export type FieldWithPath = Field & {

View File

@@ -2,8 +2,8 @@ import { Payload } from '..';
import { ValidationError } from '../errors';
import sanitizeFallbackLocale from '../localization/sanitizeFallbackLocale';
import traverseFields from './traverseFields';
import { CollectionConfig } from '../collections/config/types';
import { GlobalConfig } from '../globals/config/types';
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';
@@ -25,7 +25,7 @@ type Arguments = {
currentDepth?: number
}
export default async function performFieldOperations(this: Payload, entityConfig: CollectionConfig | GlobalConfig, args: Arguments): Promise<{ [key: string]: unknown }> {
export default async function performFieldOperations(this: Payload, entityConfig: SanitizedCollectionConfig | SanitizedGlobalConfig, args: Arguments): Promise<{ [key: string]: unknown }> {
const {
data,
originalDoc: fullOriginalDoc,
@@ -66,6 +66,7 @@ export default async function performFieldOperations(this: Payload, entityConfig
const relationshipPopulations = [];
const hookPromises = [];
const unflattenLocaleActions = [];
const transformActions = [];
const errors: { message: string, field: string }[] = [];
// //////////////////////////////////////////
@@ -98,26 +99,35 @@ export default async function performFieldOperations(this: Payload, entityConfig
showHiddenFields,
unflattenLocales,
unflattenLocaleActions,
transformActions,
docWithLocales,
});
await Promise.all(hookPromises);
if (hook === 'afterRead') {
transformActions.forEach((action) => action());
}
const hookResults = hookPromises.map((promise) => promise());
await Promise.all(hookResults);
validationPromises.forEach((promise) => promise());
await Promise.all(validationPromises);
if (errors.length > 0) {
throw new ValidationError(errors);
}
if (hook === 'beforeChange') {
transformActions.forEach((action) => action());
}
unflattenLocaleActions.forEach((action) => action());
await Promise.all(accessPromises);
const accessResults = accessPromises.map((promise) => promise());
await Promise.all(accessResults);
const relationshipPopulationPromises = relationshipPopulations.map((population) => population());
await Promise.all(relationshipPopulationPromises);
const relationshipPopulationResults = relationshipPopulations.map((population) => population());
await Promise.all(relationshipPopulationResults);
return fullData;
}

View File

@@ -73,7 +73,7 @@ const recurseRichText = ({
(children as any[]).forEach((element) => {
const collection = payload.collections[element?.relationTo];
if (element.type === 'relationship'
if ((element.type === 'relationship' || element.type === 'upload')
&& element?.value?.id
&& collection
&& (depth && currentDepth <= depth)) {

View File

@@ -15,7 +15,7 @@ type Arguments = {
flattenLocales: boolean
locale: string
fallbackLocale: string
accessPromises: Promise<void>[]
accessPromises: (() => Promise<void>)[]
operation: Operation
overrideAccess: boolean
req: PayloadRequest
@@ -24,7 +24,7 @@ type Arguments = {
depth: number
currentDepth: number
hook: HookName
hookPromises: Promise<void>[]
hookPromises: (() => Promise<void>)[]
fullOriginalDoc: Record<string, any>
fullData: Record<string, any>
validationPromises: (() => Promise<string | boolean>)[]
@@ -33,7 +33,9 @@ type Arguments = {
showHiddenFields: boolean
unflattenLocales: boolean
unflattenLocaleActions: (() => void)[]
transformActions: (() => void)[]
docWithLocales?: Record<string, any>
skipValidation?: boolean
}
const traverseFields = (args: Arguments): void => {
@@ -63,22 +65,46 @@ const traverseFields = (args: Arguments): void => {
showHiddenFields,
unflattenLocaleActions,
unflattenLocales,
transformActions,
docWithLocales = {},
skipValidation,
} = args;
fields.forEach((field) => {
const dataCopy = data;
if (hook === 'afterRead' && field.hidden && typeof data[field.name] !== 'undefined' && !showHiddenFields) {
delete data[field.name];
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 (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')) {
dataCopy[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]?.[0] === '' || data[field.name]?.[0] === 'none' || data[field.name]?.[0] === 'null')) {
if (field.type === 'relationship' && field.hasMany && (data[field.name] === '' || data[field.name] === 'none' || data[field.name] === 'null')) {
dataCopy[field.name] = [];
}
@@ -86,6 +112,15 @@ const traverseFields = (args: Arguments): void => {
dataCopy[field.name] = parseFloat(data[field.name]);
}
if (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;
@@ -125,6 +160,7 @@ const traverseFields = (args: Arguments): void => {
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;
}
@@ -161,7 +197,7 @@ const traverseFields = (args: Arguments): void => {
});
}
accessPromises.push(accessPromise({
accessPromises.push(() => accessPromise({
data,
fullData,
originalDoc,
@@ -177,7 +213,7 @@ const traverseFields = (args: Arguments): void => {
payload,
}));
hookPromises.push(hookPromise({
hookPromises.push(() => hookPromise({
data,
field,
hook,
@@ -187,11 +223,15 @@ const traverseFields = (args: Arguments): void => {
fullData,
}));
const passesCondition = (field.admin?.condition && hook === 'beforeChange') ? field.admin.condition(fullData, data) : true;
const skipValidationFromHere = skipValidation || !passesCondition;
if (fieldHasSubFields(field)) {
if (field.name === undefined) {
traverseFields({
...args,
fields: field.fields,
skipValidation: skipValidationFromHere,
});
} else if (fieldIsArrayType(field)) {
if (Array.isArray(data[field.name])) {
@@ -207,6 +247,7 @@ const traverseFields = (args: Arguments): void => {
originalDoc: originalDoc?.[field.name]?.[i],
docWithLocales: docWithLocales?.[field.name]?.[i],
path: `${path}${field.name}.${i}.`,
skipValidation: skipValidationFromHere,
});
}
}
@@ -218,6 +259,7 @@ const traverseFields = (args: Arguments): void => {
originalDoc: originalDoc[field.name],
docWithLocales: docWithLocales?.[field.name],
path: `${path}${field.name}.`,
skipValidation: skipValidationFromHere,
});
}
}
@@ -235,6 +277,7 @@ const traverseFields = (args: Arguments): void => {
originalDoc: originalDoc?.[field.name]?.[i],
docWithLocales: docWithLocales?.[field.name]?.[i],
path: `${path}${field.name}.${i}.`,
skipValidation: skipValidationFromHere,
});
}
});
@@ -248,6 +291,58 @@ const traverseFields = (args: Arguments): void => {
updatedData[field.name] = field.defaultValue;
}
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) => 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) => 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) => 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) => 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 : 0;
@@ -267,6 +362,7 @@ const traverseFields = (args: Arguments): void => {
existingData: { [field.name]: existingRowCount },
field,
path,
skipValidation: skipValidationFromHere,
}));
} else {
validationPromises.push(() => validationPromise({
@@ -276,6 +372,7 @@ const traverseFields = (args: Arguments): void => {
existingData: originalDoc,
field,
path,
skipValidation: skipValidationFromHere,
}));
}
}

View File

@@ -7,6 +7,7 @@ type Arguments = {
errors: {message: string, field: string}[]
newData: Record<string, unknown>
existingData: Record<string, unknown>
skipValidation?: boolean
}
const validationPromise = async ({
@@ -16,8 +17,9 @@ const validationPromise = async ({
existingData,
field,
path,
skipValidation,
}: Arguments): Promise<string | boolean> => {
if (hook !== 'beforeChange') return true;
if (hook !== 'beforeChange' || skipValidation) return true;
const hasCondition = field.admin && field.admin.condition;
const shouldValidate = field.validate && !hasCondition;

View File

@@ -199,6 +199,24 @@ export const blocks: Validate = (value, options = {}) => {
return true;
};
export const point: Validate = (value: [number | string, number | string] = ['', ''], options = {}) => {
const x = parseFloat(String(value[0]));
const y = parseFloat(String(value[1]));
if (
(value[0] && value[1] && typeof x !== 'number' && typeof y !== 'number')
|| (options.required && (Number.isNaN(x) || Number.isNaN(y)))
|| (Array.isArray(value) && value.length !== 2)
) {
return 'This field requires two numbers';
}
if (!options.required && typeof value[0] !== typeof value[1]) {
return 'This field requires two numbers or both can be empty';
}
return true;
};
export default {
number,
text,
@@ -216,4 +234,5 @@ export default {
select,
radio,
blocks,
point,
};