feat: remove joi schema validation (#7226)

We do not really need runtime joi schema validation - this is what TypeScript is for. If people are ignoring TypeScript errors in your schema, or JavaScript errors, that is their fault and does not warrant an extra dependency (joi), lots of code to maintain, as well as slower startups.

If we wanna keep runtime schema validation, we should switch to zod so that we can generate TypeScript types based on the schema and do not have to manually maintain config properties in 2 different places (types & schema).

**joi PROs:**
- Safety for JavaScript-only evangelists messing up their schema
- Safety for people putting @ts-expect-error or `as any` everywhere in their code

**joi CONs:**
- Larger bundle size
- More Modules
- Slower Compilation Speed in dev. Worse DX
- Slower Startup (it needs to validate) in dev. Worse DX
- More code to maintain. For every schema change we'll have to change the types AND the joi schema
- TypeScript already throws proper errors if you mess up your schema. Why have runtime errors?
- The errors are bad. They might tell you what field has an issue, but they do not tell you what exactly is wrong. You have probably seen those "Field XY, value is incorrect" errors - and value could mean anything. Worse DX
- Having extra properties in your schema, even if they are useless, doesn't cause any harm

Cons outweigh the pros
This commit is contained in:
Alessio Gravili
2024-07-22 13:22:54 -04:00
committed by GitHub
parent f50e599684
commit e83eb99436
12 changed files with 0 additions and 1376 deletions

View File

@@ -15,7 +15,6 @@ async function build() {
splitting: false,
external: [
'lodash',
//'joi',
'*.scss',
'*.css',
'@payloadcms/translations',

View File

@@ -99,7 +99,6 @@
"get-tsconfig": "^4.7.2",
"http-status": "1.6.2",
"image-size": "^1.1.1",
"joi": "^17.12.1",
"json-schema-to-typescript": "11.0.3",
"jsonwebtoken": "9.0.1",
"minimist": "1.2.8",
@@ -117,7 +116,6 @@
"@monaco-editor/react": "4.5.1",
"@payloadcms/eslint-config": "workspace:*",
"@types/express-fileupload": "1.4.1",
"@types/joi": "14.3.4",
"@types/json-schema": "7.0.15",
"@types/jsonwebtoken": "8.5.9",
"@types/minimist": "1.2.2",

View File

@@ -1,240 +0,0 @@
import joi from 'joi'
import { endpointsSchema } from '../../config/schema.js'
import {
componentSchema,
customViewSchema,
livePreviewSchema,
} from '../../config/shared/componentSchema.js'
import { openGraphSchema } from '../../config/shared/openGraphSchema.js'
const collectionSchema = joi.object().keys({
slug: joi.string().required(),
access: joi.object({
admin: joi.func(),
create: joi.func(),
delete: joi.func(),
read: joi.func(),
readVersions: joi.func(),
unlock: joi.func(),
update: joi.func(),
}),
admin: joi.object({
components: joi.object({
afterList: joi.array().items(componentSchema),
afterListTable: joi.array().items(componentSchema),
beforeList: joi.array().items(componentSchema),
beforeListTable: joi.array().items(componentSchema),
edit: joi.object({
Description: componentSchema,
PreviewButton: componentSchema,
PublishButton: componentSchema,
SaveButton: componentSchema,
SaveDraftButton: componentSchema,
Upload: componentSchema,
}),
views: joi.object({
Edit: joi.alternatives().try(
componentSchema,
joi.object({
API: joi.alternatives().try(componentSchema, customViewSchema),
Default: joi.alternatives().try(componentSchema, customViewSchema),
LivePreview: joi.alternatives().try(componentSchema, customViewSchema),
Version: joi.alternatives().try(componentSchema, customViewSchema),
Versions: joi.alternatives().try(componentSchema, customViewSchema),
// Relationships
// References
}),
),
List: joi.alternatives().try(
componentSchema,
joi.object({
Component: componentSchema,
actions: joi.array().items(componentSchema),
}),
),
}),
}),
custom: joi.object().pattern(joi.string(), joi.any()),
defaultColumns: joi.array().items(joi.string()),
description: joi
.alternatives()
.try(joi.func(), joi.object().pattern(joi.string(), [joi.string()]), joi.string()),
enableRichTextLink: joi.boolean(),
enableRichTextRelationship: joi.boolean(),
group: joi.alternatives().try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
hidden: joi.alternatives().try(joi.boolean(), joi.func()),
hideAPIURL: joi.bool(),
listSearchableFields: joi.array().items(joi.string()),
livePreview: joi.object(livePreviewSchema),
meta: joi.object({
description: joi.string(),
openGraph: openGraphSchema,
}),
pagination: joi.object({
defaultLimit: joi.number(),
limits: joi.array().items(joi.number()),
}),
preview: joi.func(),
useAsTitle: joi.string(),
}),
auth: joi.alternatives().try(
joi.object({
cookies: joi.object().keys({
domain: joi.string(),
sameSite: joi.string(), // TODO: add further specificity with joi.xor
secure: joi.boolean(),
}),
depth: joi.number(),
disableLocalStrategy: joi.boolean().valid(true),
forgotPassword: joi.object().keys({
generateEmailHTML: joi.func(),
generateEmailSubject: joi.func(),
}),
lockTime: joi.number(),
loginWithUsername: joi.alternatives().try(
joi.boolean(),
joi.object().keys({
allowEmailLogin: joi.boolean(),
requireEmail: joi.boolean(),
}),
),
maxLoginAttempts: joi.number(),
removeTokenFromResponses: joi.boolean().valid(true),
strategies: joi.array().items(
joi.object().keys({
name: joi.string().required(),
authenticate: joi.func().required(),
}),
),
tokenExpiration: joi.number(),
useAPIKey: joi.boolean(),
verify: joi.alternatives().try(
joi.boolean(),
joi.object().keys({
generateEmailHTML: joi.func(),
generateEmailSubject: joi.func(),
}),
),
}),
joi.boolean(),
),
custom: joi.object().pattern(joi.string(), joi.any()),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultSort: joi.string(),
disableDuplicate: joi.bool(),
endpoints: endpointsSchema,
fields: joi.array(),
graphQL: joi.alternatives().try(
joi.object().keys({
pluralName: joi.string(),
singularName: joi.string(),
}),
joi.boolean(),
),
hooks: joi.object({
afterChange: joi.array().items(joi.func()),
afterDelete: joi.array().items(joi.func()),
afterForgotPassword: joi.array().items(joi.func()),
afterLogin: joi.array().items(joi.func()),
afterLogout: joi.array().items(joi.func()),
afterMe: joi.array().items(joi.func()),
afterOperation: joi.array().items(joi.func()),
afterRead: joi.array().items(joi.func()),
afterRefresh: joi.array().items(joi.func()),
beforeChange: joi.array().items(joi.func()),
beforeDelete: joi.array().items(joi.func()),
beforeLogin: joi.array().items(joi.func()),
beforeOperation: joi.array().items(joi.func()),
beforeRead: joi.array().items(joi.func()),
beforeValidate: joi.array().items(joi.func()),
me: joi.array().items(joi.func()),
refresh: joi.array().items(joi.func()),
}),
labels: joi.object({
plural: joi
.alternatives()
.try(joi.func(), joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
singular: joi
.alternatives()
.try(joi.func(), joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
}),
timestamps: joi.boolean(),
typescript: joi.object().keys({
interface: joi.string(),
}),
upload: joi.alternatives().try(
joi.object({
adapter: joi.string(),
adminThumbnail: joi.alternatives().try(joi.string(), componentSchema),
crop: joi.bool(),
disableLocalStorage: joi.bool(),
externalFileHeaderFilter: joi.func(),
filesRequiredOnCreate: joi.bool(),
focalPoint: joi.bool(),
formatOptions: joi.object().keys({
format: joi.string(),
options: joi.object(),
}),
handlers: joi.array().items(joi.func()),
imageSizes: joi.array().items(
joi
.object()
.keys({
name: joi.string(),
crop: joi.string(), // TODO: add further specificity with joi.xor
height: joi.number().integer().allow(null),
width: joi.number().integer().allow(null),
})
.unknown(),
),
mimeTypes: joi.array().items(joi.string()),
modifyResponseHeaders: joi.func(),
resizeOptions: joi
.object()
.keys({
background: joi.string(),
fastShrinkOnLoad: joi.bool(),
fit: joi.string(),
height: joi.number().allow(null),
kernel: joi.string(),
position: joi.alternatives().try(joi.string(), joi.number()),
width: joi.number().allow(null),
withoutEnlargement: joi.bool(),
})
.allow(null),
staticDir: joi.string(),
tempFileDir: joi.string(),
trimOptions: joi.alternatives().try(
joi.object().keys({
format: joi.string(),
options: joi.object(),
}),
joi.string(),
joi.number(),
),
useTempFiles: joi.bool(),
}),
joi.boolean(),
),
versions: joi.alternatives().try(
joi.object({
drafts: joi.alternatives().try(
joi.object({
autosave: joi.alternatives().try(
joi.boolean(),
joi.object({
interval: joi.number(),
}),
),
validate: joi.boolean(),
}),
joi.boolean(),
),
maxPerDoc: joi.number(),
}),
joi.boolean(),
),
})
export default collectionSchema

View File

@@ -1,217 +0,0 @@
import joi from 'joi'
import { adminViewSchema } from './shared/adminViewSchema.js'
import { componentSchema, livePreviewSchema } from './shared/componentSchema.js'
import { openGraphSchema } from './shared/openGraphSchema.js'
const component = joi.alternatives().try(joi.object().unknown(), joi.func())
export const endpointsSchema = joi.alternatives().try(
joi.array().items(
joi.object({
custom: joi.object().pattern(joi.string(), joi.any()),
handler: joi.alternatives().try(joi.array().items(joi.func()), joi.func()),
method: joi
.string()
.valid('get', 'head', 'post', 'put', 'patch', 'delete', 'connect', 'options'),
path: joi.string(),
root: joi.bool(),
}),
),
joi.boolean(),
)
export default joi.object({
admin: joi.object({
autoLogin: joi.alternatives().try(
joi.object().keys({
email: joi.string(),
password: joi.string(),
prefillOnly: joi.boolean(),
username: joi.string(),
}),
joi.boolean(),
),
avatar: joi.alternatives().try(joi.string(), component),
buildPath: joi.string(),
components: joi.object().keys({
Nav: component,
actions: joi.array().items(component),
afterDashboard: joi.array().items(component),
afterLogin: joi.array().items(component),
afterNavLinks: joi.array().items(component),
beforeDashboard: joi.array().items(component),
beforeLogin: joi.array().items(component),
beforeNavLinks: joi.array().items(component),
graphics: joi.object({
Icon: component,
Logo: component,
}),
logout: joi.object({
Button: component,
}),
providers: joi.array().items(component),
views: joi.alternatives().try(
joi.object({
Account: joi.alternatives().try(component, adminViewSchema),
Dashboard: joi.alternatives().try(component, adminViewSchema),
}),
joi.object().pattern(joi.string(), component),
),
}),
custom: joi.object().pattern(joi.string(), joi.any()),
dateFormat: joi.string(),
disable: joi.bool(),
livePreview: joi.object({
...livePreviewSchema,
collections: joi.array().items(joi.string()),
globals: joi.array().items(joi.string()),
}),
meta: joi.object().keys({
defaultOGImageType: joi.string().valid('off', 'dynamic', 'static'),
description: joi.string(),
icons: joi.array().items(
joi.object().keys({
type: joi.string(),
color: joi.string(),
fetchPriority: joi.string().valid('auto', 'high', 'low'),
media: joi.string(),
rel: joi.string(),
sizes: joi.string(),
url: joi.string(),
}),
),
openGraph: openGraphSchema,
titleSuffix: joi.string(),
}),
routes: joi.object({
account: joi.string(),
createFirstUser: joi.string(),
forgot: joi.string(),
inactivity: joi.string(),
login: joi.string(),
logout: joi.string(),
reset: joi.string(),
unauthorized: joi.string(),
}),
user: joi.string(),
}),
bin: joi.array().items(
joi.object().keys({
key: joi.string(),
scriptPath: joi.string(),
}),
),
collections: joi.array(),
cookiePrefix: joi.string(),
cors: [
joi.string().valid('*'),
joi.array().items(joi.string()),
joi.object().keys({
headers: joi.array().items(joi.string()),
origins: [joi.string().valid('*'), joi.array().items(joi.string())],
}),
],
csrf: joi.array().items(joi.string().allow('')).sparse(),
custom: joi.object().pattern(joi.string(), joi.any()),
db: joi.any(),
debug: joi.boolean(),
defaultDepth: joi.number().min(0).max(30),
defaultMaxTextLength: joi.number(),
editor: joi
.object()
.optional()
.keys({
CellComponent: componentSchema.optional(),
FieldComponent: componentSchema.optional(),
afterReadPromise: joi.func().optional(),
outputSchema: joi.func().optional(),
populationPromise: joi.func().optional(),
validate: joi.func().required(),
})
.unknown(),
email: joi.alternatives().try(joi.object(), joi.func()),
endpoints: endpointsSchema,
globals: joi.array(),
graphQL: joi.object().keys({
disable: joi.boolean(),
disablePlaygroundInProduction: joi.boolean(),
maxComplexity: joi.number(),
mutations: joi.function(),
queries: joi.function(),
schemaOutputFile: joi.string(),
}),
hooks: joi.object().keys({
afterError: joi.func(),
}),
i18n: joi.object(),
indexSortableFields: joi.boolean(),
local: joi.boolean(),
localization: joi.alternatives().try(
joi.object().keys({
defaultLocale: joi.string(),
fallback: joi.boolean(),
localeCodes: joi.array().items(joi.string()),
locales: joi.alternatives().try(
joi.array().items(
joi.object().keys({
code: joi.string(),
fallbackLocale: joi.string(),
label: joi
.alternatives()
.try(
joi.object().pattern(joi.string(), [joi.string()]),
joi.string(),
joi.valid(false),
),
rtl: joi.boolean(),
toString: joi.func(),
}),
),
joi.array().items(joi.string()),
),
}),
joi.boolean(),
),
maxDepth: joi.number().min(0).max(100),
onInit: joi.func(),
plugins: joi.array().items(joi.func()),
routes: joi.object({
admin: joi.string(),
api: joi.string(),
graphQL: joi.string(),
graphQLPlayground: joi.string(),
}),
secret: joi.string(),
serverURL: joi
.string()
.uri()
.allow('')
.custom((value, helper) => {
const urlWithoutProtocol = value.split('//')[1]
if (!urlWithoutProtocol) {
return helper.message({
custom: 'You need to include either "https://" or "http://" in your serverURL.',
})
}
if (urlWithoutProtocol.indexOf('/') > -1) {
return helper.message({
custom:
'Your serverURL cannot have a path. It can only contain a protocol, a domain, and an optional port.',
})
}
return value
}),
sharp: joi.any(),
telemetry: joi.boolean(),
typescript: joi.object({
autoGenerate: joi.boolean(),
declare: joi.alternatives().try(joi.boolean(), joi.object({ ignoreTSError: joi.boolean() })),
outputFile: joi.string(),
schema: joi.array().items(joi.func()),
}),
upload: joi.object(),
})

View File

@@ -1,13 +0,0 @@
import joi from 'joi'
import { componentSchema } from './componentSchema.js'
export const adminViewSchema = joi.array().items(
joi.object().keys({
Component: componentSchema,
exact: joi.bool(),
path: joi.string().required(),
sensitive: joi.bool(),
strict: joi.bool(),
}),
)

View File

@@ -1,31 +0,0 @@
import joi from 'joi'
export const componentSchema = joi.alternatives().try(joi.object().unknown(), joi.func())
export const documentTabSchema = {
condition: joi.func(),
href: joi.alternatives().try(joi.string(), joi.func()).required(),
isActive: joi.alternatives().try(joi.func(), joi.boolean()),
label: joi.alternatives().try(joi.string(), joi.func()).required(),
newTab: joi.boolean(),
pillLabel: joi.alternatives().try(joi.string(), joi.func()),
}
export const customViewSchema = joi.object({
Component: componentSchema,
Tab: joi.alternatives().try(documentTabSchema, componentSchema),
actions: joi.array().items(componentSchema),
path: joi.string(),
})
export const livePreviewSchema = {
breakpoints: joi.array().items(
joi.object({
name: joi.string(),
height: joi.alternatives().try(joi.number(), joi.string()),
label: joi.string(),
width: joi.alternatives().try(joi.number(), joi.string()),
}),
),
url: joi.alternatives().try(joi.string(), joi.func()),
}

View File

@@ -1,16 +0,0 @@
import joi from 'joi'
const ogImageObj = joi.object({
type: joi.string(),
alt: joi.string(),
height: joi.alternatives().try(joi.string(), joi.number()),
url: joi.string(),
width: joi.alternatives().try(joi.string(), joi.number()),
})
export const openGraphSchema = joi.object({
description: joi.string(),
images: joi.alternatives().try(ogImageObj, joi.array().items(ogImageObj)),
siteName: joi.string(),
title: joi.string(),
})

View File

@@ -1,111 +0,0 @@
import type { ValidationResult } from 'joi'
import type { Logger } from 'pino'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { SanitizedConfig } from './types.js'
import collectionSchema from '../collections/config/schema.js'
import fieldSchema, { idField } from '../fields/config/schema.js'
import { fieldAffectsData } from '../fields/config/types.js'
import globalSchema from '../globals/config/schema.js'
import schema from './schema.js'
const validateFields = (
context: string,
entity: SanitizedCollectionConfig | SanitizedGlobalConfig,
): string[] => {
const errors: string[] = []
entity.fields.forEach((field) => {
let idResult: Partial<ValidationResult> = { error: null }
if (fieldAffectsData(field) && field.name === 'id') {
idResult = idField.validate(field, { abortEarly: false })
}
const result = fieldSchema.validate(field, { abortEarly: false })
if (idResult.error) {
idResult.error.details.forEach(({ message }) => {
errors.push(
`${context} "${entity.slug}" > Field${
fieldAffectsData(field) ? ` "${field.name}" >` : ''
} ${message}`,
)
})
}
if (result.error) {
result.error.details.forEach(({ message }) => {
errors.push(
`${context} "${entity.slug}" > Field${
fieldAffectsData(field) ? ` "${field.name}" >` : ''
} ${message}`,
)
})
}
})
return errors
}
const validateCollections = (collections: SanitizedCollectionConfig[]): string[] => {
const errors: string[] = []
collections.forEach((collection) => {
const result = collectionSchema.validate(collection, { abortEarly: false })
if (result.error) {
result.error.details.forEach(({ message }) => {
errors.push(`Collection "${collection.slug}" > ${message}`)
})
}
errors.push(...validateFields('Collection', collection))
})
return errors
}
const validateGlobals = (globals: SanitizedGlobalConfig[]): string[] => {
const errors: string[] = []
globals.forEach((global) => {
const result = globalSchema.validate(global, { abortEarly: false })
if (result.error) {
result.error.details.forEach(({ message }) => {
errors.push(`Globals "${global.slug}" > ${message}`)
})
}
errors.push(...validateFields('Global', global))
})
return errors
}
export const validateSchema = (config: SanitizedConfig, logger: Logger): SanitizedConfig => {
const result = schema.validate(config, {
abortEarly: false,
})
const nestedErrors = [
...validateCollections(config.collections),
...validateGlobals(config.globals),
]
if (result.error || nestedErrors.length > 0) {
logger.error(
`There were ${
(result.error?.details?.length || 0) + nestedErrors.length
} errors validating your Payload config`,
)
let i = 0
if (result.error) {
result.error.details.forEach(({ message }) => {
i += 1
logger.error(`${i}: ${message}`)
})
}
nestedErrors.forEach((message) => {
i += 1
logger.error(`${i}: ${message}`)
})
process.exit(1)
}
return result.value
}

View File

@@ -1,592 +0,0 @@
import joi from 'joi'
import { componentSchema } from '../../config/shared/componentSchema.js'
export const baseAdminComponentFields = joi
.object()
.keys({
Cell: componentSchema,
Description: componentSchema,
Field: componentSchema,
Filter: componentSchema,
})
.default({})
export const baseAdminFields = joi.object().keys({
className: joi.string(),
components: baseAdminComponentFields,
condition: joi.func(),
custom: joi.object().pattern(joi.string(), joi.any()),
description: joi
.alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()]), joi.function()),
disableBulkEdit: joi.boolean().default(false),
disableListColumn: joi.boolean().default(false),
disableListFilter: joi.boolean().default(false),
disabled: joi.boolean().default(false),
hidden: joi.boolean().default(false),
initCollapsed: joi.boolean().default(false),
position: joi.string().valid('sidebar'),
readOnly: joi.boolean().default(false),
style: joi.object().unknown(),
width: joi.string(),
})
export const baseField = joi
.object()
.keys({
access: joi.object().keys({
create: joi.func(),
read: joi.func(),
update: joi.func(),
}),
admin: baseAdminFields.default(),
custom: joi.object().pattern(joi.string(), joi.any()),
hidden: joi.boolean().default(false),
hooks: joi
.object()
.keys({
afterChange: joi.array().items(joi.func()).default([]),
afterRead: joi.array().items(joi.func()).default([]),
beforeChange: joi.array().items(joi.func()).default([]),
beforeDuplicate: joi.array().items(joi.func()).default([]),
beforeValidate: joi.array().items(joi.func()).default([]),
})
.default(),
index: joi.boolean().default(false),
label: joi
.alternatives()
.try(
joi.func(),
joi.object().pattern(joi.string(), [joi.string()]),
joi.string(),
joi.valid(false),
),
localized: joi.boolean().default(false),
required: joi.boolean().default(false),
saveToJWT: joi.alternatives().try(joi.boolean(), joi.string()).default(false),
typescriptSchema: joi.array().items(joi.func()),
unique: joi.boolean().default(false),
validate: joi.func(),
})
.default()
export const idField = baseField.keys({
name: joi.string().valid('id'),
type: joi.string().valid('text', 'number'),
localized: joi.invalid(true),
required: joi.not(false, 0).default(true),
})
export const text = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('text').required(),
admin: baseAdminFields.keys({
autoComplete: joi.string(),
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
placeholder: joi
.alternatives()
.try(joi.object().pattern(joi.string(), [joi.string()]), joi.string()),
rtl: joi.boolean(),
}),
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
hasMany: joi.boolean().default(false),
maxLength: joi.number(),
maxRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
minLength: joi.number(),
minRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
})
export const number = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('number').required(),
admin: baseAdminFields.keys({
autoComplete: joi.string(),
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
afterInput: joi
.array()
.items(componentSchema)
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
beforeInput: joi
.array()
.items(componentSchema)
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
}),
placeholder: joi.string(),
step: joi.number(),
}),
defaultValue: joi
.alternatives()
.try(
joi.number(),
joi.func(),
joi.array().when('hasMany', { not: true, then: joi.forbidden() }),
),
hasMany: joi.boolean().default(false),
max: joi.number(),
maxRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
min: joi.number(),
minRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
})
export const textarea = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('textarea').required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
placeholder: joi.string(),
rows: joi.number(),
rtl: joi.boolean(),
}),
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
maxLength: joi.number(),
minLength: joi.number(),
})
export const email = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('email').required(),
admin: baseAdminFields.keys({
autoComplete: joi.string(),
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
placeholder: joi.string(),
}),
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
maxLength: joi.number(),
minLength: joi.number(),
})
export const code = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('code').required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
}),
editorOptions: joi.object().unknown(), // Editor['options'] @monaco-editor/react
language: joi.string(),
}),
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
})
export const json = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('json').required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
}),
editorOptions: joi.object().unknown(), // Editor['options'] @monaco-editor/react
}),
defaultValue: joi.alternatives().try(joi.array(), joi.func(), joi.object()),
jsonSchema: joi.object().unknown(),
})
export const select = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('select').required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
}),
isClearable: joi.boolean().default(false),
isSortable: joi.boolean().default(false),
}),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultValue: joi
.alternatives()
.try(joi.string().allow(''), joi.array().items(joi.string().allow('')), joi.func()),
enumName: joi.alternatives().try(joi.string(), joi.func()),
hasMany: joi.boolean().default(false),
options: joi
.array()
.min(1)
.items(
joi.alternatives().try(
joi.string(),
joi.object({
label: joi
.alternatives()
.try(joi.func(), joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
value: joi.string().required().allow(''),
}),
),
)
.required(),
})
export const radio = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('radio').required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
}),
layout: joi.string().valid('vertical', 'horizontal'),
}),
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
enumName: joi.alternatives().try(joi.string(), joi.func()),
options: joi
.array()
.min(1)
.items(
joi.alternatives().try(
joi.string(),
joi.object({
label: joi
.alternatives()
.try(joi.func(), joi.string(), joi.object().pattern(joi.string(), [joi.string()]))
.required(),
value: joi.string().required().allow(''),
}),
),
)
.required(),
})
export const row = baseField.keys({
type: joi.string().valid('row').required(),
admin: baseAdminFields.default(),
fields: joi.array().items(joi.link('#field')),
})
export const collapsible = baseField.keys({
type: joi.string().valid('collapsible').required(),
admin: baseAdminFields
.keys({
components: baseAdminComponentFields
.keys({
RowLabel: componentSchema.optional(),
})
.default({}),
})
.default({}),
fields: joi.array().items(joi.link('#field')),
label: joi.alternatives().conditional('admin.components.RowLabel', {
is: joi.exist(),
otherwise: joi
.alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()]), joi.function())
.required(),
then: joi
.alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()]), joi.function())
.optional(),
}),
})
const tab = baseField.keys({
name: joi.string().when('localized', { is: joi.exist(), then: joi.required() }),
description: joi.alternatives().try(joi.string(), componentSchema),
fields: joi.array().items(joi.link('#field')).required(),
interfaceName: joi.string().when('name', { not: joi.exist(), then: joi.forbidden() }),
label: joi
.alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()]))
.when('name', { is: joi.not(), then: joi.required() }),
localized: joi.boolean(),
saveToJWT: joi.alternatives().try(joi.boolean(), joi.string()),
})
export const tabs = baseField.keys({
type: joi.string().valid('tabs').required(),
admin: baseAdminFields.keys({
description: joi.forbidden(),
}),
fields: joi.forbidden(),
localized: joi.forbidden(),
tabs: joi.array().items(tab).required(),
})
export const group = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('group').required(),
admin: baseAdminFields.keys({
hideGutter: joi.boolean().default(true),
}),
defaultValue: joi.alternatives().try(joi.object(), joi.func()),
fields: joi.array().items(joi.link('#field')),
interfaceName: joi.string(),
})
export const array = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('array').required(),
admin: baseAdminFields
.keys({
components: baseAdminComponentFields
.keys({
RowLabel: componentSchema,
})
.default({}),
isSortable: joi.boolean(),
})
.default({}),
dbName: joi.alternatives().try(joi.string(), joi.func()),
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()),
fields: joi.array().items(joi.link('#field')).required(),
interfaceName: joi.string(),
labels: joi.object({
plural: joi
.alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
singular: joi
.alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
}),
maxRows: joi.number(),
minRows: joi.number(),
})
export const upload = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('upload').required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
}),
}),
defaultValue: joi.alternatives().try(joi.object(), joi.func()),
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
maxDepth: joi.number(),
relationTo: joi.string().required(),
})
export const checkbox = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('checkbox').required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.boolean(), joi.func()),
})
export const point = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('point').required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.array().items(joi.number()).max(2).min(2), joi.func()),
})
export const relationship = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('relationship').required(),
admin: baseAdminFields.keys({
allowCreate: joi.boolean().default(true),
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
}),
isSortable: joi.boolean().default(false),
sortOptions: joi.alternatives().conditional(joi.ref('...relationTo'), {
is: joi.string(),
otherwise: joi.object().pattern(joi.string(), joi.string()),
then: joi.string(),
}),
}),
defaultValue: joi.alternatives().try(joi.func()),
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
hasMany: joi.boolean().default(false),
max: joi
.number()
.when('hasMany', { is: joi.not(true), then: joi.forbidden() })
.warning('deprecated', { message: 'Use maxRows instead.' }),
maxDepth: joi.number(),
maxRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
min: joi
.number()
.when('hasMany', { is: joi.not(true), then: joi.forbidden() })
.warning('deprecated', { message: 'Use minRows instead.' }),
minRows: joi.number().when('hasMany', { is: joi.not(true), then: joi.forbidden() }),
relationTo: joi.alternatives().try(joi.string().required(), joi.array().items(joi.string())),
})
export const blocks = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('blocks').required(),
admin: baseAdminFields
.keys({
isSortable: joi.boolean(),
})
.default({}),
blocks: joi
.array()
.items(
joi.object({
slug: joi.string().required(),
admin: joi.object().keys({
components: joi.object().keys({
Label: componentSchema,
}),
custom: joi.object().pattern(joi.string(), joi.any()),
}),
custom: joi.object().pattern(joi.string(), joi.any()),
dbName: joi.alternatives().try(joi.string(), joi.func()),
fields: joi.array().items(joi.link('#field')),
graphQL: joi.object().keys({
singularName: joi.string(),
}),
imageAltText: joi.string(),
imageURL: joi.string(),
interfaceName: joi.string(),
labels: joi.object({
plural: joi
.alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
singular: joi
.alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
}),
}),
)
.required(),
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func()),
labels: joi.object({
plural: joi
.alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
singular: joi
.alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
}),
maxRows: joi.number(),
minRows: joi.number(),
})
export const richText = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('richText').required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
}),
}),
defaultValue: joi.alternatives().try(joi.array().items(joi.object()), joi.func(), joi.object()),
editor: joi
.object()
.keys({
CellComponent: componentSchema.optional(),
FieldComponent: componentSchema.optional(),
afterReadPromise: joi.func().optional(),
graphQLPopulationPromises: joi.func().optional(),
outputSchema: joi.func().optional(),
validate: joi.func().required(),
})
.unknown(),
maxDepth: joi.number(),
})
export const date = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('date').required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Error: componentSchema,
Label: componentSchema,
afterInput: joi.array().items(componentSchema),
beforeInput: joi.array().items(componentSchema),
}),
date: joi.object({
displayFormat: joi.string(),
maxDate: joi.date(),
maxTime: joi.date(),
minDate: joi.date(),
minTime: joi.date(),
monthsToShow: joi.number(),
overrides: joi.object().unknown(),
pickerAppearance: joi.string(),
timeFormat: joi.string(),
timeIntervals: joi.number(),
}),
placeholder: joi.string(),
}),
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
})
export const ui = joi.object().keys({
name: joi.string().required(),
type: joi.string().valid('ui').required(),
admin: joi
.object()
.keys({
components: joi
.object()
.keys({
Cell: componentSchema,
Field: componentSchema,
})
.default({}),
condition: joi.func(),
custom: joi.object().pattern(joi.string(), joi.any()),
disableListColumn: joi.boolean().default(false),
position: joi.string().valid('sidebar'),
width: joi.string(),
})
.default(),
custom: joi.object().pattern(joi.string(), joi.any()),
label: joi.alternatives().try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
})
const fieldSchema = joi
.alternatives()
.try(
text,
number,
textarea,
email,
code,
json,
select,
group,
array,
row,
collapsible,
tabs,
radio,
relationship,
checkbox,
upload,
richText,
blocks,
date,
point,
ui,
)
.id('field')
export default fieldSchema

