diff --git a/packages/next/src/exports/utilities.ts b/packages/next/src/exports/utilities.ts index 95b657a0c2a..a333611fc42 100644 --- a/packages/next/src/exports/utilities.ts +++ b/packages/next/src/exports/utilities.ts @@ -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' diff --git a/packages/next/src/routes/rest/buildFormState.ts b/packages/next/src/routes/rest/buildFormState.ts index c37ea7db8fd..feab12d682b 100644 --- a/packages/next/src/routes/rest/buildFormState.ts +++ b/packages/next/src/routes/rest/buildFormState.ts @@ -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 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, @@ -109,126 +32,13 @@ export const buildFormState = async ({ req }: { req: PayloadRequestWithData }) = ) } - let docPreferences = reqData.docPreferences - let data = reqData.data - - const promises: { - data?: Promise - preferences?: Promise - } = {} - - // 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({ diff --git a/packages/payload/src/fields/getDefaultValue.ts b/packages/payload/src/fields/getDefaultValue.ts index b11539f7a63..ac463b7a1ce 100644 --- a/packages/payload/src/fields/getDefaultValue.ts +++ b/packages/payload/src/fields/getDefaultValue.ts @@ -1,4 +1,3 @@ -import type { User } from '../auth/index.js' import type { PayloadRequestWithData } from '../types/index.js' import { deepCopyObject } from '../utilities/deepCopyObject.js' diff --git a/packages/richtext-lexical/src/field/features/blocks/feature.server.ts b/packages/richtext-lexical/src/field/features/blocks/feature.server.ts index 493884601e9..14e8df6ddd2 100644 --- a/packages/richtext-lexical/src/field/features/blocks/feature.server.ts +++ b/packages/richtext-lexical/src/field/features/blocks/feature.server.ts @@ -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' diff --git a/packages/richtext-lexical/src/field/features/link/feature.server.ts b/packages/richtext-lexical/src/field/features/link/feature.server.ts index 615f21f13c5..86e704749d0 100644 --- a/packages/richtext-lexical/src/field/features/link/feature.server.ts +++ b/packages/richtext-lexical/src/field/features/link/feature.server.ts @@ -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' diff --git a/packages/richtext-lexical/src/field/features/upload/feature.server.ts b/packages/richtext-lexical/src/field/features/upload/feature.server.ts index 7d09b69deb2..a744647ff73 100644 --- a/packages/richtext-lexical/src/field/features/upload/feature.server.ts +++ b/packages/richtext-lexical/src/field/features/upload/feature.server.ts @@ -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' diff --git a/packages/next/src/utilities/buildFieldSchemaMap/index.ts b/packages/ui/src/utilities/buildFieldSchemaMap/index.ts similarity index 73% rename from packages/next/src/utilities/buildFieldSchemaMap/index.ts rename to packages/ui/src/utilities/buildFieldSchemaMap/index.ts index 3659d241881..9a53fbaa19e 100644 --- a/packages/next/src/utilities/buildFieldSchemaMap/index.ts +++ b/packages/ui/src/utilities/buildFieldSchemaMap/index.ts @@ -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) || [] diff --git a/packages/next/src/utilities/buildFieldSchemaMap/traverseFields.ts b/packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts similarity index 100% rename from packages/next/src/utilities/buildFieldSchemaMap/traverseFields.ts rename to packages/ui/src/utilities/buildFieldSchemaMap/traverseFields.ts diff --git a/packages/next/src/utilities/buildFieldSchemaMap/types.ts b/packages/ui/src/utilities/buildFieldSchemaMap/types.ts similarity index 100% rename from packages/next/src/utilities/buildFieldSchemaMap/types.ts rename to packages/ui/src/utilities/buildFieldSchemaMap/types.ts diff --git a/packages/ui/src/utilities/buildFormState.ts b/packages/ui/src/utilities/buildFormState.ts new file mode 100644 index 00000000000..3bb2956adf5 --- /dev/null +++ b/packages/ui/src/utilities/buildFormState.ts @@ -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 => { + 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 + preferences?: Promise + } = {} + + // 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 +}