Skip to content

Commit

Permalink
feat: extracts buildFormState logic from endpoint for reuse (#6501)
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobsfletch authored May 29, 2024
1 parent 4e0dfd4 commit 321e97f
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 215 deletions.
1 change: 0 additions & 1 deletion packages/next/src/exports/utilities.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
220 changes: 15 additions & 205 deletions packages/next/src/routes/rest/buildFormState.ts
Original file line number Diff line number Diff line change
@@ -1,106 +1,29 @@
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(),
req,
})

try {
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) {
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)
}
const result = await buildFormStateFn({ req })

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,
Expand All @@ -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 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
}
if (err.message === 'Unauthorized') {
return Response.json(null, {
headers,
status: httpStatus.UNAUTHORIZED,
})
}

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({
Expand Down
1 change: 0 additions & 1 deletion packages/payload/src/fields/getDefaultValue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { User } from '../auth/index.js'
import type { PayloadRequestWithData } from '../types/index.js'

import { deepCopyObject } from '../utilities/deepCopyObject.js'
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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) || []
Expand Down
Loading

0 comments on commit 321e97f

Please sign in to comment.