strapi-plugin-publisher
Version:
A plugin for Strapi Headless CMS that provides the ability to schedule publishing for any content type.
407 lines (341 loc) • 9.96 kB
JavaScript
// From https://github.com/strapi/strapi/blob/main/packages/core/admin/admin/src/content-manager/utils/schema.js
import { translatedErrors as errorsTrads } from '@strapi/helper-plugin';
import get from 'lodash/get';
import isBoolean from 'lodash/isBoolean';
import isEmpty from 'lodash/isEmpty';
import isNaN from 'lodash/isNaN';
import toNumber from 'lodash/toNumber';
import * as yup from 'yup';
function isFieldTypeNumber(type) {
return ['integer', 'biginteger', 'decimal', 'float', 'number'].includes(type);
}
yup.addMethod(yup.mixed, 'defined', function () {
return this.test('defined', errorsTrads.required, (value) => value !== undefined);
});
yup.addMethod(yup.array, 'notEmptyMin', function (min) {
return this.test('notEmptyMin', errorsTrads.min, (value) => {
if (isEmpty(value)) {
return true;
}
return value.length >= min;
});
});
yup.addMethod(yup.string, 'isInferior', function (message, max) {
return this.test('isInferior', message, function (value) {
if (!value) {
return true;
}
if (Number.isNaN(toNumber(value))) {
return true;
}
return toNumber(max) >= toNumber(value);
});
});
yup.addMethod(yup.string, 'isSuperior', function (message, min) {
return this.test('isSuperior', message, function (value) {
if (!value) {
return true;
}
if (Number.isNaN(toNumber(value))) {
return true;
}
return toNumber(value) >= toNumber(min);
});
});
const getAttributes = (data) => get(data, ['attributes'], {});
export function createYupSchema(
model,
{ components },
options = {
isCreatingEntry: true,
isDraft: true,
isFromComponent: false,
isJSONTestDisabled: false,
}
) {
const attributes = getAttributes(model);
return yup.object().shape(
Object.keys(attributes).reduce((acc, current) => {
const attribute = attributes[current];
if (
attribute.type !== 'relation' &&
attribute.type !== 'component' &&
attribute.type !== 'dynamiczone'
) {
const formatted = createYupSchemaAttribute(attribute.type, attribute, options);
acc[current] = formatted;
}
if (attribute.type === 'relation') {
acc[current] = [
'oneWay',
'oneToOne',
'manyToOne',
'oneToManyMorph',
'oneToOneMorph',
].includes(attribute.relationType)
? yup.object().nullable()
: yup.array().nullable();
}
if (attribute.type === 'component') {
const componentFieldSchema = createYupSchema(
components[attribute.component],
{
components,
},
{ ...options, isFromComponent: true }
);
if (attribute.repeatable === true) {
const { min, max, required } = attribute;
let componentSchema = yup.lazy((value) => {
let baseSchema = yup.array().of(componentFieldSchema);
if (min) {
if (required) {
baseSchema = baseSchema.min(min, errorsTrads.min);
} else if (required !== true && isEmpty(value)) {
baseSchema = baseSchema.nullable();
} else {
baseSchema = baseSchema.min(min, errorsTrads.min);
}
} else if (required && !options.isDraft) {
baseSchema = baseSchema.min(1, errorsTrads.required);
}
if (max) {
baseSchema = baseSchema.max(max, errorsTrads.max);
}
return baseSchema;
});
acc[current] = componentSchema;
return acc;
}
const componentSchema = yup.lazy((obj) => {
if (obj !== undefined) {
return attribute.required === true && !options.isDraft
? componentFieldSchema.defined()
: componentFieldSchema.nullable();
}
return attribute.required === true ? yup.object().defined() : yup.object().nullable();
});
acc[current] = componentSchema;
return acc;
}
if (attribute.type === 'dynamiczone') {
let dynamicZoneSchema = yup.array().of(
yup.lazy(({ __component }) => {
return createYupSchema(
components[__component],
{ components },
{ ...options, isFromComponent: true }
);
})
);
const { max, min } = attribute;
if (min) {
if (attribute.required) {
dynamicZoneSchema = dynamicZoneSchema
.test('min', errorsTrads.min, (value) => {
if (options.isCreatingEntry) {
return value && value.length >= min;
}
if (value === undefined) {
return true;
}
return value !== null && value.length >= min;
})
.test('required', errorsTrads.required, (value) => {
if (options.isCreatingEntry) {
return value !== null || value !== undefined;
}
if (value === undefined) {
return true;
}
return value !== null;
});
} else {
dynamicZoneSchema = dynamicZoneSchema.notEmptyMin(min);
}
} else if (attribute.required && !options.isDraft) {
dynamicZoneSchema = dynamicZoneSchema.test('required', errorsTrads.required, (value) => {
if (options.isCreatingEntry) {
return value !== null || value !== undefined;
}
if (value === undefined) {
return true;
}
return value !== null;
});
}
if (max) {
dynamicZoneSchema = dynamicZoneSchema.max(max, errorsTrads.max);
}
acc[current] = dynamicZoneSchema;
}
return acc;
}, {})
);
}
const createYupSchemaAttribute = (type, validations, options) => {
let schema = yup.mixed();
if (['string', 'uid', 'text', 'richtext', 'email', 'password', 'enumeration'].includes(type)) {
schema = yup.string();
}
if (type === 'blocks') {
schema = yup.mixed().test('isJSON', errorsTrads.json, (value) => {
// Disable the test for bulk publish, it's valid when it comes from the db
if (options.isJSONTestDisabled) {
return true;
}
// Don't run validations on drafts
if (options.isDraft) {
return true;
}
// The backend validates the actual schema, check if a value different than null is not an array
if (value && !Array.isArray(value)) {
return false;
}
return true;
});
}
if (type === 'json') {
schema = yup
.mixed(errorsTrads.json)
.test('isJSON', errorsTrads.json, (value) => {
// Disable the test for bulk publish, it's valid when it comes from the db
if (options.isJSONTestDisabled) {
return true;
}
if (!value || !value.length) {
return true;
}
try {
JSON.parse(value);
return true;
} catch (err) {
return false;
}
})
.nullable()
.test('required', errorsTrads.required, (value) => {
if (validations.required && (!value || !value.length)) {
return false;
}
return true;
});
}
if (type === 'email') {
schema = schema.email(errorsTrads.email);
}
if (['number', 'integer', 'float', 'decimal'].includes(type)) {
schema = yup
.number()
.transform((cv) => (isNaN(cv) ? undefined : cv))
.typeError();
}
if (type === 'biginteger') {
schema = yup.string().matches(/^-?\d*$/);
}
if (['date', 'datetime'].includes(type)) {
schema = yup.date();
}
Object.keys(validations).forEach((validation) => {
const validationValue = validations[validation];
if (
!!validationValue ||
(!isBoolean(validationValue) && Number.isInteger(Math.floor(validationValue))) ||
validationValue === 0
) {
switch (validation) {
case 'required': {
if (!options.isDraft) {
if (type === 'password' && options.isCreatingEntry) {
schema = schema.required(errorsTrads.required);
}
if (type !== 'password') {
if (options.isCreatingEntry) {
schema = schema.required(errorsTrads.required);
} else {
schema = schema.test('required', errorsTrads.required, (value) => {
// Field is not touched and the user is editing the entry
if (value === undefined && !options.isFromComponent) {
return true;
}
if (isFieldTypeNumber(type)) {
if (value === 0) {
return true;
}
return !!value;
}
if (type === 'boolean') {
// Boolean value can be undefined/unset in modifiedData when generated in a new component
return value !== null && value !== undefined;
}
if (type === 'date' || type === 'datetime') {
if (typeof value === 'string') {
return !isEmpty(value);
}
return !isEmpty(value?.toString());
}
return !isEmpty(value);
});
}
}
}
break;
}
case 'max': {
if (type === 'biginteger') {
schema = schema.isInferior(errorsTrads.max, validationValue);
} else {
schema = schema.max(validationValue, errorsTrads.max);
}
break;
}
case 'maxLength':
schema = schema.max(validationValue, errorsTrads.maxLength);
break;
case 'min': {
if (type === 'biginteger') {
schema = schema.isSuperior(errorsTrads.min, validationValue);
} else {
schema = schema.min(validationValue, errorsTrads.min);
}
break;
}
case 'minLength': {
if (!options.isDraft) {
schema = schema.min(validationValue, errorsTrads.minLength);
}
break;
}
case 'regex':
schema = schema.matches(new RegExp(validationValue), {
message: errorsTrads.regex,
excludeEmptyString: !validations.required,
});
break;
case 'lowercase':
if (['text', 'textarea', 'email', 'string'].includes(type)) {
schema = schema.strict().lowercase();
}
break;
case 'uppercase':
if (['text', 'textarea', 'email', 'string'].includes(type)) {
schema = schema.strict().uppercase();
}
break;
case 'positive':
if (isFieldTypeNumber(type)) {
schema = schema.positive();
}
break;
case 'negative':
if (isFieldTypeNumber(type)) {
schema = schema.negative();
}
break;
default:
schema = schema.nullable();
}
}
});
return schema;
};