@letar/forms

Маппинг серверных ошибок

Автоматический маппинг ошибок Prisma, ZenStack и Zod на поля формы

Обзор

mapServerErrors() автоматически определяет формат ошибки и маппит её на поля формы.

import { applyServerErrors, mapServerErrors } from '@letar/forms'

;<Form
  schema={UserSchema}
  onSubmit={async ({ value }) => {
    try {
      await createUser(value)
    } catch (error) {
      const mapped = mapServerErrors(error, {
        fieldMap: {
          email: { field: 'email', message: 'Этот email уже зарегистрирован' },
        },
      })
      applyServerErrors(form, mapped)
    }
  }}
>
  <Form.Field.String name="email" />
  <Form.Errors />
  <Form.Button.Submit>Создать</Form.Button.Submit>
</Form>

С декларативным <Form>

Пример выше использует низкоуровневый useAppForm — там form доступен напрямую из замыкания. Декларативная обёртка (createForm()<Form onSubmit={(data) => ...}>) устроена иначе: её onSubmit получает только data, без инстанса формы, а обработка ошибок вынесена в middleware.onError — туда попадает то, что бросил сам onSubmit/Server Action (FormSimple перехватывает исключение и вызывает middleware.onError, см. libs/forms/src/lib/declarative/form-root/form-simple.tsx).

Чтобы применить mapServerErrors/applyServerErrors в этом случае, нужен доступ к инстансу формы снаружи onSubmit — для этого useFormRef():

import { applyServerErrors, mapServerErrors, useFormRef } from '@letar/forms'

function MaterialForm() {
  const formRef = useFormRef()

  async function handleSubmit(data: MaterialFormData) {
    // Server Action просто бросает ошибку (Prisma/ZenStack/Error) — оборачивать вручную не нужно
    await createMaterial(data)
  }

  return (
    <Form
      schema={MaterialSchema}
      initialValue={initialValue}
      onSubmit={handleSubmit}
      formRef={formRef}
      middleware={{
        onError: (error) => {
          const mapped = mapServerErrors(error, {
            fieldMap: { sku: { field: 'sku', message: 'Такой артикул уже используется' } },
          })
          if (formRef.current) {
            applyServerErrors(formRef.current, mapped)
          }
        },
      }}
    >
      <Form.Errors />
      <Form.Field.String name="sku" />
      <Form.Button.Submit>Сохранить</Form.Button.Submit>
    </Form>
  )
}

Ключевые моменты:

  • formRef передаётся в <Form> и используется внутри middleware.onError — без него applyServerErrors некуда применять ошибки.
  • Server Action, вызываемый из onSubmit, не должен ловить и оборачивать ошибку сам — mapServerErrors умеет разбирать «сырые» Prisma/ZenStack/Zod исключения.
  • Не забудь <Form.Errors /> в JSX — иначе formErrors (например P2025/rejected-by-policy) будут применены к форме, но нигде не отрисуются.
  • Рабочий пример — apps/domwellbes/src/app/(admin)/admin/materials/_components/material-form.tsx.

Поддерживаемые форматы

ФорматПримерРезультат
Prisma P2002{ code: 'P2002', meta: { target: ['email'] } }fieldErrors: [{ field: 'email' }]
Prisma P2003{ code: 'P2003', meta: { field_name: 'categoryId' } }fieldErrors: [{ field: 'categoryId' }]
Prisma P2025{ code: 'P2025' }formErrors: ['Запись не найдена']
ZenStack policy{ reason: 'rejected-by-policy' }formErrors: ['Нет доступа']
ZenStack db-query{ reason: 'db-query-error', code: 'P2002', meta }Делегация Prisma
Zod flatten{ fieldErrors: { email: ['msg'] } }Прямой маппинг
ActionResult{ success: false, error: 'msg' }formErrors: ['msg']

API

mapServerErrors(error, config?)

const mapped = mapServerErrors(error, {
  fieldMap: Record<string, { field: string; message: string }>,
  format: 'auto' | 'prisma' | 'zenstack' | 'zod' | 'action-result',
  locale: 'ru' | 'en',
  defaultMessage: 'Произошла ошибка',
})

applyServerErrors(form, mapped)

Применяет ошибки к TanStack Form через form.setFieldMeta и form.setErrorMap.

Импорт

import { applyServerErrors, mapServerErrors } from '@letar/forms'
import { mapServerErrors } from '@letar/forms/server-errors'
import { parsePrismaError, parseZenStackError } from '@letar/forms/server-errors'

On this page