View File

@@ -1,104 +0,0 @@
import joi from 'joi'
import { endpointsSchema } from '../../config/schema.js'
import {
componentSchema,
customViewSchema,
livePreviewSchema,
} from '../../config/shared/componentSchema.js'
import { openGraphSchema } from '../../config/shared/openGraphSchema.js'
const globalSchema = joi
.object()
.keys({
slug: joi.string().required(),
access: joi.object({
read: joi.func(),
readVersions: joi.func(),
update: joi.func(),
}),
admin: joi.object({
components: joi.object({
elements: joi.object({
Description: componentSchema,
PreviewButton: componentSchema,
PublishButton: componentSchema,
SaveButton: componentSchema,
SaveDraftButton: componentSchema,
}),
views: joi.object({
Edit: joi.alternatives().try(
componentSchema,
joi.object({
API: joi.alternatives().try(componentSchema, customViewSchema),
Default: joi.alternatives().try(componentSchema, customViewSchema),
Preview: joi.alternatives().try(componentSchema, customViewSchema),
Version: joi.alternatives().try(componentSchema, customViewSchema),
Versions: joi.alternatives().try(componentSchema, customViewSchema),
// Relationships
// References
}),
),
}),
}),
custom: joi.object().pattern(joi.string(), joi.any()),
description: joi
.alternatives()
.try(joi.func(), joi.object().pattern(joi.string(), [joi.string()]), joi.string()),
group: joi
.alternatives()
.try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
hidden: joi.alternatives().try(joi.boolean(), joi.func()),
hideAPIURL: joi.boolean(),
livePreview: joi.object(livePreviewSchema),
meta: joi.object({
description: joi.string(),
openGraph: openGraphSchema,
}),
preview: joi.func(),
}),
custom: joi.object().pattern(joi.string(), joi.any()),
dbName: joi.alternatives().try(joi.string(), joi.func()),
endpoints: endpointsSchema,
fields: joi.array(),
graphQL: joi.alternatives().try(
joi.object().keys({
name: joi.string(),
}),
joi.boolean(),
),
hooks: joi.object({
afterChange: joi.array().items(joi.func()),
afterRead: joi.array().items(joi.func()),
beforeChange: joi.array().items(joi.func()),
beforeRead: joi.array().items(joi.func()),
beforeValidate: joi.array().items(joi.func()),
}),
label: joi
.alternatives()
.try(joi.func(), joi.string(), joi.object().pattern(joi.string(), [joi.string()])),
typescript: joi.object().keys({
interface: joi.string(),
}),
versions: joi.alternatives().try(
joi.object({
drafts: joi.alternatives().try(
joi.object({
autosave: joi.alternatives().try(
joi.boolean(),
joi.object({
interval: joi.number(),
}),
),
validate: joi.boolean(),
}),
joi.boolean(),
),
max: joi.number(),
}),
joi.boolean(),
),
})
.unknown()
export default globalSchema

