mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-30 19:36:25 +02:00
feat: implement Zod schemas for runtime validation across various components to enhance data integrity and error handling
This commit is contained in:
parent
7ca490c740
commit
5fd872b798
6 changed files with 253 additions and 214 deletions
|
|
@ -17,6 +17,33 @@ import {
|
|||
UserIcon,
|
||||
} from "lucide-react";
|
||||
import { Component, type ReactNode, useCallback } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Zod schema for serializable article data (from backend)
|
||||
*/
|
||||
const SerializableArticleSchema = z.object({
|
||||
id: z.string().default("article-unknown"),
|
||||
assetId: z.string().optional(),
|
||||
kind: z.literal("article").optional(),
|
||||
title: z.string().default("Untitled Article"),
|
||||
description: z.string().optional(),
|
||||
content: z.string().optional(),
|
||||
href: z.string().url().optional(),
|
||||
domain: z.string().optional(),
|
||||
author: z.string().optional(),
|
||||
date: z.string().optional(),
|
||||
word_count: z.number().optional(),
|
||||
wordCount: z.number().optional(),
|
||||
was_truncated: z.boolean().optional(),
|
||||
wasTruncated: z.boolean().optional(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Serializable article data type (from backend)
|
||||
*/
|
||||
export type SerializableArticle = z.infer<typeof SerializableArticleSchema>;
|
||||
|
||||
/**
|
||||
* Article component props
|
||||
|
|
@ -61,44 +88,36 @@ export interface ArticleProps {
|
|||
}
|
||||
|
||||
/**
|
||||
* Serializable article data type (from backend)
|
||||
*/
|
||||
export interface SerializableArticle {
|
||||
id: string;
|
||||
assetId?: string;
|
||||
kind?: "article";
|
||||
title: string;
|
||||
description?: string;
|
||||
content?: string;
|
||||
href?: string;
|
||||
domain?: string;
|
||||
author?: string;
|
||||
date?: string;
|
||||
word_count?: number;
|
||||
wordCount?: number;
|
||||
was_truncated?: boolean;
|
||||
wasTruncated?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse serializable article data to ArticleProps
|
||||
* Parse and validate serializable article data to ArticleProps
|
||||
*/
|
||||
export function parseSerializableArticle(data: unknown): ArticleProps {
|
||||
const obj = data as Record<string, unknown>;
|
||||
const result = SerializableArticleSchema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
console.warn("Invalid article data:", result.error.issues);
|
||||
// Return fallback with basic info
|
||||
const obj = (data && typeof data === "object" ? data : {}) as Record<string, unknown>;
|
||||
return {
|
||||
id: String(obj.id || "article-unknown"),
|
||||
title: String(obj.title || "Untitled Article"),
|
||||
error: "Failed to parse article data",
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = result.data;
|
||||
return {
|
||||
id: String(obj.id || "article-unknown"),
|
||||
assetId: obj.assetId as string | undefined,
|
||||
title: String(obj.title || "Untitled Article"),
|
||||
description: obj.description as string | undefined,
|
||||
content: obj.content as string | undefined,
|
||||
href: obj.href as string | undefined,
|
||||
domain: obj.domain as string | undefined,
|
||||
author: obj.author as string | undefined,
|
||||
date: obj.date as string | undefined,
|
||||
wordCount: (obj.word_count || obj.wordCount) as number | undefined,
|
||||
wasTruncated: (obj.was_truncated || obj.wasTruncated) as boolean | undefined,
|
||||
error: obj.error as string | undefined,
|
||||
id: parsed.id,
|
||||
assetId: parsed.assetId,
|
||||
title: parsed.title,
|
||||
description: parsed.description,
|
||||
content: parsed.content,
|
||||
href: parsed.href,
|
||||
domain: parsed.domain,
|
||||
author: parsed.author,
|
||||
date: parsed.date,
|
||||
wordCount: parsed.word_count ?? parsed.wordCount,
|
||||
wasTruncated: parsed.was_truncated ?? parsed.wasTruncated,
|
||||
error: parsed.error,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { Brain, CheckCircle2, Loader2, Search, Sparkles } from "lucide-react";
|
||||
import { useMemo, useState, useEffect, useRef } from "react";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
ChainOfThought,
|
||||
ChainOfThoughtContent,
|
||||
|
|
@ -13,24 +14,61 @@ import {
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Types for the deepagent thinking/reasoning tool
|
||||
* Zod schemas for runtime validation
|
||||
*/
|
||||
interface ThinkingStep {
|
||||
id: string;
|
||||
title: string;
|
||||
items: string[];
|
||||
status: "pending" | "in_progress" | "completed";
|
||||
const ThinkingStepSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
items: z.array(z.string()).default([]),
|
||||
status: z.enum(["pending", "in_progress", "completed"]).default("pending"),
|
||||
});
|
||||
|
||||
const DeepAgentThinkingArgsSchema = z.object({
|
||||
query: z.string().optional(),
|
||||
context: z.string().optional(),
|
||||
});
|
||||
|
||||
const DeepAgentThinkingResultSchema = z.object({
|
||||
steps: z.array(ThinkingStepSchema).optional(),
|
||||
status: z.enum(["thinking", "searching", "synthesizing", "completed"]).optional(),
|
||||
summary: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Types derived from Zod schemas
|
||||
*/
|
||||
type ThinkingStep = z.infer<typeof ThinkingStepSchema>;
|
||||
type DeepAgentThinkingArgs = z.infer<typeof DeepAgentThinkingArgsSchema>;
|
||||
type DeepAgentThinkingResult = z.infer<typeof DeepAgentThinkingResultSchema>;
|
||||
|
||||
/**
|
||||
* Parse and validate a single thinking step
|
||||
*/
|
||||
export function parseThinkingStep(data: unknown): ThinkingStep {
|
||||
const result = ThinkingStepSchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
console.warn("Invalid thinking step data:", result.error.issues);
|
||||
// Return a fallback step
|
||||
return {
|
||||
id: "unknown",
|
||||
title: "Processing...",
|
||||
items: [],
|
||||
status: "pending",
|
||||
};
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
interface DeepAgentThinkingArgs {
|
||||
query?: string;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
interface DeepAgentThinkingResult {
|
||||
steps?: ThinkingStep[];
|
||||
status?: "thinking" | "searching" | "synthesizing" | "completed";
|
||||
summary?: string;
|
||||
/**
|
||||
* Parse and validate thinking result
|
||||
*/
|
||||
export function parseThinkingResult(data: unknown): DeepAgentThinkingResult {
|
||||
const result = DeepAgentThinkingResultSchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
console.warn("Invalid thinking result data:", result.error.issues);
|
||||
return {};
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ function ImageCancelledState({ src }: { src: string }) {
|
|||
|
||||
/**
|
||||
* Parsed Image component with error handling
|
||||
* Note: Image component has built-in click handling via href/src,
|
||||
* so no additional responseActions needed.
|
||||
*/
|
||||
function ParsedImage({ result }: { result: unknown }) {
|
||||
const image = parseSerializableImage(result);
|
||||
|
|
@ -75,14 +77,6 @@ function ParsedImage({ result }: { result: unknown }) {
|
|||
<Image
|
||||
{...image}
|
||||
maxWidth="420px"
|
||||
responseActions={[
|
||||
{ id: "open", label: "Open", variant: "default" },
|
||||
]}
|
||||
onResponseAction={(id) => {
|
||||
if (id === "open" && image.src) {
|
||||
window.open(image.src, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,37 +3,79 @@
|
|||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { AlertCircleIcon, Loader2Icon, MicIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { Audio } from "@/components/tool-ui/audio";
|
||||
import { baseApiService } from "@/lib/apis/base-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { clearActivePodcastTaskId, setActivePodcastTaskId } from "@/lib/chat/podcast-state";
|
||||
|
||||
/**
|
||||
* Type definitions for the generate_podcast tool
|
||||
* Zod schemas for runtime validation
|
||||
*/
|
||||
interface GeneratePodcastArgs {
|
||||
source_content: string;
|
||||
podcast_title?: string;
|
||||
user_prompt?: string;
|
||||
const GeneratePodcastArgsSchema = z.object({
|
||||
source_content: z.string(),
|
||||
podcast_title: z.string().optional(),
|
||||
user_prompt: z.string().optional(),
|
||||
});
|
||||
|
||||
const GeneratePodcastResultSchema = z.object({
|
||||
status: z.enum(["processing", "already_generating", "success", "error"]),
|
||||
task_id: z.string().optional(),
|
||||
podcast_id: z.number().optional(),
|
||||
title: z.string().optional(),
|
||||
transcript_entries: z.number().optional(),
|
||||
message: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
const TaskStatusResponseSchema = z.object({
|
||||
status: z.enum(["processing", "success", "error"]),
|
||||
podcast_id: z.number().optional(),
|
||||
title: z.string().optional(),
|
||||
transcript_entries: z.number().optional(),
|
||||
state: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
const PodcastTranscriptEntrySchema = z.object({
|
||||
speaker_id: z.number(),
|
||||
dialog: z.string(),
|
||||
});
|
||||
|
||||
const PodcastDetailsSchema = z.object({
|
||||
podcast_transcript: z.array(PodcastTranscriptEntrySchema).optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Types derived from Zod schemas
|
||||
*/
|
||||
type GeneratePodcastArgs = z.infer<typeof GeneratePodcastArgsSchema>;
|
||||
type GeneratePodcastResult = z.infer<typeof GeneratePodcastResultSchema>;
|
||||
type TaskStatusResponse = z.infer<typeof TaskStatusResponseSchema>;
|
||||
type PodcastTranscriptEntry = z.infer<typeof PodcastTranscriptEntrySchema>;
|
||||
|
||||
/**
|
||||
* Parse and validate task status response
|
||||
*/
|
||||
function parseTaskStatusResponse(data: unknown): TaskStatusResponse {
|
||||
const result = TaskStatusResponseSchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
console.warn("Invalid task status response:", result.error.issues);
|
||||
return { status: "error", error: "Invalid response from server" };
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
interface GeneratePodcastResult {
|
||||
status: "processing" | "already_generating" | "success" | "error";
|
||||
task_id?: string;
|
||||
podcast_id?: number;
|
||||
title?: string;
|
||||
transcript_entries?: number;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface TaskStatusResponse {
|
||||
status: "processing" | "success" | "error";
|
||||
podcast_id?: number;
|
||||
title?: string;
|
||||
transcript_entries?: number;
|
||||
state?: string;
|
||||
error?: string;
|
||||
/**
|
||||
* Parse and validate podcast details
|
||||
*/
|
||||
function parsePodcastDetails(data: unknown): { podcast_transcript?: PodcastTranscriptEntry[] } {
|
||||
const result = PodcastDetailsSchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
console.warn("Invalid podcast details:", result.error.issues);
|
||||
return {};
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -112,14 +154,6 @@ function AudioLoadingState({ title }: { title: string }) {
|
|||
/**
|
||||
* Podcast Player Component - Fetches audio and transcript with authentication
|
||||
*/
|
||||
/**
|
||||
* Transcript entry type for podcast transcripts
|
||||
*/
|
||||
interface PodcastTranscriptEntry {
|
||||
speaker_id: number;
|
||||
dialog: string;
|
||||
}
|
||||
|
||||
function PodcastPlayer({
|
||||
podcastId,
|
||||
title,
|
||||
|
|
@ -163,12 +197,12 @@ function PodcastPlayer({
|
|||
|
||||
try {
|
||||
// Fetch audio blob and podcast details in parallel
|
||||
const [audioResponse, podcastDetails] = await Promise.all([
|
||||
const [audioResponse, rawPodcastDetails] = await Promise.all([
|
||||
authenticatedFetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/${podcastId}/audio`,
|
||||
{ method: "GET", signal: controller.signal }
|
||||
),
|
||||
baseApiService.get<{ podcast_transcript?: PodcastTranscriptEntry[] }>(
|
||||
baseApiService.get<unknown>(
|
||||
`/api/v1/podcasts/${podcastId}`
|
||||
),
|
||||
]);
|
||||
|
|
@ -184,8 +218,9 @@ function PodcastPlayer({
|
|||
objectUrlRef.current = objectUrl;
|
||||
setAudioSrc(objectUrl);
|
||||
|
||||
// Set transcript from podcast details
|
||||
if (podcastDetails?.podcast_transcript) {
|
||||
// Parse and validate podcast details, then set transcript
|
||||
const podcastDetails = parsePodcastDetails(rawPodcastDetails);
|
||||
if (podcastDetails.podcast_transcript) {
|
||||
setTranscript(podcastDetails.podcast_transcript);
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -268,9 +303,10 @@ function PodcastTaskPoller({ taskId, title }: { taskId: string; title: string })
|
|||
useEffect(() => {
|
||||
const pollStatus = async () => {
|
||||
try {
|
||||
const response = await baseApiService.get<TaskStatusResponse>(
|
||||
const rawResponse = await baseApiService.get<unknown>(
|
||||
`/api/v1/podcasts/task/${taskId}/status`
|
||||
);
|
||||
const response = parseTaskStatusResponse(rawResponse);
|
||||
setTaskStatus(response);
|
||||
|
||||
// Stop polling if task is complete or errored
|
||||
|
|
|
|||
|
|
@ -3,28 +3,43 @@
|
|||
import { ExternalLinkIcon, ImageIcon, Loader2 } from "lucide-react";
|
||||
import NextImage from "next/image";
|
||||
import { Component, type ReactNode, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Aspect ratio options for images
|
||||
* Zod schemas for runtime validation
|
||||
*/
|
||||
type AspectRatio = "1:1" | "4:3" | "16:9" | "9:16" | "auto";
|
||||
const AspectRatioSchema = z.enum(["1:1", "4:3", "16:9", "9:16", "auto"]);
|
||||
const ImageFitSchema = z.enum(["cover", "contain"]);
|
||||
|
||||
const ImageSourceSchema = z.object({
|
||||
label: z.string(),
|
||||
iconUrl: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
});
|
||||
|
||||
const SerializableImageSchema = z.object({
|
||||
id: z.string(),
|
||||
assetId: z.string(),
|
||||
src: z.string(),
|
||||
alt: z.string(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
href: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
ratio: AspectRatioSchema.optional(),
|
||||
source: ImageSourceSchema.optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Image fit options
|
||||
* Types derived from Zod schemas
|
||||
*/
|
||||
type ImageFit = "cover" | "contain";
|
||||
|
||||
/**
|
||||
* Source attribution
|
||||
*/
|
||||
interface ImageSource {
|
||||
label: string;
|
||||
iconUrl?: string;
|
||||
url?: string;
|
||||
}
|
||||
type AspectRatio = z.infer<typeof AspectRatioSchema>;
|
||||
type ImageFit = z.infer<typeof ImageFitSchema>;
|
||||
type ImageSource = z.infer<typeof ImageSourceSchema>;
|
||||
export type SerializableImage = z.infer<typeof SerializableImageSchema>;
|
||||
|
||||
/**
|
||||
* Props for the Image component
|
||||
|
|
@ -45,58 +60,20 @@ export interface ImageProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializable schema for Image props (for tool results)
|
||||
*/
|
||||
export interface SerializableImage {
|
||||
id: string;
|
||||
assetId: string;
|
||||
src: string;
|
||||
alt: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
href?: string;
|
||||
domain?: string;
|
||||
ratio?: AspectRatio;
|
||||
source?: ImageSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate serializable image from tool result
|
||||
*/
|
||||
export function parseSerializableImage(result: unknown): SerializableImage {
|
||||
if (typeof result !== "object" || result === null) {
|
||||
throw new Error("Invalid image result: expected object");
|
||||
const parsed = SerializableImageSchema.safeParse(result);
|
||||
|
||||
if (!parsed.success) {
|
||||
console.warn("Invalid image data:", parsed.error.issues);
|
||||
// Try to extract basic info for error display
|
||||
const obj = (result && typeof result === "object" ? result : {}) as Record<string, unknown>;
|
||||
throw new Error(`Invalid image: ${parsed.error.issues.map(i => i.message).join(", ")}`);
|
||||
}
|
||||
|
||||
const obj = result as Record<string, unknown>;
|
||||
|
||||
// Validate required fields
|
||||
if (typeof obj.id !== "string") {
|
||||
throw new Error("Invalid image: missing id");
|
||||
}
|
||||
if (typeof obj.assetId !== "string") {
|
||||
throw new Error("Invalid image: missing assetId");
|
||||
}
|
||||
if (typeof obj.src !== "string") {
|
||||
throw new Error("Invalid image: missing src");
|
||||
}
|
||||
if (typeof obj.alt !== "string") {
|
||||
throw new Error("Invalid image: missing alt");
|
||||
}
|
||||
|
||||
return {
|
||||
id: obj.id,
|
||||
assetId: obj.assetId,
|
||||
src: obj.src,
|
||||
alt: obj.alt,
|
||||
title: typeof obj.title === "string" ? obj.title : undefined,
|
||||
description: typeof obj.description === "string" ? obj.description : undefined,
|
||||
href: typeof obj.href === "string" ? obj.href : undefined,
|
||||
domain: typeof obj.domain === "string" ? obj.domain : undefined,
|
||||
ratio: typeof obj.ratio === "string" ? (obj.ratio as AspectRatio) : undefined,
|
||||
source: typeof obj.source === "object" && obj.source !== null ? (obj.source as ImageSource) : undefined,
|
||||
};
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { ExternalLinkIcon, Globe, ImageIcon, LinkIcon, Loader2 } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { Component, type ReactNode } from "react";
|
||||
import { z } from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
|
@ -10,24 +11,38 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Aspect ratio options for media cards
|
||||
* Zod schemas for runtime validation
|
||||
*/
|
||||
type AspectRatio = "1:1" | "4:3" | "16:9" | "21:9" | "auto";
|
||||
const AspectRatioSchema = z.enum(["1:1", "4:3", "16:9", "21:9", "auto"]);
|
||||
const MediaCardKindSchema = z.enum(["link", "image", "video", "audio"]);
|
||||
|
||||
const ResponseActionSchema = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
variant: z.enum(["default", "secondary", "outline", "destructive", "ghost"]).optional(),
|
||||
confirmLabel: z.string().optional(),
|
||||
});
|
||||
|
||||
const SerializableMediaCardSchema = z.object({
|
||||
id: z.string(),
|
||||
assetId: z.string(),
|
||||
kind: MediaCardKindSchema,
|
||||
href: z.string().optional(),
|
||||
src: z.string().optional(),
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
thumb: z.string().optional(),
|
||||
ratio: AspectRatioSchema.optional(),
|
||||
domain: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* MediaCard kind - determines the display style
|
||||
* Types derived from Zod schemas
|
||||
*/
|
||||
type MediaCardKind = "link" | "image" | "video" | "audio";
|
||||
|
||||
/**
|
||||
* Response action configuration
|
||||
*/
|
||||
interface ResponseAction {
|
||||
id: string;
|
||||
label: string;
|
||||
variant?: "default" | "secondary" | "outline" | "destructive" | "ghost";
|
||||
confirmLabel?: string;
|
||||
}
|
||||
type AspectRatio = z.infer<typeof AspectRatioSchema>;
|
||||
type MediaCardKind = z.infer<typeof MediaCardKindSchema>;
|
||||
type ResponseAction = z.infer<typeof ResponseActionSchema>;
|
||||
export type SerializableMediaCard = z.infer<typeof SerializableMediaCardSchema>;
|
||||
|
||||
/**
|
||||
* Props for the MediaCard component
|
||||
|
|
@ -50,58 +65,18 @@ export interface MediaCardProps {
|
|||
onResponseAction?: (id: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializable schema for MediaCard props (for tool results)
|
||||
*/
|
||||
export interface SerializableMediaCard {
|
||||
id: string;
|
||||
assetId: string;
|
||||
kind: MediaCardKind;
|
||||
href?: string;
|
||||
src?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
thumb?: string;
|
||||
ratio?: AspectRatio;
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate serializable media card from tool result
|
||||
*/
|
||||
export function parseSerializableMediaCard(result: unknown): SerializableMediaCard {
|
||||
if (typeof result !== "object" || result === null) {
|
||||
throw new Error("Invalid media card result: expected object");
|
||||
const parsed = SerializableMediaCardSchema.safeParse(result);
|
||||
|
||||
if (!parsed.success) {
|
||||
console.warn("Invalid media card data:", parsed.error.issues);
|
||||
throw new Error(`Invalid media card: ${parsed.error.issues.map(i => i.message).join(", ")}`);
|
||||
}
|
||||
|
||||
const obj = result as Record<string, unknown>;
|
||||
|
||||
// Validate required fields
|
||||
if (typeof obj.id !== "string") {
|
||||
throw new Error("Invalid media card: missing id");
|
||||
}
|
||||
if (typeof obj.assetId !== "string") {
|
||||
throw new Error("Invalid media card: missing assetId");
|
||||
}
|
||||
if (typeof obj.kind !== "string") {
|
||||
throw new Error("Invalid media card: missing kind");
|
||||
}
|
||||
if (typeof obj.title !== "string") {
|
||||
throw new Error("Invalid media card: missing title");
|
||||
}
|
||||
|
||||
return {
|
||||
id: obj.id,
|
||||
assetId: obj.assetId,
|
||||
kind: obj.kind as MediaCardKind,
|
||||
href: typeof obj.href === "string" ? obj.href : undefined,
|
||||
src: typeof obj.src === "string" ? obj.src : undefined,
|
||||
title: obj.title,
|
||||
description: typeof obj.description === "string" ? obj.description : undefined,
|
||||
thumb: typeof obj.thumb === "string" ? obj.thumb : undefined,
|
||||
ratio: typeof obj.ratio === "string" ? (obj.ratio as AspectRatio) : undefined,
|
||||
domain: typeof obj.domain === "string" ? obj.domain : undefined,
|
||||
};
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue