feat: extracts buildFormState logic from endpoint for reuse (#6501)

This commit is contained in:
Jacob Fletcher
2024-05-29 12:51:16 -04:00
committed by GitHub
parent 4e0dfd410d
commit 321e97f9fe
10 changed files with 238 additions and 215 deletions

View File

@@ -1,6 +1,5 @@
export { addDataAndFileToRequest } from '../utilities/addDataAndFileToRequest.js'
export { addLocalesToRequestFromData, sanitizeLocales } from '../utilities/addLocalesToRequest.js'
export { traverseFields } from '../utilities/buildFieldSchemaMap/traverseFields.js'
export { createPayloadRequest } from '../utilities/createPayloadRequest.js'
export { getNextRequestI18n } from '../utilities/getNextRequestI18n.js'
export { getPayloadHMR, reload } from '../utilities/getPayloadHMR.js'

View File

@@ -1,33 +1,11 @@
import type { BuildFormStateArgs } from '@payloadcms/ui/forms/buildStateFromSchema'
import type { DocumentPreferences, Field, PayloadRequestWithData, TypeWithID } from 'payload/types'
import type { PayloadRequestWithData } from 'payload/types'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import { reduceFieldsToValues } from '@payloadcms/ui/utilities/reduceFieldsToValues'
import { buildFormState as buildFormStateFn } from '@payloadcms/ui/utilities/buildFormState'
import httpStatus from 'http-status'
import type { FieldSchemaMap } from '../../utilities/buildFieldSchemaMap/types.js'
import { buildFieldSchemaMap } from '../../utilities/buildFieldSchemaMap/index.js'
import { headersWithCors } from '../../utilities/headersWithCors.js'
import { routeError } from './routeError.js'
let cached = global._payload_fieldSchemaMap
if (!cached) {
// eslint-disable-next-line no-multi-assign
cached = global._payload_fieldSchemaMap = null
}
export const getFieldSchemaMap = (req: PayloadRequestWithData): FieldSchemaMap => {
if (cached && process.env.NODE_ENV !== 'development') {
return cached
}
cached = buildFieldSchemaMap(req)
return cached
}
export const buildFormState = async ({ req }: { req: PayloadRequestWithData }) => {
const headers = headersWithCors({
headers: new Headers(),
@@ -35,72 +13,17 @@ export const buildFormState = async ({ req }: { req: PayloadRequestWithData }) =
})
try {
const reqData: BuildFormStateArgs = req.data as BuildFormStateArgs
const { collectionSlug, formState, globalSlug, locale, operation, schemaPath } = reqData
const result = await buildFormStateFn({ req })
const incomingUserSlug = req.user?.collection
const adminUserSlug = req.payload.config.admin.user
// If we have a user slug, test it against the functions
if (incomingUserSlug) {
const adminAccessFunction = req.payload.collections[incomingUserSlug].config.access?.admin
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction({ req })
if (!canAccessAdmin) {
return Response.json(null, {
headers,
status: httpStatus.UNAUTHORIZED,
})
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
return Response.json(null, {
headers,
status: httpStatus.UNAUTHORIZED,
})
}
} else {
const hasUsers = await req.payload.find({
collection: adminUserSlug,
depth: 0,
limit: 1,
pagination: false,
})
// If there are users, we should not allow access because of /create-first-user
if (hasUsers.docs.length) {
return Response.json(null, {
headers,
status: httpStatus.UNAUTHORIZED,
})
}
}
const fieldSchemaMap = getFieldSchemaMap(req)
const id = collectionSlug ? reqData.id : undefined
const schemaPathSegments = schemaPath.split('.')
let fieldSchema: Field[]
if (schemaPathSegments.length === 1) {
if (req.payload.collections[schemaPath]) {
fieldSchema = req.payload.collections[schemaPath].config.fields
} else {
fieldSchema = req.payload.config.globals.find(
(global) => global.slug === schemaPath,
)?.fields
}
} else if (fieldSchemaMap.has(schemaPath)) {
fieldSchema = fieldSchemaMap.get(schemaPath)
}
if (!fieldSchema) {
return Response.json(result, {
headers,
status: httpStatus.OK,
})
} catch (err) {
if (err.message === 'Could not find field schema for given path') {
return Response.json(
{
message: 'Could not find field schema for given path',
message: err.message,
},
{
headers,
@@ -109,126 +32,13 @@ export const buildFormState = async ({ req }: { req: PayloadRequestWithData }) =
)
}
let docPreferences = reqData.docPreferences
let data = reqData.data
const promises: {
data?: Promise<void>
preferences?: Promise<void>
} = {}
// If the request does not include doc preferences,
// we should fetch them. This is useful for DocumentInfoProvider
// as it reduces the amount of client-side fetches necessary
// when we fetch data for the Edit view
if (!docPreferences) {
let preferencesKey
if (collectionSlug && id) {
preferencesKey = `collection-${collectionSlug}-${id}`
}
if (globalSlug) {
preferencesKey = `global-${globalSlug}`
}
if (preferencesKey) {
const fetchPreferences = async () => {
const preferencesResult = (await req.payload.find({
collection: 'payload-preferences',
depth: 0,
limit: 1,
where: {
key: {
equals: preferencesKey,
},
},
})) as unknown as { docs: { value: DocumentPreferences }[] }
if (preferencesResult?.docs?.[0]?.value) docPreferences = preferencesResult.docs[0].value
}
promises.preferences = fetchPreferences()
}
if (err.message === 'Unauthorized') {
return Response.json(null, {
headers,
status: httpStatus.UNAUTHORIZED,
})
}
// If there is a form state,
// then we can deduce data from that form state
if (formState) data = reduceFieldsToValues(formState, true)
// If we do not have data at this point,
// we can fetch it. This is useful for DocumentInfoProvider
// to reduce the amount of fetches required
if (!data) {
const fetchData = async () => {
let resolvedData: TypeWithID
if (collectionSlug && id) {
resolvedData = await req.payload.findByID({
id,
collection: collectionSlug,
depth: 0,
draft: true,
fallbackLocale: null,
locale,
overrideAccess: false,
user: req.user,
})
}
if (globalSlug && schemaPath === globalSlug) {
resolvedData = await req.payload.findGlobal({
slug: globalSlug,
depth: 0,
draft: true,
fallbackLocale: null,
locale,
overrideAccess: false,
user: req.user,
})
}
data = resolvedData
}
promises.data = fetchData()
}
if (Object.keys(promises).length > 0) {
await Promise.all(Object.values(promises))
}
const result = await buildStateFromSchema({
id,
data,
fieldSchema,
operation,
preferences: docPreferences || { fields: {} },
req,
})
// Maintain form state of auth / upload fields
if (collectionSlug && formState) {
if (req.payload.collections[collectionSlug]?.config?.upload && formState.file) {
result.file = formState.file
}
if (
req.payload.collections[collectionSlug]?.config?.auth &&
!req.payload.collections[collectionSlug].config.auth.disableLocalStrategy
) {
if (formState.password) result.password = formState.password
if (formState['confirm-password'])
result['confirm-password'] = formState['confirm-password']
if (formState.email) result.email = formState.email
}
}
return Response.json(result, {
headers,
status: httpStatus.OK,
})
} catch (err) {
req.payload.logger.error({ err, msg: `There was an error building form state` })
return routeError({

View File

@@ -1,4 +1,3 @@
import type { User } from '../auth/index.js'
import type { PayloadRequestWithData } from '../types/index.js'
import { deepCopyObject } from '../utilities/deepCopyObject.js'

View File

@@ -1,7 +1,7 @@
import type { Config } from 'payload/config'
import type { Block, BlockField, Field } from 'payload/types'
import { traverseFields } from '@payloadcms/next/utilities'
import { traverseFields } from '@payloadcms/ui/utilities/buildFieldSchemaMap/traverseFields'
import { baseBlockFields, sanitizeFields } from 'payload/config'
import { fieldsToJSONSchema, formatLabels } from 'payload/utilities'

View File

@@ -1,7 +1,7 @@
import type { Config, SanitizedConfig } from 'payload/config'
import type { Field } from 'payload/types'
import { traverseFields } from '@payloadcms/next/utilities'
import { traverseFields } from '@payloadcms/ui/utilities/buildFieldSchemaMap/traverseFields'
import { sanitizeFields } from 'payload/config'
import { deepCopyObject } from 'payload/utilities'

View File

@@ -1,7 +1,7 @@
import type { Config } from 'payload/config'
import type { Field, FileData, FileSize, Payload, TypeWithID } from 'payload/types'
import { traverseFields } from '@payloadcms/next/utilities'
import { traverseFields } from '@payloadcms/ui/utilities/buildFieldSchemaMap/traverseFields'
import { sanitizeFields } from 'payload/config'
import type { FeatureProviderProviderServer } from '../types.js'

View File

@@ -1,13 +1,16 @@
import type { PayloadRequestWithData } from 'payload/types'
import type { I18n } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types'
import type { FieldSchemaMap } from './types.js'
import { traverseFields } from './traverseFields.js'
export const buildFieldSchemaMap = ({
i18n,
payload: { config },
}: PayloadRequestWithData): FieldSchemaMap => {
export const buildFieldSchemaMap = (args: {
config: SanitizedConfig
i18n: I18n
}): FieldSchemaMap => {
const { config, i18n } = args
const result: FieldSchemaMap = new Map()
const validRelationships = config.collections.map((c) => c.slug) || []

View File

@@ -0,0 +1,212 @@
import type { BuildFormStateArgs } from '@payloadcms/ui/forms/buildStateFromSchema'
import type {
DocumentPreferences,
Field,
FormState,
PayloadRequestWithData,
TypeWithID,
} from 'payload/types'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import { reduceFieldsToValues } from '@payloadcms/ui/utilities/reduceFieldsToValues'
import type { FieldSchemaMap } from './buildFieldSchemaMap/types.js'
import { buildFieldSchemaMap } from './buildFieldSchemaMap/index.js'
let cached = global._payload_fieldSchemaMap
if (!cached) {
// eslint-disable-next-line no-multi-assign
cached = global._payload_fieldSchemaMap = null
}
export const getFieldSchemaMap = (req: PayloadRequestWithData): FieldSchemaMap => {
if (cached && process.env.NODE_ENV !== 'development') {
return cached
}
cached = buildFieldSchemaMap({
config: req.payload.config,
i18n: req.i18n,
})
return cached
}
export const buildFormState = async ({
req,
}: {
req: PayloadRequestWithData
}): Promise<FormState> => {
const reqData: BuildFormStateArgs = req.data as BuildFormStateArgs
const { collectionSlug, formState, globalSlug, locale, operation, schemaPath } = reqData
const incomingUserSlug = req.user?.collection
const adminUserSlug = req.payload.config.admin.user
// If we have a user slug, test it against the functions
if (incomingUserSlug) {
const adminAccessFunction = req.payload.collections[incomingUserSlug].config.access?.admin
// Run the admin access function from the config if it exists
if (adminAccessFunction) {
const canAccessAdmin = await adminAccessFunction({ req })
if (!canAccessAdmin) {
throw new Error('Unauthorized')
}
// Match the user collection to the global admin config
} else if (adminUserSlug !== incomingUserSlug) {
throw new Error('Unauthorized')
}
} else {
const hasUsers = await req.payload.find({
collection: adminUserSlug,
depth: 0,
limit: 1,
pagination: false,
})
// If there are users, we should not allow access because of /create-first-user
if (hasUsers.docs.length) {
throw new Error('Unauthorized')
}
}
const fieldSchemaMap = getFieldSchemaMap(req)
const id = collectionSlug ? reqData.id : undefined
const schemaPathSegments = schemaPath.split('.')
let fieldSchema: Field[]
if (schemaPathSegments.length === 1) {
if (req.payload.collections[schemaPath]) {
fieldSchema = req.payload.collections[schemaPath].config.fields
} else {
fieldSchema = req.payload.config.globals.find((global) => global.slug === schemaPath)?.fields
}
} else if (fieldSchemaMap.has(schemaPath)) {
fieldSchema = fieldSchemaMap.get(schemaPath)
}
if (!fieldSchema) {
throw new Error('Could not find field schema for given path')
}
let docPreferences = reqData.docPreferences
let data = reqData.data
const promises: {
data?: Promise<void>
preferences?: Promise<void>
} = {}
// If the request does not include doc preferences,
// we should fetch them. This is useful for DocumentInfoProvider
// as it reduces the amount of client-side fetches necessary
// when we fetch data for the Edit view
if (!docPreferences) {
let preferencesKey
if (collectionSlug && id) {
preferencesKey = `collection-${collectionSlug}-${id}`
}
if (globalSlug) {
preferencesKey = `global-${globalSlug}`
}
if (preferencesKey) {
const fetchPreferences = async () => {
const preferencesResult = (await req.payload.find({
collection: 'payload-preferences',
depth: 0,
limit: 1,
where: {
key: {
equals: preferencesKey,
},
},
})) as unknown as { docs: { value: DocumentPreferences }[] }
if (preferencesResult?.docs?.[0]?.value) docPreferences = preferencesResult.docs[0].value
}
promises.preferences = fetchPreferences()
}
}
// If there is a form state,
// then we can deduce data from that form state
if (formState) data = reduceFieldsToValues(formState, true)
// If we do not have data at this point,
// we can fetch it. This is useful for DocumentInfoProvider
// to reduce the amount of fetches required
if (!data) {
const fetchData = async () => {
let resolvedData: TypeWithID
if (collectionSlug && id) {
resolvedData = await req.payload.findByID({
id,
collection: collectionSlug,
depth: 0,
draft: true,
fallbackLocale: null,
locale,
overrideAccess: false,
user: req.user,
})
}
if (globalSlug && schemaPath === globalSlug) {
resolvedData = await req.payload.findGlobal({
slug: globalSlug,
depth: 0,
draft: true,
fallbackLocale: null,
locale,
overrideAccess: false,
user: req.user,
})
}
data = resolvedData
}
promises.data = fetchData()
}
if (Object.keys(promises).length > 0) {
await Promise.all(Object.values(promises))
}
const result = await buildStateFromSchema({
id,
data,
fieldSchema,
operation,
preferences: docPreferences || { fields: {} },
req,
})
// Maintain form state of auth / upload fields
if (collectionSlug && formState) {
if (req.payload.collections[collectionSlug]?.config?.upload && formState.file) {
result.file = formState.file
}
if (
req.payload.collections[collectionSlug]?.config?.auth &&
!req.payload.collections[collectionSlug].config.auth.disableLocalStrategy
) {
if (formState.password) result.password = formState.password
if (formState['confirm-password']) result['confirm-password'] = formState['confirm-password']
if (formState.email) result.email = formState.email
}
}
return result
}