View File

@@ -57,7 +57,6 @@ import { decrypt, encrypt } from './auth/crypto.js'
import { APIKeyAuthentication } from './auth/strategies/apiKey.js'
import { JWTAuthentication } from './auth/strategies/jwt.js'
import localOperations from './collections/operations/local/index.js'
import { validateSchema } from './config/validate.js'
import { consoleEmailAdapter } from './email/consoleEmailAdapter.js'
import { fieldAffectsData } from './fields/config/types.js'
import localGlobalOperations from './globals/operations/local/index.js'
@@ -488,10 +487,6 @@ export class BasePayload {
this.config = await options.config
if (process.env.NODE_ENV !== 'production') {
validateSchema(this.config, this.logger)
}
if (!this.config.secret) {
throw new Error('Error: missing secret key. A secret key is needed to secure Payload.')
}

44
pnpm-lock.yaml generated
View File

@@ -694,9 +694,6 @@ importers:
image-size:
specifier: ^1.1.1
version: 1.1.1
joi:
specifier: ^17.12.1
version: 17.13.3
json-schema-to-typescript:
specifier: 11.0.3
version: 11.0.3
@@ -743,9 +740,6 @@ importers:
'@types/express-fileupload':
specifier: 1.4.1
version: 1.4.1
'@types/joi':
specifier: 14.3.4
version: 14.3.4
'@types/json-schema':
specifier: 7.0.15
version: 7.0.15
@@ -5107,16 +5101,6 @@ packages:
- encoding
- supports-color
/@hapi/hoek@9.3.0:
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
dev: false
/@hapi/topo@5.1.0:
resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==}
dependencies:
'@hapi/hoek': 9.3.0
dev: false
/@humanwhocodes/module-importer@1.0.1:
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
engines: {node: '>=12.22'}
@@ -6174,20 +6158,6 @@ packages:
dependencies:
'@sentry/types': 7.118.0
/@sideway/address@4.1.5:
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
dependencies:
'@hapi/hoek': 9.3.0
dev: false
/@sideway/formula@3.0.1:
resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==}
dev: false
/@sideway/pinpoint@2.0.0:
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
dev: false
/@sinclair/typebox@0.27.8:
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
@@ -7011,10 +6981,6 @@ packages:
pretty-format: 29.7.0
dev: true
/@types/joi@14.3.4:
resolution: {integrity: sha512-1TQNDJvIKlgYXGNIABfgFp9y0FziDpuGrd799Q5RcnsDu+krD+eeW/0Fs5PHARvWWFelOhIG2OPCo6KbadBM4A==}
dev: true
/@types/jsdom@20.0.1:
resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==}
dependencies:
@@ -12057,16 +12023,6 @@ packages:
hasBin: true
dev: true
/joi@17.13.3:
resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==}
dependencies:
'@hapi/hoek': 9.3.0
'@hapi/topo': 5.1.0
'@sideway/address': 4.1.5
'@sideway/formula': 3.0.1
'@sideway/pinpoint': 2.0.0
dev: false
/joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}