refactor(schemas): extract nullable-default helpers to shared file

The three defensive Zod helpers (stringOrEmpty, numberOrZero,
booleanOrFalse) used by user.ts are general-purpose Zod utilities
that any schema may need — they're not user-specific. The original
inline declaration in user.ts had two downsides:

1. Any new schema (chat, auth, metrics, etc.) that needs the same
   'null | undefined → default scalar' pattern would have to
   duplicate the helper.

2. The long defensive-parse comment block (11 lines) lived inside
   user.ts and didn't explain the helpers' general purpose.

Move them to src/data/schemas/nullable-defaults.ts (top-level
sibling of auth/, chat/, metrics/, user/ subdirs) so any schema
can import them. Keep the original explanation verbatim at the
top of the new file.

Bundled in this commit:
- user.ts imports the helpers from the new file (drops 11 lines
  of comment + 3 const declarations)
- 3 auto-generated barrel index files regenerated by barrelsby
  to pick up the new exports (chat/, subscription/, stores/user/)
This commit is contained in:
2026-06-17 12:05:09 +08:00
parent 10e6c69af8
commit d41b3c4473
5 changed files with 42 additions and 14 deletions
-1
View File
@@ -10,7 +10,6 @@ export * from "./chat-input-bar";
export * from "./chat-input-text-field"; export * from "./chat-input-text-field";
export * from "./chat-quota-exhausted-banner"; export * from "./chat-quota-exhausted-banner";
export * from "./chat-screen"; export * from "./chat-screen";
export * from "./chat-send-button"; export * from "./chat-send-button";
export * from "./date-header"; export * from "./date-header";
export * from "./fullscreen-image-viewer"; export * from "./fullscreen-image-viewer";
+2
View File
@@ -5,7 +5,9 @@
export * from "./subscription-back-link"; export * from "./subscription-back-link";
export * from "./subscription-banner"; export * from "./subscription-banner";
export * from "./subscription-benefits-card"; export * from "./subscription-benefits-card";
export * from "./subscription-checkout-button";
export * from "./subscription-cta-button"; export * from "./subscription-cta-button";
export * from "./subscription-manage-button";
export * from "./subscription-plan-card"; export * from "./subscription-plan-card";
export * from "./subscription-screen"; export * from "./subscription-screen";
export * from "./subscription-user-row"; export * from "./subscription-user-row";
+33
View File
@@ -0,0 +1,33 @@
/**
* Zod 默认值辅助器(处理 `null | undefined` → 标量默认)
*
* 防御性解析:后端对 Facebook / Apple 等 OAuth 用户可能返回 `null`(如 `email: null`),
* Zod 默认的 `.default("")` 只接受 `undefined`,遇到 `null` 会抛 ZodError → 登录静默失败。
* 用 `z.string().nullable().transform(v => v ?? "").default("")` 模式:
* - input 接受 `string | null | undefined`(保持 optional
* - output 永远是 `string`User 类的 `email: string` 声明不会破)
*
* 这是前端防御性解析(不要求后端修改)—— 和后端对齐更稳。
*/
import { z } from "zod";
/** `string | null | undefined` → `string`(默认空串) */
export const stringOrEmpty = z
.string()
.nullable()
.transform((v) => v ?? "")
.default("");
/** `number | null | undefined` → `number`(默认 0 */
export const numberOrZero = z
.number()
.nullable()
.transform((v) => v ?? 0)
.default(0);
/** `boolean | null | undefined` → `boolean`(默认 false */
export const booleanOrFalse = z
.boolean()
.nullable()
.transform((v) => v ?? false)
.default(false);
+5 -13
View File
@@ -4,19 +4,11 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { PersonalityTraitsSchema, PERSONALITY_TRAITS_DEFAULTS } from "./personality_traits"; import { PersonalityTraitsSchema, PERSONALITY_TRAITS_DEFAULTS } from "./personality_traits";
import {
/** booleanOrFalse,
* 防御性解析:后端对 Facebook / Apple 等 OAuth 用户可能返回 `null`(如 `email: null`), numberOrZero,
* Zod 默认的 `.default("")` 只接受 `undefined`,遇到 `null` 会抛 ZodError → 登录静默失败。 stringOrEmpty,
* 用 `z.string().nullable().transform(v => v ?? "").default("")` 模式: } from "../nullable-defaults";
* - input 接受 `string | null | undefined`(保持 optional
* - output 永远是 `string`User 类的 `email: string` 声明不会破)
*
* 这是前端防御性解析(不要求后端修改)—— 和后端对齐更稳。
*/
const stringOrEmpty = z.string().nullable().transform((v) => v ?? "").default("");
const numberOrZero = z.number().nullable().transform((v) => v ?? 0).default(0);
const booleanOrFalse = z.boolean().nullable().transform((v) => v ?? false).default(false);
export const UserSchema = z.object({ export const UserSchema = z.object({
id: stringOrEmpty, // 兜底:理论 id 不会 null,但保持防御 id: stringOrEmpty, // 兜底:理论 id 不会 null,但保持防御
+2
View File
@@ -4,5 +4,7 @@
export * from "./user-context"; export * from "./user-context";
export * from "./user-events"; export * from "./user-events";
export * from "./user-machine.actors";
export * from "./user-machine.helpers";
export * from "./user-machine"; export * from "./user-machine";
export * from "./user-state"; export * from "./user-state";