import { z } from "zod"; import { imageTable } from "../db/schema.ts"; export const PersonSchema = z.object({ _type: z.literal("Person"), name: z.string().optional(), }); export const ReviewRatingSchema = z.object({ bestRating: z.number().optional(), worstRating: z.number().optional(), // Accept number or string (e.g., "⭐️⭐️⭐️⭐️⭐️") ratingValue: z.union([z.number(), z.string()]).optional(), }); const WithAuthor = z.object({ author: PersonSchema.optional() }); const WithKeywords = z.object({ keywords: z.array(z.string()).optional() }); const WithImage = z.object({ image: z.string().optional() }); const WithDatePublished = z.object({ datePublished: z.string().optional() }); const BaseContent = WithAuthor.merge(WithKeywords) .merge(WithImage) .merge(WithDatePublished); export const BaseFileSchema = z.object({ type: z.literal("file"), name: z.string(), path: z.string(), modTime: z.string(), // ISO timestamp string mime: z.string(), size: z.number().int().nonnegative(), }); const makeContentSchema = < TName extends "Article" | "Review" | "Recipe", TShape extends z.ZodRawShape, >( name: TName, shape: TShape, ) => z .object({ _type: z.literal(name), keywords: z.array(z.string()).optional(), }) .merge(BaseContent) .extend(shape); export const ArticleContentSchema = makeContentSchema("Article", { headline: z.string().optional(), articleBody: z.string().optional(), url: z.string().optional(), reviewRating: ReviewRatingSchema.optional(), }); export const ReviewContentSchema = makeContentSchema("Review", { tmdbId: z.number().optional(), link: z.string().optional(), reviewRating: ReviewRatingSchema.optional(), reviewBody: z.string().optional(), itemReviewed: z .object({ name: z.string().optional(), }) .optional(), }); export const RecipeContentSchema = makeContentSchema("Recipe", { description: z.string().optional(), name: z.string().optional(), reviewRating: ReviewRatingSchema.optional(), recipeIngredient: z.array(z.string()).optional(), recipeInstructions: z.array(z.string()).optional(), totalTime: z.string().optional(), recipeYield: z.number().optional(), url: z.string().optional(), }); export const articleMetadataSchema = z.object({ headline: z.union([z.null(), z.string()]).describe("Headline of the article"), author: z.union([z.null(), z.string()]).describe("Author of the article"), datePublished: z.union([z.null(), z.string()]).describe( "Date the article was published", ), keywords: z.union([z.null(), z.array(z.string())]).describe( "Keywords for the article", ), }); export const ArticleSchema = BaseFileSchema.extend({ content: ArticleContentSchema, }); export const ReviewSchema = BaseFileSchema.extend({ content: ReviewContentSchema, }); export const RecipeSchema = BaseFileSchema.extend({ content: RecipeContentSchema, }); export const GenericResourceSchema = z.union([ ArticleSchema, ReviewSchema, RecipeSchema, ]); export type Person = z.infer; export type ReviewRating = z.infer; export type BaseFile = z.infer; export type ArticleResource = z.infer & { image?: typeof imageTable.$inferSelect; }; export type ReviewResource = z.infer & { image?: typeof imageTable.$inferSelect; }; export type RecipeResource = z.infer & { image?: typeof imageTable.$inferSelect; }; export type GenericResource = z.infer & { image?: typeof imageTable.$inferSelect; }; export function getNameOfResource(res: GenericResource): string { if (res.content?._type === "Article" && res.content.headline) { return res.content.headline; } if (res.content?._type === "Review" && res.content.itemReviewed?.name) { return res.content.itemReviewed.name; } if (res.content?._type === "Recipe" && res.content.name) { return res.content.name; } return "Unnamed Resource"; }