82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
import type { ApiFailure } from "../../contracts/errors.js";
|
|
import {
|
|
formatMessage,
|
|
type ParameterlessMessageKey,
|
|
} from "../i18n/index.js";
|
|
|
|
export type FormValues = Readonly<Record<string, unknown>>;
|
|
export type FieldName<Values extends FormValues> = Extract<keyof Values, string>;
|
|
export type FieldErrors<Values extends FormValues> = Readonly<
|
|
Partial<Record<FieldName<Values>, string>>
|
|
>;
|
|
|
|
export type FormResult<Value> =
|
|
| Readonly<{ ok: true; value: Value }>
|
|
| Readonly<{ ok: false; error: ApiFailure }>;
|
|
|
|
export type FormResultState =
|
|
| "idle"
|
|
| "success"
|
|
| "validation-error"
|
|
| "conflict"
|
|
| "unavailable";
|
|
|
|
export type MappedValidationFailure<Values extends FormValues> = Readonly<{
|
|
fieldErrors: FieldErrors<Values>;
|
|
formErrors: readonly string[];
|
|
}>;
|
|
|
|
const VALIDATION_COPY = Object.freeze({
|
|
REQUIRED: "form.validation.required",
|
|
too_small: "form.validation.tooSmall",
|
|
too_big: "form.validation.tooBig",
|
|
invalid_type: "form.validation.invalidType",
|
|
invalid_format: "form.validation.invalidFormat",
|
|
invalid_value: "form.validation.invalidValue",
|
|
});
|
|
|
|
type MessageResolver = (key: ParameterlessMessageKey) => string;
|
|
|
|
const defaultMessage: MessageResolver = (key) => formatMessage("ko-KR", key);
|
|
|
|
export function validationMessage(
|
|
code: string,
|
|
message: MessageResolver = defaultMessage,
|
|
): string {
|
|
return message(
|
|
VALIDATION_COPY[code as keyof typeof VALIDATION_COPY] ??
|
|
"form.validation.unknown",
|
|
);
|
|
}
|
|
|
|
export function mapValidationFailureToFields<Values extends FormValues>(
|
|
failure: ApiFailure,
|
|
allowedFields: readonly FieldName<Values>[],
|
|
message: MessageResolver = defaultMessage,
|
|
): MappedValidationFailure<Values> {
|
|
if (failure.kind !== "VALIDATION_REJECTED") {
|
|
return Object.freeze({ fieldErrors: Object.freeze({}), formErrors: [] });
|
|
}
|
|
const allowed = new Set<string>(allowedFields);
|
|
const fieldErrors: Partial<Record<FieldName<Values>, string>> = {};
|
|
const formErrors: string[] = [];
|
|
const issues = failure.validationIssues ?? [];
|
|
|
|
if (issues.length === 0) {
|
|
formErrors.push(message("form.validation.retry"));
|
|
}
|
|
for (const issue of issues) {
|
|
const field = issue.path.split(".").at(0) ?? "";
|
|
if (allowed.has(field)) {
|
|
const name = field as FieldName<Values>;
|
|
fieldErrors[name] ??= validationMessage(issue.code, message);
|
|
} else {
|
|
formErrors.push(message("form.validation.unknownField"));
|
|
}
|
|
}
|
|
return Object.freeze({
|
|
fieldErrors: Object.freeze(fieldErrors),
|
|
formErrors: Object.freeze([...new Set(formErrors)]),
|
|
});
|
|
}
|