Files
multica/packages/core/api/schema.ts
Naiyuan Qing 3d7b2a6312 fix(teams): frontend data-layer fixes for team rollout
- export core teams subpath from package.json so team imports resolve
- invalidate team caches on realtime team events
- share team list query key; filter active teams in select
- optimistic update/archive team mutations with rollback
- seed create-project default team once, stop fighting deselection
- validate team mutation responses against TeamSchema
- add malformed-response tests for team schemas
- constrain issue/autopilot team pickers to the selected project's teams
- fix create-project / quick-create test mocks for the team picker
  (queryOptions export, quick-create store lastTeamId/setLastTeamId)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:31:36 +08:00

86 lines
2.9 KiB
TypeScript

import type { ZodType } from "zod";
import { type Logger, noopLogger } from "../logger";
// Module-level logger for schema warnings. Defaults to no-op so test
// runs don't spam stderr; the platform layer wires a real logger via
// `setSchemaLogger` at app boot.
let schemaLogger: Logger = noopLogger;
export function setSchemaLogger(logger: Logger): void {
schemaLogger = logger;
}
export interface ParseOptions {
/** Endpoint identifier used in the warning log so we can grep for which
* contract drifted in production telemetry. */
endpoint: string;
}
/**
* Validate a JSON value parsed from an API response against a zod schema,
* returning the parsed value on success or `fallback` on failure.
*
* On failure we log a warning with the endpoint and zod's structured error,
* but never throw — the UI layer must keep rendering. This is the boundary
* defense that turns "API contract drifted" from a white-screen incident
* into a degraded-but-rendering page.
*
* The return type is anchored to `T` (inferred from `fallback`), not to the
* schema's `z.infer` type. Schemas are intentionally **lenient** — string
* enums kept as `z.string()` so an unknown enum value still parses, etc. —
* so the parsed runtime value can be wider than the strict TS type at the
* call site. The caller asserts compatibility by typing the fallback to the
* expected `T`; downstream code is already responsible for handling unknown
* enum values via `default`-bearing switches and optional chaining.
*
* See CLAUDE.md "API Response Compatibility" for when to reach for this.
*/
export function parseWithFallback<T>(
data: unknown,
schema: ZodType,
fallback: T,
opts: ParseOptions,
): T {
const result = schema.safeParse(data);
if (result.success) return result.data as T;
schemaLogger.warn(
`API response failed schema validation: ${opts.endpoint}`,
{
endpoint: opts.endpoint,
issues: result.error.issues,
received: data,
},
);
return fallback;
}
/**
* Validate a mutation response against a zod schema, warning (not throwing) on
* drift. Unlike `parseWithFallback`, there is no synthetic fallback: a write
* response has no safe empty stand-in — an `id: ""` placeholder would corrupt
* the caches these responses feed. On drift we log via the same channel as
* `parseWithFallback` and return the raw value cast to `T`; the caller's
* optimistic cache patch plus `onSettled` invalidation remain the authoritative
* safety net.
*
* See CLAUDE.md "API Compatibility" for when to reach for this over
* `parseWithFallback`.
*/
export function parseOrWarn<T>(
data: unknown,
schema: ZodType,
opts: ParseOptions,
): T {
const result = schema.safeParse(data);
if (result.success) return result.data as T;
schemaLogger.warn(
`API response failed schema validation: ${opts.endpoint}`,
{
endpoint: opts.endpoint,
issues: result.error.issues,
received: data,
},
);
return data as T;
}