@12-apps/forms-core 1.21.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +40 -2
- package/dist/index.js +30 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/en-US.ts +12 -0
- package/src/index.ts +4 -0
- package/src/locales.ts +16 -0
- package/src/messages.ts +11 -0
- package/src/pt-BR.ts +16 -0
- package/src/serverAction.ts +13 -2
- package/src/validators.ts +5 -3
package/dist/index.d.ts
CHANGED
|
@@ -91,6 +91,10 @@ declare const ok: <T>(data: T) => Result<T>;
|
|
|
91
91
|
*/
|
|
92
92
|
declare const err: (error: string, fieldErrors?: Record<string, string>) => Result<never>;
|
|
93
93
|
|
|
94
|
+
interface ServerActionOptions {
|
|
95
|
+
/** The sentence an unexpected failure shows the user, in the host's language. */
|
|
96
|
+
genericError?: string;
|
|
97
|
+
}
|
|
94
98
|
/**
|
|
95
99
|
* An error whose `message` is safe to show to the end user.
|
|
96
100
|
*
|
|
@@ -133,9 +137,10 @@ declare class ActionError extends Error {
|
|
|
133
137
|
* @typeParam O - The handler's success payload type.
|
|
134
138
|
* @param schema - Per-field validators applied before the handler runs.
|
|
135
139
|
* @param handler - The business logic, run only when input is valid.
|
|
140
|
+
* @param options - `genericError`: the host's sentence for an unexpected failure.
|
|
136
141
|
* @returns An async action that resolves to a {@link Result}.
|
|
137
142
|
*/
|
|
138
|
-
declare const createServerAction: <I extends Record<string, string>, O>(schema: ValidatorSchema<I>, handler: (input: I) => Promise<O> | O) => ((input: I) => Promise<Result<O>>);
|
|
143
|
+
declare const createServerAction: <I extends Record<string, string>, O>(schema: ValidatorSchema<I>, handler: (input: I) => Promise<O> | O, options?: ServerActionOptions) => ((input: I) => Promise<Result<O>>);
|
|
139
144
|
|
|
140
145
|
/**
|
|
141
146
|
* Brazilian document + address primitives (FUT-306).
|
|
@@ -217,4 +222,37 @@ declare const cnpj: (message: string) => Validator;
|
|
|
217
222
|
*/
|
|
218
223
|
declare function formatBrPhone(raw: string | null | undefined): string | null;
|
|
219
224
|
|
|
220
|
-
|
|
225
|
+
/** The sentences this package falls back to, in one host-chosen language. */
|
|
226
|
+
interface FormsMessages {
|
|
227
|
+
/** `createServerAction`'s answer to an unexpected throw. */
|
|
228
|
+
genericError: string;
|
|
229
|
+
/** `required()`. */
|
|
230
|
+
required: string;
|
|
231
|
+
/** `email()`. */
|
|
232
|
+
email: string;
|
|
233
|
+
/** `minLength(n)`. */
|
|
234
|
+
minLength: (length: number) => string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The en-US sentences — a NAMED export a host passes by hand, exactly as
|
|
239
|
+
* `PT_BR_FORMS_MESSAGES`. They match what the package falls back to.
|
|
240
|
+
*/
|
|
241
|
+
declare const EN_US_FORMS_MESSAGES: FormsMessages;
|
|
242
|
+
|
|
243
|
+
declare const FORMS_MESSAGES: {
|
|
244
|
+
readonly 'pt-BR': FormsMessages;
|
|
245
|
+
readonly 'en-US': FormsMessages;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The pt-BR sentences for this package's English fallbacks — a NAMED export a
|
|
250
|
+
* host passes by hand, never a default:
|
|
251
|
+
*
|
|
252
|
+
* - `createServerAction(schema, handler, { genericError: PT_BR_FORMS_MESSAGES.genericError })`
|
|
253
|
+
* - `required(PT_BR_FORMS_MESSAGES.required)`, `email(PT_BR_FORMS_MESSAGES.email)`,
|
|
254
|
+
* `minLength(n, PT_BR_FORMS_MESSAGES.minLength(n))`
|
|
255
|
+
*/
|
|
256
|
+
declare const PT_BR_FORMS_MESSAGES: FormsMessages;
|
|
257
|
+
|
|
258
|
+
export { ActionError, CEP_PATTERN, CNPJ_PATTERN, EN_US_FORMS_MESSAGES, FORMS_MESSAGES, type FieldErrors, type FormsMessages, PT_BR_FORMS_MESSAGES, type Result, type ServerActionOptions, UF_LIST, type Uf, type Validator, type ValidatorSchema, cep, cepDigits, cnpj, cnpjChars, createServerAction, email, err, formatBrPhone, formatCep, formatCnpj, isUf, isValidCep, isValidCnpj, min, minLength, ok, required, validateFields };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
|
+
// src/en-US.ts
|
|
5
|
+
var EN_US_FORMS_MESSAGES = {
|
|
6
|
+
genericError: "Something went wrong. Please try again.",
|
|
7
|
+
required: "This field is required",
|
|
8
|
+
email: "Enter a valid email address",
|
|
9
|
+
minLength: /* @__PURE__ */ __name((length) => `Must be at least ${length} characters`, "minLength")
|
|
10
|
+
};
|
|
11
|
+
|
|
4
12
|
// src/validators.ts
|
|
5
13
|
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6
|
-
var required = /* @__PURE__ */ __name((message =
|
|
7
|
-
var email = /* @__PURE__ */ __name((message =
|
|
8
|
-
var minLength = /* @__PURE__ */ __name((length, message =
|
|
14
|
+
var required = /* @__PURE__ */ __name((message = EN_US_FORMS_MESSAGES.required) => (value) => value.trim().length === 0 ? message : void 0, "required");
|
|
15
|
+
var email = /* @__PURE__ */ __name((message = EN_US_FORMS_MESSAGES.email) => (value) => EMAIL_PATTERN.test(value) ? void 0 : message, "email");
|
|
16
|
+
var minLength = /* @__PURE__ */ __name((length, message = EN_US_FORMS_MESSAGES.minLength(length)) => (value) => value.length < length ? message : void 0, "minLength");
|
|
9
17
|
var min = minLength;
|
|
10
18
|
var firstFieldError = /* @__PURE__ */ __name((validators, value) => {
|
|
11
19
|
for (const validate of validators) {
|
|
@@ -61,7 +69,7 @@ var ActionError = class extends Error {
|
|
|
61
69
|
this.fieldErrors = fieldErrors;
|
|
62
70
|
}
|
|
63
71
|
};
|
|
64
|
-
var createServerAction = /* @__PURE__ */ __name((schema, handler) => {
|
|
72
|
+
var createServerAction = /* @__PURE__ */ __name((schema, handler, options = {}) => {
|
|
65
73
|
return async (input) => {
|
|
66
74
|
const errors = validateFields(input, schema);
|
|
67
75
|
const firstField = Object.keys(errors).find(
|
|
@@ -79,7 +87,7 @@ var createServerAction = /* @__PURE__ */ __name((schema, handler) => {
|
|
|
79
87
|
return err(error.message, fieldErrors);
|
|
80
88
|
}
|
|
81
89
|
console.error("[serverAction] handler threw an unexpected error:", error);
|
|
82
|
-
return err(GENERIC_ERROR);
|
|
90
|
+
return err(options.genericError ?? GENERIC_ERROR);
|
|
83
91
|
}
|
|
84
92
|
};
|
|
85
93
|
}, "createServerAction");
|
|
@@ -192,10 +200,27 @@ function formatBrPhone(raw) {
|
|
|
192
200
|
return `(${ddd}) ${subscriber.slice(0, split)}-${subscriber.slice(split)}`;
|
|
193
201
|
}
|
|
194
202
|
__name(formatBrPhone, "formatBrPhone");
|
|
203
|
+
|
|
204
|
+
// src/pt-BR.ts
|
|
205
|
+
var PT_BR_FORMS_MESSAGES = {
|
|
206
|
+
genericError: "Algo deu errado. Tente novamente.",
|
|
207
|
+
required: "Campo obrigat\xF3rio",
|
|
208
|
+
email: "Informe um e-mail v\xE1lido",
|
|
209
|
+
minLength: /* @__PURE__ */ __name((length) => `Deve ter pelo menos ${length} caracteres`, "minLength")
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// src/locales.ts
|
|
213
|
+
var FORMS_MESSAGES = {
|
|
214
|
+
"pt-BR": PT_BR_FORMS_MESSAGES,
|
|
215
|
+
"en-US": EN_US_FORMS_MESSAGES
|
|
216
|
+
};
|
|
195
217
|
export {
|
|
196
218
|
ActionError,
|
|
197
219
|
CEP_PATTERN,
|
|
198
220
|
CNPJ_PATTERN,
|
|
221
|
+
EN_US_FORMS_MESSAGES,
|
|
222
|
+
FORMS_MESSAGES,
|
|
223
|
+
PT_BR_FORMS_MESSAGES,
|
|
199
224
|
UF_LIST,
|
|
200
225
|
cep,
|
|
201
226
|
cepDigits,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/validators.ts","../src/result.ts","../src/serverAction.ts","../src/br.ts"],"sourcesContent":["/**\n * Field-level form validators\n *\n * Pure, framework-agnostic validators — NO zod, NO react-hook-form. Each\n * validator is curried and returns a `string` error message for invalid input\n * or `undefined` when the value is valid.\n *\n * @module forms-core/validators\n */\n\n/** A field validator: returns an error message, or `undefined` when valid. */\nexport type Validator = (value: string) => string | undefined;\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/**\n * Requires a non-empty value (after trimming whitespace).\n *\n * @param message - Custom error message for empty input.\n * @returns A validator returning the message for empty input, else `undefined`.\n */\nexport const required =\n (message = 'This field is required'): Validator =>\n (value) =>\n value.trim().length === 0 ? message : undefined;\n\n/**\n * Validates that the value is a well-formed email address.\n *\n * @param message - Custom error message for malformed input.\n * @returns A validator returning the message for invalid input, else `undefined`.\n */\nexport const email =\n (message = 'Enter a valid email address'): Validator =>\n (value) =>\n EMAIL_PATTERN.test(value) ? undefined : message;\n\n/**\n * Validates that the value has at least `length` characters.\n *\n * @param length - The minimum number of characters required.\n * @param message - Custom error message for too-short input.\n * @returns A validator returning the message for short input, else `undefined`.\n */\nexport const minLength =\n (length: number, message = `Must be at least ${length} characters`): Validator =>\n (value) =>\n value.length < length ? message : undefined;\n\n/** Alias of {@link minLength}. */\nexport const min = minLength;\n\n/** A map of field names to the list of validators applied to that field. */\nexport type ValidatorSchema<T> = {\n [K in keyof T]?: Validator[];\n};\n\n/** A map of field names to the first error message produced for that field. */\nexport type FieldErrors<T> = {\n [K in keyof T]?: string;\n};\n\n/**\n * The first error message a field's validators produce, or `undefined` when\n * every validator passes. Short-circuits on the first failure.\n */\nconst firstFieldError = (validators: Validator[], value: string): string | undefined => {\n for (const validate of validators) {\n const error = validate(value);\n if (error !== undefined) {\n return error;\n }\n }\n return undefined;\n};\n\n/**\n * Runs each field's validators against `values`, short-circuiting on the first\n * failing validator per field.\n *\n * @param values - The field values to validate (keyed by field name).\n * @param schema - Validators to apply per field.\n * @returns A record containing one entry per failing field; `{}` when all valid.\n */\nexport const validateFields = <T extends Record<string, string>>(\n values: T,\n schema: ValidatorSchema<T>,\n): FieldErrors<T> => {\n const errors: FieldErrors<T> = {};\n\n for (const field of Object.keys(schema) as (keyof T)[]) {\n const validators = schema[field];\n if (!validators) {\n continue;\n }\n const error = firstFieldError(validators, values[field] ?? '');\n if (error !== undefined) {\n errors[field] = error;\n }\n }\n\n return errors;\n};\n","/**\n * Discriminated-union result type for operations that can fail.\n *\n * Pure and framework-agnostic. The `ok` discriminant narrows the union so\n * consumers can safely read `data` on success or `error` on failure.\n *\n * @typeParam T - The success payload type.\n * @module forms-core/result\n */\nexport type Result<T> =\n | { ok: true; data: T }\n | {\n ok: false;\n error: string;\n /**\n * Per-field error messages, keyed by form field name, when the failure is\n * attributable to specific input(s). Lets a form highlight the exact\n * offending field(s) instead of only showing the summary `error`.\n */\n fieldErrors?: Record<string, string>;\n };\n\n/**\n * Builds a successful {@link Result} wrapping `data`.\n *\n * @typeParam T - The success payload type.\n * @param data - The value to wrap.\n * @returns `{ ok: true, data }`.\n */\nexport const ok = <T>(data: T): Result<T> => ({ ok: true, data });\n\n/**\n * Builds a failed {@link Result} wrapping an error `message`.\n *\n * @param error - A user-safe error message (never an internal/stack detail).\n * @param fieldErrors - Optional per-field messages so a form can highlight the\n * exact offending field(s). Omitted when the failure isn't field-specific.\n * @returns `{ ok: false, error, fieldErrors? }`.\n */\nexport const err = (\n error: string,\n fieldErrors?: Record<string, string>,\n): Result<never> =>\n fieldErrors ? { ok: false, error, fieldErrors } : { ok: false, error };\n","import { err, ok, type Result } from './result';\nimport { validateFields, type ValidatorSchema } from './validators';\n\n/** Generic, user-safe message returned when a handler throws. */\nconst GENERIC_ERROR = 'Something went wrong. Please try again.';\n\n/**\n * An error whose `message` is safe to show to the end user.\n *\n * Handlers throw this to surface a *specific, actionable* failure (e.g. \"a\n * product with this name already exists\") through {@link createServerAction}\n * instead of collapsing into the {@link GENERIC_ERROR}. Any error that is NOT\n * an `ActionError` is treated as unexpected: it is logged server-side and the\n * caller receives the generic message so internal details never leak.\n */\nexport class ActionError extends Error {\n /**\n * The form field this failure is attributable to (e.g. `\"name\"` for a\n * duplicate-name conflict). When set, {@link createServerAction} surfaces it\n * as `fieldErrors` so the form can highlight that exact field.\n */\n readonly field?: string;\n\n /**\n * Per-field messages when a single failure attributes to MORE than one field\n * (e.g. several duplicate SKUs across a product's variation rows). Takes\n * precedence over {@link field}; when set, {@link createServerAction} passes\n * this map straight through as `fieldErrors`.\n */\n readonly fieldErrors?: Record<string, string>;\n\n constructor(message: string, field?: string, fieldErrors?: Record<string, string>) {\n super(message);\n this.name = 'ActionError';\n this.field = field;\n this.fieldErrors = fieldErrors;\n }\n}\n\n/**\n * Wraps a handler-agnostic server action with validate-first semantics.\n *\n * The returned action:\n * 1. Runs {@link validateFields} against `input` using `schema`. On the first\n * field error it returns `err(message)` **without invoking `handler`**.\n * 2. Otherwise invokes `handler(input)` and returns `ok` with its resolved\n * value.\n * 3. Passes through the message of an {@link ActionError} thrown by `handler`\n * (a deliberate, user-safe failure), and maps any *other* exception to a\n * generic `err` message — logging the real cause server-side so production\n * failures stay diagnosable without leaking internals to the client.\n *\n * @typeParam I - The validated input shape (string fields keyed by name).\n * @typeParam O - The handler's success payload type.\n * @param schema - Per-field validators applied before the handler runs.\n * @param handler - The business logic, run only when input is valid.\n * @returns An async action that resolves to a {@link Result}.\n */\nexport const createServerAction = <I extends Record<string, string>, O>(\n schema: ValidatorSchema<I>,\n handler: (input: I) => Promise<O> | O,\n): ((input: I) => Promise<Result<O>>) => {\n return async (input: I): Promise<Result<O>> => {\n const errors = validateFields(input, schema);\n // Surface the first failing field by name so the form highlights it.\n const firstField = (Object.keys(errors) as (keyof I)[]).find(\n (field) => typeof errors[field] === 'string',\n );\n if (firstField !== undefined) {\n const message = errors[firstField] as string;\n return err(message, { [String(firstField)]: message });\n }\n\n try {\n return ok(await handler(input));\n } catch (error) {\n if (error instanceof ActionError) {\n // A multi-field map (fieldErrors) wins over the single-field shorthand so\n // one failure can highlight several inputs (e.g. duplicate variation SKUs).\n const fieldErrors =\n error.fieldErrors ?? (error.field ? { [error.field]: error.message } : undefined);\n return err(error.message, fieldErrors);\n }\n // Unexpected: log the real cause server-side (never sent to the client)\n // so prod failures are diagnosable, and return a safe generic message.\n console.error('[serverAction] handler threw an unexpected error:', error);\n return err(GENERIC_ERROR);\n }\n };\n};\n","/**\n * Brazilian document + address primitives (FUT-306).\n *\n * Masks, shape checks and check-digit validation for the two fields every\n * pt-BR form asks for — CEP and CNPJ — plus the UF list. Pure and dependency\n * free like the rest of `forms-core`, so the SAME rules run in the browser\n * (input mask + inline validation), in a route handler (before hitting a\n * lookup provider) and in a test.\n *\n * @module forms-core/br\n */\n\nimport type { Validator } from './validators';\n\n/** Brazilian federative units, for UF selects and client-side validation. */\nexport const UF_LIST = [\n 'AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO',\n 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE', 'PI',\n 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO',\n] as const;\n\n/** A Brazilian federative unit. */\nexport type Uf = (typeof UF_LIST)[number];\n\n/** True when `value` is one of the 27 {@link UF_LIST} entries (case-sensitive). */\nexport function isUf(value: string): value is Uf {\n return (UF_LIST as readonly string[]).includes(value);\n}\n\n// ---------------------------------------------------------------------------\n// CEP\n// ---------------------------------------------------------------------------\n\n/** Canonical CEP shape after masking/normalization: `01310-100`. */\nexport const CEP_PATTERN = /^\\d{5}-\\d{3}$/;\n\n/** The bare digits of a CEP, capped at the 8 a CEP has. */\nexport function cepDigits(raw: string): string {\n return raw.replace(/\\D/g, '').slice(0, 8);\n}\n\n/**\n * Mask raw CEP input to `#####-###`: strips non-digits, caps at 8 digits and\n * inserts the hyphen once the sixth digit arrives. Pure — safe to run on every\n * keystroke and to normalize before matching {@link CEP_PATTERN}.\n */\nexport function formatCep(raw: string): string {\n const digits = cepDigits(raw);\n return digits.length > 5 ? `${digits.slice(0, 5)}-${digits.slice(5)}` : digits;\n}\n\n/**\n * True when `raw` holds a complete, well-formed CEP.\n *\n * Shape only: a CEP is a postal RANGE allocated by the Correios, so there is no\n * check digit to verify. Whether it actually EXISTS is answered by a lookup,\n * not by this function.\n */\nexport function isValidCep(raw: string): boolean {\n return CEP_PATTERN.test(formatCep(raw));\n}\n\n/** Empty ⇒ valid (optional field); otherwise the value must be a full CEP. */\nexport const cep =\n (message: string): Validator =>\n (value) =>\n value.trim() === '' || isValidCep(value) ? undefined : message;\n\n// ---------------------------------------------------------------------------\n// CNPJ\n// ---------------------------------------------------------------------------\n\n/** Canonical CNPJ shape after masking: `12.ABC.345/01DE-35`. */\nexport const CNPJ_PATTERN = /^[0-9A-Z]{2}\\.[0-9A-Z]{3}\\.[0-9A-Z]{3}\\/[0-9A-Z]{4}-\\d{2}$/;\n\n/** How many characters a CNPJ has, check digits included. */\nconst CNPJ_LENGTH = 14;\n\n/**\n * Strip a CNPJ to its 14 significant characters, uppercased.\n *\n * Alphanumeric-aware (Receita Federal IN 2.229/2024, live in production since\n * 2026-07-06): the first 12 positions may be letters OR digits, so only\n * punctuation is removed — never letters.\n */\nexport function cnpjChars(raw: string): string {\n return raw.toUpperCase().replace(/[^0-9A-Z]/g, '').slice(0, CNPJ_LENGTH);\n}\n\n/**\n * Mask raw CNPJ input to `##.###.###/####-##`, accepting letters in the first\n * twelve positions and digits in the two check positions.\n */\nexport function formatCnpj(raw: string): string {\n const chars = cnpjChars(raw);\n const parts = [\n chars.slice(0, 2),\n chars.slice(2, 5),\n chars.slice(5, 8),\n chars.slice(8, 12),\n // The check digits are always numeric, even on an alphanumeric CNPJ.\n chars.slice(12, 14).replace(/\\D/g, ''),\n ];\n let masked = parts[0] as string;\n if (chars.length > 2) masked += `.${parts[1]}`;\n if (chars.length > 5) masked += `.${parts[2]}`;\n if (chars.length > 8) masked += `/${parts[3]}`;\n if (chars.length > 12) masked += `-${parts[4]}`;\n return masked;\n}\n\n/**\n * The mod-11 weights, right-to-left cycling 2…9. `slice(-n)` takes the tail so\n * the same table serves the 12-char first pass and the 13-char second.\n */\nconst CNPJ_WEIGHTS = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] as const;\n\n/**\n * A character's numeric contribution: its ASCII code minus 48. Digits `0`–`9`\n * map to 0–9 (identical to the legacy rule, so existing CNPJs keep validating)\n * and letters `A`–`Z` map to 17–42.\n */\nfunction cnpjValue(char: string): number {\n return char.charCodeAt(0) - 48;\n}\n\n/** One mod-11 check digit over the first `base.length` characters. */\nfunction cnpjCheckDigit(base: string): number {\n const weights = CNPJ_WEIGHTS.slice(-base.length);\n const sum = [...base].reduce((acc, char, i) => acc + cnpjValue(char) * (weights[i] as number), 0);\n const remainder = sum % 11;\n return remainder < 2 ? 0 : 11 - remainder;\n}\n\n/**\n * Validate a CNPJ's check digits (mod 11), alphanumeric-aware.\n *\n * Rejects the repeated-character sequences (`00000000000000`, `AAAAAAAAAAAA35`)\n * that satisfy the arithmetic but are never issued. Returns false for anything\n * that is not exactly 14 characters with numeric check digits.\n */\nexport function isValidCnpj(raw: string): boolean {\n const chars = cnpjChars(raw);\n if (chars.length !== CNPJ_LENGTH) return false;\n // Only the 12-character root/establishment may be alphanumeric; the two check\n // digits stay numeric.\n if (!/^\\d{2}$/.test(chars.slice(12))) return false;\n if (new Set(chars).size === 1) return false;\n\n const base = chars.slice(0, 12);\n const first = cnpjCheckDigit(base);\n if (first !== Number(chars[12])) return false;\n return cnpjCheckDigit(`${base}${first}`) === Number(chars[13]);\n}\n\n/** Empty ⇒ valid (optional field); otherwise the check digits must match. */\nexport const cnpj =\n (message: string): Validator =>\n (value) =>\n value.trim() === '' || isValidCnpj(value) ? undefined : message;\n\n// ---------------------------------------------------------------------------\n// Telefone\n// ---------------------------------------------------------------------------\n\n/**\n * Format a Brazilian phone as `(11) 3214-5600` / `(11) 91234-5678`.\n *\n * Both sources that hand us a phone give it as bare concatenated digits — the\n * Receita Federal's `ddd_telefone_1` and an NF-e's `<fone>` — so the same\n * formatting rule serves the CNPJ lookup and the nota importer.\n *\n * Anything that is not a 10- or 11-digit national number is returned trimmed\n * but otherwise untouched: an unrecognized phone (a foreign issuer, an extension\n * appended by the emitter's software) is still better dropped into the field\n * than discarded.\n *\n * @param raw - Digits as the source supplies them, e.g. `1132145600`.\n * @returns The formatted phone, or `null` when there is nothing to format.\n */\nexport function formatBrPhone(raw: string | null | undefined): string | null {\n const trimmed = raw?.trim();\n if (!trimmed) return null;\n const digits = trimmed.replace(/\\D/g, '');\n if (digits.length !== 10 && digits.length !== 11) return trimmed;\n const ddd = digits.slice(0, 2);\n const subscriber = digits.slice(2);\n const split = subscriber.length - 4;\n return `(${ddd}) ${subscriber.slice(0, split)}-${subscriber.slice(split)}`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/en-US.ts","../src/validators.ts","../src/result.ts","../src/serverAction.ts","../src/br.ts","../src/pt-BR.ts","../src/locales.ts"],"sourcesContent":["import type { FormsMessages } from './messages';\n\n/**\n * The en-US sentences — a NAMED export a host passes by hand, exactly as\n * `PT_BR_FORMS_MESSAGES`. They match what the package falls back to.\n */\nexport const EN_US_FORMS_MESSAGES: FormsMessages = {\n genericError: 'Something went wrong. Please try again.',\n required: 'This field is required',\n email: 'Enter a valid email address',\n minLength: (length: number): string => `Must be at least ${length} characters`,\n};\n","/**\n * Field-level form validators\n *\n * Pure, framework-agnostic validators — NO zod, NO react-hook-form. Each\n * validator is curried and returns a `string` error message for invalid input\n * or `undefined` when the value is valid.\n *\n * @module forms-core/validators\n */\n\nimport { EN_US_FORMS_MESSAGES } from './en-US';\n\n/** A field validator: returns an error message, or `undefined` when valid. */\nexport type Validator = (value: string) => string | undefined;\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/**\n * Requires a non-empty value (after trimming whitespace).\n *\n * @param message - Custom error message for empty input.\n * @returns A validator returning the message for empty input, else `undefined`.\n */\nexport const required =\n (message = EN_US_FORMS_MESSAGES.required): Validator =>\n (value) =>\n value.trim().length === 0 ? message : undefined;\n\n/**\n * Validates that the value is a well-formed email address.\n *\n * @param message - Custom error message for malformed input.\n * @returns A validator returning the message for invalid input, else `undefined`.\n */\nexport const email =\n (message = EN_US_FORMS_MESSAGES.email): Validator =>\n (value) =>\n EMAIL_PATTERN.test(value) ? undefined : message;\n\n/**\n * Validates that the value has at least `length` characters.\n *\n * @param length - The minimum number of characters required.\n * @param message - Custom error message for too-short input.\n * @returns A validator returning the message for short input, else `undefined`.\n */\nexport const minLength =\n (length: number, message = EN_US_FORMS_MESSAGES.minLength(length)): Validator =>\n (value) =>\n value.length < length ? message : undefined;\n\n/** Alias of {@link minLength}. */\nexport const min = minLength;\n\n/** A map of field names to the list of validators applied to that field. */\nexport type ValidatorSchema<T> = {\n [K in keyof T]?: Validator[];\n};\n\n/** A map of field names to the first error message produced for that field. */\nexport type FieldErrors<T> = {\n [K in keyof T]?: string;\n};\n\n/**\n * The first error message a field's validators produce, or `undefined` when\n * every validator passes. Short-circuits on the first failure.\n */\nconst firstFieldError = (validators: Validator[], value: string): string | undefined => {\n for (const validate of validators) {\n const error = validate(value);\n if (error !== undefined) {\n return error;\n }\n }\n return undefined;\n};\n\n/**\n * Runs each field's validators against `values`, short-circuiting on the first\n * failing validator per field.\n *\n * @param values - The field values to validate (keyed by field name).\n * @param schema - Validators to apply per field.\n * @returns A record containing one entry per failing field; `{}` when all valid.\n */\nexport const validateFields = <T extends Record<string, string>>(\n values: T,\n schema: ValidatorSchema<T>,\n): FieldErrors<T> => {\n const errors: FieldErrors<T> = {};\n\n for (const field of Object.keys(schema) as (keyof T)[]) {\n const validators = schema[field];\n if (!validators) {\n continue;\n }\n const error = firstFieldError(validators, values[field] ?? '');\n if (error !== undefined) {\n errors[field] = error;\n }\n }\n\n return errors;\n};\n","/**\n * Discriminated-union result type for operations that can fail.\n *\n * Pure and framework-agnostic. The `ok` discriminant narrows the union so\n * consumers can safely read `data` on success or `error` on failure.\n *\n * @typeParam T - The success payload type.\n * @module forms-core/result\n */\nexport type Result<T> =\n | { ok: true; data: T }\n | {\n ok: false;\n error: string;\n /**\n * Per-field error messages, keyed by form field name, when the failure is\n * attributable to specific input(s). Lets a form highlight the exact\n * offending field(s) instead of only showing the summary `error`.\n */\n fieldErrors?: Record<string, string>;\n };\n\n/**\n * Builds a successful {@link Result} wrapping `data`.\n *\n * @typeParam T - The success payload type.\n * @param data - The value to wrap.\n * @returns `{ ok: true, data }`.\n */\nexport const ok = <T>(data: T): Result<T> => ({ ok: true, data });\n\n/**\n * Builds a failed {@link Result} wrapping an error `message`.\n *\n * @param error - A user-safe error message (never an internal/stack detail).\n * @param fieldErrors - Optional per-field messages so a form can highlight the\n * exact offending field(s). Omitted when the failure isn't field-specific.\n * @returns `{ ok: false, error, fieldErrors? }`.\n */\nexport const err = (\n error: string,\n fieldErrors?: Record<string, string>,\n): Result<never> =>\n fieldErrors ? { ok: false, error, fieldErrors } : { ok: false, error };\n","import { err, ok, type Result } from './result';\nimport { validateFields, type ValidatorSchema } from './validators';\n\n/**\n * Generic, user-safe message returned when a handler throws — the English\n * fallback. A host passes its own through `options.genericError` (pt-BR:\n * `PT_BR_FORMS_MESSAGES.genericError`).\n */\nconst GENERIC_ERROR = 'Something went wrong. Please try again.';\n\nexport interface ServerActionOptions {\n /** The sentence an unexpected failure shows the user, in the host's language. */\n genericError?: string;\n}\n\n/**\n * An error whose `message` is safe to show to the end user.\n *\n * Handlers throw this to surface a *specific, actionable* failure (e.g. \"a\n * product with this name already exists\") through {@link createServerAction}\n * instead of collapsing into the {@link GENERIC_ERROR}. Any error that is NOT\n * an `ActionError` is treated as unexpected: it is logged server-side and the\n * caller receives the generic message so internal details never leak.\n */\nexport class ActionError extends Error {\n /**\n * The form field this failure is attributable to (e.g. `\"name\"` for a\n * duplicate-name conflict). When set, {@link createServerAction} surfaces it\n * as `fieldErrors` so the form can highlight that exact field.\n */\n readonly field?: string;\n\n /**\n * Per-field messages when a single failure attributes to MORE than one field\n * (e.g. several duplicate SKUs across a product's variation rows). Takes\n * precedence over {@link field}; when set, {@link createServerAction} passes\n * this map straight through as `fieldErrors`.\n */\n readonly fieldErrors?: Record<string, string>;\n\n constructor(message: string, field?: string, fieldErrors?: Record<string, string>) {\n super(message);\n this.name = 'ActionError';\n this.field = field;\n this.fieldErrors = fieldErrors;\n }\n}\n\n/**\n * Wraps a handler-agnostic server action with validate-first semantics.\n *\n * The returned action:\n * 1. Runs {@link validateFields} against `input` using `schema`. On the first\n * field error it returns `err(message)` **without invoking `handler`**.\n * 2. Otherwise invokes `handler(input)` and returns `ok` with its resolved\n * value.\n * 3. Passes through the message of an {@link ActionError} thrown by `handler`\n * (a deliberate, user-safe failure), and maps any *other* exception to a\n * generic `err` message — logging the real cause server-side so production\n * failures stay diagnosable without leaking internals to the client.\n *\n * @typeParam I - The validated input shape (string fields keyed by name).\n * @typeParam O - The handler's success payload type.\n * @param schema - Per-field validators applied before the handler runs.\n * @param handler - The business logic, run only when input is valid.\n * @param options - `genericError`: the host's sentence for an unexpected failure.\n * @returns An async action that resolves to a {@link Result}.\n */\nexport const createServerAction = <I extends Record<string, string>, O>(\n schema: ValidatorSchema<I>,\n handler: (input: I) => Promise<O> | O,\n options: ServerActionOptions = {},\n): ((input: I) => Promise<Result<O>>) => {\n return async (input: I): Promise<Result<O>> => {\n const errors = validateFields(input, schema);\n // Surface the first failing field by name so the form highlights it.\n const firstField = (Object.keys(errors) as (keyof I)[]).find(\n (field) => typeof errors[field] === 'string',\n );\n if (firstField !== undefined) {\n const message = errors[firstField] as string;\n return err(message, { [String(firstField)]: message });\n }\n\n try {\n return ok(await handler(input));\n } catch (error) {\n if (error instanceof ActionError) {\n // A multi-field map (fieldErrors) wins over the single-field shorthand so\n // one failure can highlight several inputs (e.g. duplicate variation SKUs).\n const fieldErrors =\n error.fieldErrors ?? (error.field ? { [error.field]: error.message } : undefined);\n return err(error.message, fieldErrors);\n }\n // Unexpected: log the real cause server-side (never sent to the client)\n // so prod failures are diagnosable, and return a safe generic message.\n console.error('[serverAction] handler threw an unexpected error:', error);\n return err(options.genericError ?? GENERIC_ERROR);\n }\n };\n};\n","/**\n * Brazilian document + address primitives (FUT-306).\n *\n * Masks, shape checks and check-digit validation for the two fields every\n * pt-BR form asks for — CEP and CNPJ — plus the UF list. Pure and dependency\n * free like the rest of `forms-core`, so the SAME rules run in the browser\n * (input mask + inline validation), in a route handler (before hitting a\n * lookup provider) and in a test.\n *\n * @module forms-core/br\n */\n\nimport type { Validator } from './validators';\n\n/** Brazilian federative units, for UF selects and client-side validation. */\nexport const UF_LIST = [\n 'AC', 'AL', 'AP', 'AM', 'BA', 'CE', 'DF', 'ES', 'GO',\n 'MA', 'MT', 'MS', 'MG', 'PA', 'PB', 'PR', 'PE', 'PI',\n 'RJ', 'RN', 'RS', 'RO', 'RR', 'SC', 'SP', 'SE', 'TO',\n] as const;\n\n/** A Brazilian federative unit. */\nexport type Uf = (typeof UF_LIST)[number];\n\n/** True when `value` is one of the 27 {@link UF_LIST} entries (case-sensitive). */\nexport function isUf(value: string): value is Uf {\n return (UF_LIST as readonly string[]).includes(value);\n}\n\n// ---------------------------------------------------------------------------\n// CEP\n// ---------------------------------------------------------------------------\n\n/** Canonical CEP shape after masking/normalization: `01310-100`. */\nexport const CEP_PATTERN = /^\\d{5}-\\d{3}$/;\n\n/** The bare digits of a CEP, capped at the 8 a CEP has. */\nexport function cepDigits(raw: string): string {\n return raw.replace(/\\D/g, '').slice(0, 8);\n}\n\n/**\n * Mask raw CEP input to `#####-###`: strips non-digits, caps at 8 digits and\n * inserts the hyphen once the sixth digit arrives. Pure — safe to run on every\n * keystroke and to normalize before matching {@link CEP_PATTERN}.\n */\nexport function formatCep(raw: string): string {\n const digits = cepDigits(raw);\n return digits.length > 5 ? `${digits.slice(0, 5)}-${digits.slice(5)}` : digits;\n}\n\n/**\n * True when `raw` holds a complete, well-formed CEP.\n *\n * Shape only: a CEP is a postal RANGE allocated by the Correios, so there is no\n * check digit to verify. Whether it actually EXISTS is answered by a lookup,\n * not by this function.\n */\nexport function isValidCep(raw: string): boolean {\n return CEP_PATTERN.test(formatCep(raw));\n}\n\n/** Empty ⇒ valid (optional field); otherwise the value must be a full CEP. */\nexport const cep =\n (message: string): Validator =>\n (value) =>\n value.trim() === '' || isValidCep(value) ? undefined : message;\n\n// ---------------------------------------------------------------------------\n// CNPJ\n// ---------------------------------------------------------------------------\n\n/** Canonical CNPJ shape after masking: `12.ABC.345/01DE-35`. */\nexport const CNPJ_PATTERN = /^[0-9A-Z]{2}\\.[0-9A-Z]{3}\\.[0-9A-Z]{3}\\/[0-9A-Z]{4}-\\d{2}$/;\n\n/** How many characters a CNPJ has, check digits included. */\nconst CNPJ_LENGTH = 14;\n\n/**\n * Strip a CNPJ to its 14 significant characters, uppercased.\n *\n * Alphanumeric-aware (Receita Federal IN 2.229/2024, live in production since\n * 2026-07-06): the first 12 positions may be letters OR digits, so only\n * punctuation is removed — never letters.\n */\nexport function cnpjChars(raw: string): string {\n return raw.toUpperCase().replace(/[^0-9A-Z]/g, '').slice(0, CNPJ_LENGTH);\n}\n\n/**\n * Mask raw CNPJ input to `##.###.###/####-##`, accepting letters in the first\n * twelve positions and digits in the two check positions.\n */\nexport function formatCnpj(raw: string): string {\n const chars = cnpjChars(raw);\n const parts = [\n chars.slice(0, 2),\n chars.slice(2, 5),\n chars.slice(5, 8),\n chars.slice(8, 12),\n // The check digits are always numeric, even on an alphanumeric CNPJ.\n chars.slice(12, 14).replace(/\\D/g, ''),\n ];\n let masked = parts[0] as string;\n if (chars.length > 2) masked += `.${parts[1]}`;\n if (chars.length > 5) masked += `.${parts[2]}`;\n if (chars.length > 8) masked += `/${parts[3]}`;\n if (chars.length > 12) masked += `-${parts[4]}`;\n return masked;\n}\n\n/**\n * The mod-11 weights, right-to-left cycling 2…9. `slice(-n)` takes the tail so\n * the same table serves the 12-char first pass and the 13-char second.\n */\nconst CNPJ_WEIGHTS = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] as const;\n\n/**\n * A character's numeric contribution: its ASCII code minus 48. Digits `0`–`9`\n * map to 0–9 (identical to the legacy rule, so existing CNPJs keep validating)\n * and letters `A`–`Z` map to 17–42.\n */\nfunction cnpjValue(char: string): number {\n return char.charCodeAt(0) - 48;\n}\n\n/** One mod-11 check digit over the first `base.length` characters. */\nfunction cnpjCheckDigit(base: string): number {\n const weights = CNPJ_WEIGHTS.slice(-base.length);\n const sum = [...base].reduce((acc, char, i) => acc + cnpjValue(char) * (weights[i] as number), 0);\n const remainder = sum % 11;\n return remainder < 2 ? 0 : 11 - remainder;\n}\n\n/**\n * Validate a CNPJ's check digits (mod 11), alphanumeric-aware.\n *\n * Rejects the repeated-character sequences (`00000000000000`, `AAAAAAAAAAAA35`)\n * that satisfy the arithmetic but are never issued. Returns false for anything\n * that is not exactly 14 characters with numeric check digits.\n */\nexport function isValidCnpj(raw: string): boolean {\n const chars = cnpjChars(raw);\n if (chars.length !== CNPJ_LENGTH) return false;\n // Only the 12-character root/establishment may be alphanumeric; the two check\n // digits stay numeric.\n if (!/^\\d{2}$/.test(chars.slice(12))) return false;\n if (new Set(chars).size === 1) return false;\n\n const base = chars.slice(0, 12);\n const first = cnpjCheckDigit(base);\n if (first !== Number(chars[12])) return false;\n return cnpjCheckDigit(`${base}${first}`) === Number(chars[13]);\n}\n\n/** Empty ⇒ valid (optional field); otherwise the check digits must match. */\nexport const cnpj =\n (message: string): Validator =>\n (value) =>\n value.trim() === '' || isValidCnpj(value) ? undefined : message;\n\n// ---------------------------------------------------------------------------\n// Telefone\n// ---------------------------------------------------------------------------\n\n/**\n * Format a Brazilian phone as `(11) 3214-5600` / `(11) 91234-5678`.\n *\n * Both sources that hand us a phone give it as bare concatenated digits — the\n * Receita Federal's `ddd_telefone_1` and an NF-e's `<fone>` — so the same\n * formatting rule serves the CNPJ lookup and the nota importer.\n *\n * Anything that is not a 10- or 11-digit national number is returned trimmed\n * but otherwise untouched: an unrecognized phone (a foreign issuer, an extension\n * appended by the emitter's software) is still better dropped into the field\n * than discarded.\n *\n * @param raw - Digits as the source supplies them, e.g. `1132145600`.\n * @returns The formatted phone, or `null` when there is nothing to format.\n */\nexport function formatBrPhone(raw: string | null | undefined): string | null {\n const trimmed = raw?.trim();\n if (!trimmed) return null;\n const digits = trimmed.replace(/\\D/g, '');\n if (digits.length !== 10 && digits.length !== 11) return trimmed;\n const ddd = digits.slice(0, 2);\n const subscriber = digits.slice(2);\n const split = subscriber.length - 4;\n return `(${ddd}) ${subscriber.slice(0, split)}-${subscriber.slice(split)}`;\n}\n","import type { FormsMessages } from './messages';\n\n/**\n * The pt-BR sentences for this package's English fallbacks — a NAMED export a\n * host passes by hand, never a default:\n *\n * - `createServerAction(schema, handler, { genericError: PT_BR_FORMS_MESSAGES.genericError })`\n * - `required(PT_BR_FORMS_MESSAGES.required)`, `email(PT_BR_FORMS_MESSAGES.email)`,\n * `minLength(n, PT_BR_FORMS_MESSAGES.minLength(n))`\n */\nexport const PT_BR_FORMS_MESSAGES: FormsMessages = {\n genericError: 'Algo deu errado. Tente novamente.',\n required: 'Campo obrigatório',\n email: 'Informe um e-mail válido',\n minLength: (length: number): string => `Deve ter pelo menos ${length} caracteres`,\n};\n","import { EN_US_FORMS_MESSAGES } from './en-US';\nimport type { FormsMessages } from './messages';\nimport { PT_BR_FORMS_MESSAGES } from './pt-BR';\n\n/**\n * Both languages, keyed by tag — what a host hands to `@12-apps/i18n` when the\n * reader's language is a property of the request rather than of the deployment.\n * `LocalePack` is mirrored rather than imported, so the package stays liftable\n * into a repo that has never heard of `@12-apps/i18n`.\n */\ntype LocalePack<T> = { readonly 'pt-BR': T; readonly 'en-US': T };\n\nexport const FORMS_MESSAGES = {\n 'pt-BR': PT_BR_FORMS_MESSAGES,\n 'en-US': EN_US_FORMS_MESSAGES,\n} as const satisfies LocalePack<FormsMessages>;\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
package/src/en-US.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FormsMessages } from './messages';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The en-US sentences — a NAMED export a host passes by hand, exactly as
|
|
5
|
+
* `PT_BR_FORMS_MESSAGES`. They match what the package falls back to.
|
|
6
|
+
*/
|
|
7
|
+
export const EN_US_FORMS_MESSAGES: FormsMessages = {
|
|
8
|
+
genericError: 'Something went wrong. Please try again.',
|
|
9
|
+
required: 'This field is required',
|
|
10
|
+
email: 'Enter a valid email address',
|
|
11
|
+
minLength: (length: number): string => `Must be at least ${length} characters`,
|
|
12
|
+
};
|
package/src/index.ts
CHANGED
package/src/locales.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EN_US_FORMS_MESSAGES } from './en-US';
|
|
2
|
+
import type { FormsMessages } from './messages';
|
|
3
|
+
import { PT_BR_FORMS_MESSAGES } from './pt-BR';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Both languages, keyed by tag — what a host hands to `@12-apps/i18n` when the
|
|
7
|
+
* reader's language is a property of the request rather than of the deployment.
|
|
8
|
+
* `LocalePack` is mirrored rather than imported, so the package stays liftable
|
|
9
|
+
* into a repo that has never heard of `@12-apps/i18n`.
|
|
10
|
+
*/
|
|
11
|
+
type LocalePack<T> = { readonly 'pt-BR': T; readonly 'en-US': T };
|
|
12
|
+
|
|
13
|
+
export const FORMS_MESSAGES = {
|
|
14
|
+
'pt-BR': PT_BR_FORMS_MESSAGES,
|
|
15
|
+
'en-US': EN_US_FORMS_MESSAGES,
|
|
16
|
+
} as const satisfies LocalePack<FormsMessages>;
|
package/src/messages.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The sentences this package falls back to, in one host-chosen language. */
|
|
2
|
+
export interface FormsMessages {
|
|
3
|
+
/** `createServerAction`'s answer to an unexpected throw. */
|
|
4
|
+
genericError: string;
|
|
5
|
+
/** `required()`. */
|
|
6
|
+
required: string;
|
|
7
|
+
/** `email()`. */
|
|
8
|
+
email: string;
|
|
9
|
+
/** `minLength(n)`. */
|
|
10
|
+
minLength: (length: number) => string;
|
|
11
|
+
}
|
package/src/pt-BR.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FormsMessages } from './messages';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The pt-BR sentences for this package's English fallbacks — a NAMED export a
|
|
5
|
+
* host passes by hand, never a default:
|
|
6
|
+
*
|
|
7
|
+
* - `createServerAction(schema, handler, { genericError: PT_BR_FORMS_MESSAGES.genericError })`
|
|
8
|
+
* - `required(PT_BR_FORMS_MESSAGES.required)`, `email(PT_BR_FORMS_MESSAGES.email)`,
|
|
9
|
+
* `minLength(n, PT_BR_FORMS_MESSAGES.minLength(n))`
|
|
10
|
+
*/
|
|
11
|
+
export const PT_BR_FORMS_MESSAGES: FormsMessages = {
|
|
12
|
+
genericError: 'Algo deu errado. Tente novamente.',
|
|
13
|
+
required: 'Campo obrigatório',
|
|
14
|
+
email: 'Informe um e-mail válido',
|
|
15
|
+
minLength: (length: number): string => `Deve ter pelo menos ${length} caracteres`,
|
|
16
|
+
};
|
package/src/serverAction.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { err, ok, type Result } from './result';
|
|
2
2
|
import { validateFields, type ValidatorSchema } from './validators';
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Generic, user-safe message returned when a handler throws — the English
|
|
6
|
+
* fallback. A host passes its own through `options.genericError` (pt-BR:
|
|
7
|
+
* `PT_BR_FORMS_MESSAGES.genericError`).
|
|
8
|
+
*/
|
|
5
9
|
const GENERIC_ERROR = 'Something went wrong. Please try again.';
|
|
6
10
|
|
|
11
|
+
export interface ServerActionOptions {
|
|
12
|
+
/** The sentence an unexpected failure shows the user, in the host's language. */
|
|
13
|
+
genericError?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
/**
|
|
8
17
|
* An error whose `message` is safe to show to the end user.
|
|
9
18
|
*
|
|
@@ -54,11 +63,13 @@ export class ActionError extends Error {
|
|
|
54
63
|
* @typeParam O - The handler's success payload type.
|
|
55
64
|
* @param schema - Per-field validators applied before the handler runs.
|
|
56
65
|
* @param handler - The business logic, run only when input is valid.
|
|
66
|
+
* @param options - `genericError`: the host's sentence for an unexpected failure.
|
|
57
67
|
* @returns An async action that resolves to a {@link Result}.
|
|
58
68
|
*/
|
|
59
69
|
export const createServerAction = <I extends Record<string, string>, O>(
|
|
60
70
|
schema: ValidatorSchema<I>,
|
|
61
71
|
handler: (input: I) => Promise<O> | O,
|
|
72
|
+
options: ServerActionOptions = {},
|
|
62
73
|
): ((input: I) => Promise<Result<O>>) => {
|
|
63
74
|
return async (input: I): Promise<Result<O>> => {
|
|
64
75
|
const errors = validateFields(input, schema);
|
|
@@ -84,7 +95,7 @@ export const createServerAction = <I extends Record<string, string>, O>(
|
|
|
84
95
|
// Unexpected: log the real cause server-side (never sent to the client)
|
|
85
96
|
// so prod failures are diagnosable, and return a safe generic message.
|
|
86
97
|
console.error('[serverAction] handler threw an unexpected error:', error);
|
|
87
|
-
return err(GENERIC_ERROR);
|
|
98
|
+
return err(options.genericError ?? GENERIC_ERROR);
|
|
88
99
|
}
|
|
89
100
|
};
|
|
90
101
|
};
|
package/src/validators.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @module forms-core/validators
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { EN_US_FORMS_MESSAGES } from './en-US';
|
|
12
|
+
|
|
11
13
|
/** A field validator: returns an error message, or `undefined` when valid. */
|
|
12
14
|
export type Validator = (value: string) => string | undefined;
|
|
13
15
|
|
|
@@ -20,7 +22,7 @@ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
20
22
|
* @returns A validator returning the message for empty input, else `undefined`.
|
|
21
23
|
*/
|
|
22
24
|
export const required =
|
|
23
|
-
(message =
|
|
25
|
+
(message = EN_US_FORMS_MESSAGES.required): Validator =>
|
|
24
26
|
(value) =>
|
|
25
27
|
value.trim().length === 0 ? message : undefined;
|
|
26
28
|
|
|
@@ -31,7 +33,7 @@ export const required =
|
|
|
31
33
|
* @returns A validator returning the message for invalid input, else `undefined`.
|
|
32
34
|
*/
|
|
33
35
|
export const email =
|
|
34
|
-
(message =
|
|
36
|
+
(message = EN_US_FORMS_MESSAGES.email): Validator =>
|
|
35
37
|
(value) =>
|
|
36
38
|
EMAIL_PATTERN.test(value) ? undefined : message;
|
|
37
39
|
|
|
@@ -43,7 +45,7 @@ export const email =
|
|
|
43
45
|
* @returns A validator returning the message for short input, else `undefined`.
|
|
44
46
|
*/
|
|
45
47
|
export const minLength =
|
|
46
|
-
(length: number, message =
|
|
48
|
+
(length: number, message = EN_US_FORMS_MESSAGES.minLength(length)): Validator =>
|
|
47
49
|
(value) =>
|
|
48
50
|
value.length < length ? message : undefined;
|
|
49
51
|
|